commit
3f2cb90340
|
@ -38,16 +38,19 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@beyonk/svelte-notifications": "^2.0.3",
|
||||||
"@budibase/bbui": "^0.3.5",
|
"@budibase/bbui": "^0.3.5",
|
||||||
"@budibase/client": "^0.0.32",
|
"@budibase/client": "^0.0.32",
|
||||||
"@nx-js/compiler-util": "^2.0.0",
|
"@nx-js/compiler-util": "^2.0.0",
|
||||||
"codemirror": "^5.51.0",
|
"codemirror": "^5.51.0",
|
||||||
"date-fns": "^1.29.0",
|
"date-fns": "^1.29.0",
|
||||||
|
"deepmerge": "^4.2.2",
|
||||||
"feather-icons": "^4.21.0",
|
"feather-icons": "^4.21.0",
|
||||||
"flatpickr": "^4.5.7",
|
"flatpickr": "^4.5.7",
|
||||||
"lodash": "^4.17.13",
|
"lodash": "^4.17.13",
|
||||||
"logrocket": "^1.0.6",
|
"logrocket": "^1.0.6",
|
||||||
"lunr": "^2.3.5",
|
"lunr": "^2.3.5",
|
||||||
|
"mustache": "^4.0.1",
|
||||||
"safe-buffer": "^5.1.2",
|
"safe-buffer": "^5.1.2",
|
||||||
"shortid": "^2.2.8",
|
"shortid": "^2.2.8",
|
||||||
"string_decoder": "^1.2.0",
|
"string_decoder": "^1.2.0",
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
import AppNotification, {
|
import AppNotification, {
|
||||||
showAppNotification,
|
showAppNotification,
|
||||||
} from "components/common/AppNotification.svelte"
|
} from "components/common/AppNotification.svelte"
|
||||||
|
import { NotificationDisplay } from "@beyonk/svelte-notifications"
|
||||||
|
|
||||||
function showErrorBanner() {
|
function showErrorBanner() {
|
||||||
showAppNotification({
|
showAppNotification({
|
||||||
|
@ -26,4 +27,7 @@
|
||||||
|
|
||||||
<AppNotification />
|
<AppNotification />
|
||||||
|
|
||||||
|
<!-- svelte-notifications -->
|
||||||
|
<NotificationDisplay />
|
||||||
|
|
||||||
<Router {routes} />
|
<Router {routes} />
|
||||||
|
|
|
@ -77,7 +77,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.budibase__input {
|
.budibase__input {
|
||||||
width: 250px;
|
width: 100%;
|
||||||
|
max-width: 250px;
|
||||||
height: 35px;
|
height: 35px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
border: 1px solid #DBDBDB;
|
border: 1px solid #DBDBDB;
|
||||||
|
|
|
@ -18,10 +18,12 @@ const post = apiCall("POST")
|
||||||
const get = apiCall("GET")
|
const get = apiCall("GET")
|
||||||
const patch = apiCall("PATCH")
|
const patch = apiCall("PATCH")
|
||||||
const del = apiCall("DELETE")
|
const del = apiCall("DELETE")
|
||||||
|
const put = apiCall("PUT")
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
post,
|
post,
|
||||||
get,
|
get,
|
||||||
patch,
|
patch,
|
||||||
delete: del,
|
delete: del,
|
||||||
|
put,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { getStore } from "./store"
|
import { getStore } from "./store"
|
||||||
import { getBackendUiStore } from "./store/backend"
|
import { getBackendUiStore } from "./store/backend"
|
||||||
|
import { getWorkflowStore } from "./store/workflow/"
|
||||||
import LogRocket from "logrocket"
|
import LogRocket from "logrocket"
|
||||||
|
|
||||||
export const store = getStore()
|
export const store = getStore()
|
||||||
export const backendUiStore = getBackendUiStore()
|
export const backendUiStore = getBackendUiStore()
|
||||||
|
export const workflowStore = getWorkflowStore()
|
||||||
|
|
||||||
export const initialise = async () => {
|
export const initialise = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -156,7 +156,6 @@ const createScreen = store => (screenName, route, layoutComponentName) => {
|
||||||
description: "",
|
description: "",
|
||||||
url: "",
|
url: "",
|
||||||
_css: "",
|
_css: "",
|
||||||
uiFunctions: "",
|
|
||||||
props: createProps(rootComponent).props,
|
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 { eventHandlers } from "../../../../client/src/state/eventHandlers"
|
||||||
import { writable } from "svelte/store"
|
|
||||||
export { EVENT_TYPE_MEMBER_NAME } from "../../../../client/src/state/eventHandlers"
|
export { EVENT_TYPE_MEMBER_NAME } from "../../../../client/src/state/eventHandlers"
|
||||||
|
|
||||||
export const allHandlers = user => {
|
export const allHandlers = () => {
|
||||||
const store = writable({
|
const handlersObj = eventHandlers()
|
||||||
_bbuser: user,
|
|
||||||
})
|
|
||||||
|
|
||||||
const handlersObj = eventHandlers(store)
|
|
||||||
|
|
||||||
const handlers = Object.keys(handlersObj).map(name => ({
|
const handlers = Object.keys(handlersObj).map(name => ({
|
||||||
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
|
const ITEMS_PER_PAGE = 10
|
||||||
// Internal headers we want to hide from the user
|
// Internal headers we want to hide from the user
|
||||||
const INTERNAL_HEADERS = ["_id", "_rev", "modelId", "type"]
|
const INTERNAL_HEADERS = ["_id", "_rev", "modelId", "type"]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import api from "builderStore/api"
|
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 CREATE_USER_URL = `/api/${instanceId}/users`
|
||||||
const response = await api.post(CREATE_USER_URL, user)
|
const response = await api.post(CREATE_USER_URL, user)
|
||||||
return await response.json()
|
return await response.json()
|
||||||
|
@ -28,7 +28,7 @@ export async function saveRecord(record, instanceId, modelId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchDataForView(viewName, instanceId) {
|
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)
|
const response = await api.get(FETCH_RECORDS_URL)
|
||||||
return await response.json()
|
return await response.json()
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<heading>
|
<header>
|
||||||
{#if !showFieldView}
|
{#if !showFieldView}
|
||||||
<i class="ri-list-settings-line button--toggled" />
|
<i class="ri-list-settings-line button--toggled" />
|
||||||
<h3 class="budibase__title--3">Create / Edit Model</h3>
|
<h3 class="budibase__title--3">Create / Edit Model</h3>
|
||||||
|
@ -43,7 +43,7 @@
|
||||||
<i class="ri-file-list-line button--toggled" />
|
<i class="ri-file-list-line button--toggled" />
|
||||||
<h3 class="budibase__title--3">Create / Edit Field</h3>
|
<h3 class="budibase__title--3">Create / Edit Field</h3>
|
||||||
{/if}
|
{/if}
|
||||||
</heading>
|
</header>
|
||||||
{#if !showFieldView}
|
{#if !showFieldView}
|
||||||
<div class="padding">
|
<div class="padding">
|
||||||
<h4 class="budibase__label--big">Settings</h4>
|
<h4 class="budibase__label--big">Settings</h4>
|
||||||
|
|
|
@ -64,7 +64,6 @@
|
||||||
<NumberBox label="Max Length" bind:value={constraints.length.maximum} />
|
<NumberBox label="Max Length" bind:value={constraints.length.maximum} />
|
||||||
<ValuesList label="Categories" bind:values={constraints.inclusion} />
|
<ValuesList label="Categories" bind:values={constraints.inclusion} />
|
||||||
{:else if type === 'datetime'}
|
{:else if type === 'datetime'}
|
||||||
<!-- TODO: revisit and fix with JSON schema -->
|
|
||||||
<DatePicker
|
<DatePicker
|
||||||
label="Min Value"
|
label="Min Value"
|
||||||
bind:value={constraints.datetime.earliest} />
|
bind:value={constraints.datetime.earliest} />
|
||||||
|
|
|
@ -7,14 +7,15 @@
|
||||||
|
|
||||||
let username
|
let username
|
||||||
let password
|
let password
|
||||||
|
let accessLevelId
|
||||||
|
|
||||||
$: valid = username && password
|
$: valid = username && password && accessLevelId
|
||||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
$: instanceId = $backendUiStore.selectedDatabase._id
|
||||||
$: appId = $store.appId
|
$: appId = $store.appId
|
||||||
|
|
||||||
async function createUser() {
|
async function createUser() {
|
||||||
const user = { name: username, username, password }
|
const user = { name: username, username, password, accessLevelId }
|
||||||
const response = await api.createUser(user, appId, instanceId)
|
const response = await api.createUser(user, instanceId)
|
||||||
backendUiStore.actions.users.create(response)
|
backendUiStore.actions.users.create(response)
|
||||||
onClosed()
|
onClosed()
|
||||||
}
|
}
|
||||||
|
@ -30,6 +31,14 @@
|
||||||
<label class="uk-form-label" for="form-stacked-text">Password</label>
|
<label class="uk-form-label" for="form-stacked-text">Password</label>
|
||||||
<input class="uk-input" type="password" bind:value={password} />
|
<input class="uk-input" type="password" bind:value={password} />
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<footer>
|
<footer>
|
||||||
<ActionButton alert on:click={onClosed}>Cancel</ActionButton>
|
<ActionButton alert on:click={onClosed}>Cancel</ActionButton>
|
||||||
|
|
|
@ -117,7 +117,6 @@
|
||||||
selectedComponentType,
|
selectedComponentType,
|
||||||
selectedComponentId,
|
selectedComponentId,
|
||||||
frontendDefinition: JSON.stringify(frontendDefinition),
|
frontendDefinition: JSON.stringify(frontendDefinition),
|
||||||
currentPageFunctions: $store.currentPageFunctions,
|
|
||||||
})} />
|
})} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,7 +4,6 @@ export default ({
|
||||||
selectedComponentType,
|
selectedComponentType,
|
||||||
selectedComponentId,
|
selectedComponentId,
|
||||||
frontendDefinition,
|
frontendDefinition,
|
||||||
currentPageFunctions,
|
|
||||||
}) => `<html>
|
}) => `<html>
|
||||||
<head>
|
<head>
|
||||||
${stylesheetLinks}
|
${stylesheetLinks}
|
||||||
|
@ -36,7 +35,6 @@ export default ({
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
window["##BUDIBASE_FRONTEND_DEFINITION##"] = ${frontendDefinition};
|
window["##BUDIBASE_FRONTEND_DEFINITION##"] = ${frontendDefinition};
|
||||||
window["##BUDIBASE_FRONTEND_FUNCTIONS##"] = ${currentPageFunctions};
|
|
||||||
|
|
||||||
import('/_builder/budibase-client.esm.mjs')
|
import('/_builder/budibase-client.esm.mjs')
|
||||||
.then(module => {
|
.then(module => {
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
let categories = [
|
let categories = [
|
||||||
{ value: "design", name: "Design" },
|
{ value: "design", name: "Design" },
|
||||||
{ value: "settings", name: "Settings" },
|
{ value: "settings", name: "Settings" },
|
||||||
{ value: "actions", name: "Actions" },
|
{ value: "events", name: "Events" },
|
||||||
]
|
]
|
||||||
let selectedCategory = categories[0]
|
let selectedCategory = categories[0]
|
||||||
|
|
||||||
|
@ -93,6 +93,8 @@
|
||||||
{componentDefinition}
|
{componentDefinition}
|
||||||
{panelDefinition}
|
{panelDefinition}
|
||||||
onChange={onPropChanged} />
|
onChange={onPropChanged} />
|
||||||
|
{:else if selectedCategory.value === 'events'}
|
||||||
|
<EventsEditor component={componentInstance} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -25,16 +25,15 @@
|
||||||
export const EVENT_TYPE = "event"
|
export const EVENT_TYPE = "event"
|
||||||
|
|
||||||
export let component
|
export let component
|
||||||
export let components
|
|
||||||
|
|
||||||
let modalOpen = false
|
let modalOpen = false
|
||||||
let events = []
|
let events = []
|
||||||
let selectedEvent = null
|
let selectedEvent = null
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
const componentDefinition = components[component._component]
|
events = Object.keys(component)
|
||||||
events = Object.keys(componentDefinition.props)
|
// TODO: use real events
|
||||||
.filter(propName => componentDefinition.props[propName] === EVENT_TYPE)
|
.filter(propName => ["onChange", "onClick", "onLoad"].includes(propName))
|
||||||
.map(propName => ({
|
.map(propName => ({
|
||||||
name: propName,
|
name: propName,
|
||||||
handlers: component[propName] || [],
|
handlers: component[propName] || [],
|
||||||
|
|
|
@ -5,12 +5,8 @@
|
||||||
import Input from "components/common/Input.svelte"
|
import Input from "components/common/Input.svelte"
|
||||||
import { find, map, keys, reduce, keyBy } from "lodash/fp"
|
import { find, map, keys, reduce, keyBy } from "lodash/fp"
|
||||||
import { pipe } from "components/common/core"
|
import { pipe } from "components/common/core"
|
||||||
import {
|
import { EVENT_TYPE_MEMBER_NAME } from "components/common/eventHandlers"
|
||||||
EVENT_TYPE_MEMBER_NAME,
|
import { store, workflowStore } from "builderStore"
|
||||||
allHandlers,
|
|
||||||
} from "components/common/eventHandlers"
|
|
||||||
import { store } from "builderStore"
|
|
||||||
import StateBindingOptions from "../PropertyCascader/StateBindingOptions.svelte"
|
|
||||||
import { ArrowDownIcon } from "components/common/Icons/"
|
import { ArrowDownIcon } from "components/common/Icons/"
|
||||||
|
|
||||||
export let parameter
|
export let parameter
|
||||||
|
@ -22,18 +18,22 @@
|
||||||
<div class="handler-option">
|
<div class="handler-option">
|
||||||
<span>{parameter.name}</span>
|
<span>{parameter.name}</span>
|
||||||
<div class="handler-input">
|
<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} />
|
<Input on:change={onChange} value={parameter.value} />
|
||||||
<button on:click={() => (isOpen = !isOpen)}>
|
<button on:click={() => (isOpen = !isOpen)}>
|
||||||
<div class="icon" style={`transform: rotate(${isOpen ? 0 : 90}deg);`}>
|
<div class="icon" style={`transform: rotate(${isOpen ? 0 : 90}deg);`}>
|
||||||
<ArrowDownIcon size={36} />
|
<ArrowDownIcon size={36} />
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{#if isOpen}
|
|
||||||
<StateBindingOptions
|
|
||||||
onSelect={option => {
|
|
||||||
onChange(option)
|
|
||||||
isOpen = false
|
|
||||||
}} />
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</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 { backendUiStore } from "builderStore"
|
||||||
import IconButton from "../common/IconButton.svelte"
|
import IconButton from "../common/IconButton.svelte"
|
||||||
import Input from "../common/Input.svelte"
|
import Input from "../common/Input.svelte"
|
||||||
import PropertyCascader from "./PropertyCascader"
|
|
||||||
import { isBinding, getBinding, setBinding } from "../common/binding"
|
|
||||||
import Colorpicker from "../common/Colorpicker.svelte"
|
import Colorpicker from "../common/Colorpicker.svelte"
|
||||||
|
|
||||||
export let value = ""
|
export let value = ""
|
||||||
|
@ -49,8 +47,6 @@
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
{:else}
|
|
||||||
<PropertyCascader {onChanged} {value} />
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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;
|
--background-button: #f9f9f9;
|
||||||
--button-text: #0055ff;
|
--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 {
|
html, body {
|
||||||
|
|
|
@ -87,6 +87,11 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-transform: none;
|
||||||
|
color: var(--ink-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
.top-nav {
|
.top-nav {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
height: 60px;
|
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 { createProps } from "../src/components/userInterface/pagesParsing/createProps"
|
||||||
import { keys, some } from "lodash/fp"
|
import { keys, some } from "lodash/fp"
|
||||||
import { BB_STATE_BINDINGPATH } from "@budibase/client/src/state/parseBinding"
|
|
||||||
import { stripStandardProps } from "./testData"
|
import { stripStandardProps } from "./testData"
|
||||||
|
|
||||||
describe("createDefaultProps", () => {
|
describe("createDefaultProps", () => {
|
||||||
|
@ -94,17 +93,6 @@ describe("createDefaultProps", () => {
|
||||||
expect(props.onClick).toEqual([])
|
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 ", () => {
|
it("should create a object children array when children == true ", () => {
|
||||||
const comp = getcomponent()
|
const comp = getcomponent()
|
||||||
comp.children = true
|
comp.children = true
|
||||||
|
|
|
@ -16,6 +16,5 @@
|
||||||
},
|
},
|
||||||
"_code": ""
|
"_code": ""
|
||||||
},
|
},
|
||||||
"_css": "",
|
"_css": ""
|
||||||
"uiFunctions": ""
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,5 @@
|
||||||
},
|
},
|
||||||
"_code": ""
|
"_code": ""
|
||||||
},
|
},
|
||||||
"_css": "",
|
"_css": ""
|
||||||
"uiFunctions": ""
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,5 @@
|
||||||
},
|
},
|
||||||
"_code": ""
|
"_code": ""
|
||||||
},
|
},
|
||||||
"_css": "",
|
"_css": ""
|
||||||
"uiFunctions": ""
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,7 @@
|
||||||
"deep-equal": "^2.0.1",
|
"deep-equal": "^2.0.1",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
"lunr": "^2.3.5",
|
"lunr": "^2.3.5",
|
||||||
|
"mustache": "^4.0.1",
|
||||||
"regexparam": "^1.3.0",
|
"regexparam": "^1.3.0",
|
||||||
"shortid": "^2.2.8",
|
"shortid": "^2.2.8",
|
||||||
"svelte": "^3.9.2"
|
"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 { authenticate } from "./authenticate"
|
||||||
import { saveRecord } from "./saveRecord"
|
import { triggerWorkflow } from "./workflow"
|
||||||
|
|
||||||
export const createApi = ({ rootPath = "", setState, getState }) => {
|
export const createApi = ({ rootPath = "", setState, getState }) => {
|
||||||
const apiCall = method => ({
|
const apiCall = method => async ({ url, body }) => {
|
||||||
url,
|
const response = await fetch(`${rootPath}${url}`, {
|
||||||
body,
|
|
||||||
notFound,
|
|
||||||
badRequest,
|
|
||||||
forbidden,
|
|
||||||
}) => {
|
|
||||||
return fetch(`${rootPath}${url}`, {
|
|
||||||
method: method,
|
method: method,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: body && JSON.stringify(body),
|
body: body && JSON.stringify(body),
|
||||||
credentials: "same-origin",
|
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")
|
const post = apiCall("POST")
|
||||||
|
@ -47,10 +37,9 @@ export const createApi = ({ rootPath = "", setState, getState }) => {
|
||||||
|
|
||||||
const ERROR_MEMBER = "##error"
|
const ERROR_MEMBER = "##error"
|
||||||
const error = message => {
|
const error = message => {
|
||||||
const e = {}
|
const err = { [ERROR_MEMBER]: message }
|
||||||
e[ERROR_MEMBER] = message
|
setState("##error_message", message)
|
||||||
setState(ERROR, message)
|
return err
|
||||||
return e
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSuccess = obj => !obj || !obj[ERROR_MEMBER]
|
const isSuccess = obj => !obj || !obj[ERROR_MEMBER]
|
||||||
|
@ -68,9 +57,7 @@ export const createApi = ({ rootPath = "", setState, getState }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loadRecord: loadRecord(apiOpts),
|
|
||||||
listRecords: listRecords(apiOpts),
|
|
||||||
authenticate: authenticate(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 { attachChildren } from "./render/attachChildren"
|
||||||
import { createTreeNode } from "./render/prepareRenderComponent"
|
import { createTreeNode } from "./render/prepareRenderComponent"
|
||||||
import { screenRouter } from "./render/screenRouter"
|
import { screenRouter } from "./render/screenRouter"
|
||||||
import { createStateManager } from "./state/stateManager"
|
import { createStateManager } from "./state/stateManager"
|
||||||
|
|
||||||
export const createApp = (
|
export const createApp = ({
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
frontendDefinition,
|
frontendDefinition,
|
||||||
user,
|
window,
|
||||||
uiFunctions,
|
}) => {
|
||||||
window
|
|
||||||
) => {
|
|
||||||
let routeTo
|
let routeTo
|
||||||
let currentUrl
|
let currentUrl
|
||||||
let screenStateManager
|
let screenStateManager
|
||||||
|
|
||||||
const onScreenSlotRendered = screenSlotNode => {
|
const onScreenSlotRendered = screenSlotNode => {
|
||||||
const onScreenSelected = (screen, store, url) => {
|
const onScreenSelected = (screen, url) => {
|
||||||
const stateManager = createStateManager({
|
const stateManager = createStateManager({
|
||||||
store,
|
|
||||||
frontendDefinition,
|
frontendDefinition,
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
uiFunctions,
|
|
||||||
onScreenSlotRendered: () => {},
|
onScreenSlotRendered: () => {},
|
||||||
routeTo,
|
routeTo,
|
||||||
appRootPath: frontendDefinition.appRootPath,
|
appRootPath: frontendDefinition.appRootPath,
|
||||||
|
@ -38,11 +33,11 @@ export const createApp = (
|
||||||
currentUrl = url
|
currentUrl = url
|
||||||
}
|
}
|
||||||
|
|
||||||
routeTo = screenRouter(
|
routeTo = screenRouter({
|
||||||
frontendDefinition.screens,
|
screens: frontendDefinition.screens,
|
||||||
onScreenSelected,
|
onScreenSelected,
|
||||||
frontendDefinition.appRootPath
|
appRootPath: frontendDefinition.appRootPath,
|
||||||
)
|
})
|
||||||
const fallbackPath = window.location.pathname.replace(
|
const fallbackPath = window.location.pathname.replace(
|
||||||
frontendDefinition.appRootPath,
|
frontendDefinition.appRootPath,
|
||||||
""
|
""
|
||||||
|
@ -53,7 +48,6 @@ export const createApp = (
|
||||||
const attachChildrenParams = stateManager => {
|
const attachChildrenParams = stateManager => {
|
||||||
const getInitialiseParams = treeNode => ({
|
const getInitialiseParams = treeNode => ({
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
uiFunctions,
|
|
||||||
treeNode,
|
treeNode,
|
||||||
onScreenSlotRendered,
|
onScreenSlotRendered,
|
||||||
setupState: stateManager.setup,
|
setupState: stateManager.setup,
|
||||||
|
@ -65,10 +59,8 @@ export const createApp = (
|
||||||
|
|
||||||
let rootTreeNode
|
let rootTreeNode
|
||||||
const pageStateManager = createStateManager({
|
const pageStateManager = createStateManager({
|
||||||
store: writable({ _bbuser: user }),
|
|
||||||
frontendDefinition,
|
frontendDefinition,
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
uiFunctions,
|
|
||||||
onScreenSlotRendered,
|
onScreenSlotRendered,
|
||||||
appRootPath: frontendDefinition.appRootPath,
|
appRootPath: frontendDefinition.appRootPath,
|
||||||
// seems weird, but the routeTo variable may not be available at this point
|
// seems weird, but the routeTo variable may not be available at this point
|
||||||
|
@ -82,7 +74,6 @@ export const createApp = (
|
||||||
rootTreeNode.props = {
|
rootTreeNode.props = {
|
||||||
_children: [page.props],
|
_children: [page.props],
|
||||||
}
|
}
|
||||||
rootTreeNode.rootElement = target
|
|
||||||
const getInitialiseParams = attachChildrenParams(pageStateManager)
|
const getInitialiseParams = attachChildrenParams(pageStateManager)
|
||||||
const initChildParams = getInitialiseParams(rootTreeNode)
|
const initChildParams = getInitialiseParams(rootTreeNode)
|
||||||
|
|
||||||
|
|
|
@ -10,9 +10,7 @@ export const loadBudibase = async opts => {
|
||||||
// const _localStorage = (opts && opts.localStorage) || localStorage
|
// const _localStorage = (opts && opts.localStorage) || localStorage
|
||||||
|
|
||||||
const frontendDefinition = _window["##BUDIBASE_FRONTEND_DEFINITION##"]
|
const frontendDefinition = _window["##BUDIBASE_FRONTEND_DEFINITION##"]
|
||||||
const uiFunctions = _window["##BUDIBASE_FRONTEND_FUNCTIONS##"]
|
|
||||||
|
|
||||||
// TODO: update
|
|
||||||
const user = {}
|
const user = {}
|
||||||
|
|
||||||
const componentLibraryModules = (opts && opts.componentLibraries) || {}
|
const componentLibraryModules = (opts && opts.componentLibraries) || {}
|
||||||
|
@ -36,14 +34,12 @@ export const loadBudibase = async opts => {
|
||||||
pageStore,
|
pageStore,
|
||||||
routeTo,
|
routeTo,
|
||||||
rootNode,
|
rootNode,
|
||||||
} = createApp(
|
} = createApp({
|
||||||
componentLibraryModules,
|
componentLibraries: componentLibraryModules,
|
||||||
frontendDefinition,
|
frontendDefinition,
|
||||||
user,
|
user,
|
||||||
uiFunctions || {},
|
window,
|
||||||
_window,
|
})
|
||||||
rootNode
|
|
||||||
)
|
|
||||||
|
|
||||||
const route = _window.location
|
const route = _window.location
|
||||||
? _window.location.pathname.replace(frontendDefinition.appRootPath, "")
|
? _window.location.pathname.replace(frontendDefinition.appRootPath, "")
|
||||||
|
|
|
@ -5,12 +5,10 @@ import deepEqual from "deep-equal"
|
||||||
|
|
||||||
export const attachChildren = initialiseOpts => (htmlElement, options) => {
|
export const attachChildren = initialiseOpts => (htmlElement, options) => {
|
||||||
const {
|
const {
|
||||||
uiFunctions,
|
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
treeNode,
|
treeNode,
|
||||||
onScreenSlotRendered,
|
onScreenSlotRendered,
|
||||||
setupState,
|
setupState,
|
||||||
getCurrentState,
|
|
||||||
} = initialiseOpts
|
} = initialiseOpts
|
||||||
|
|
||||||
const anchor = options && options.anchor ? options.anchor : null
|
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 = []
|
const childNodes = []
|
||||||
for (let childProps of treeNode.props._children) {
|
for (let childProps of treeNode.props._children) {
|
||||||
const { componentName, libName } = splitName(childProps._component)
|
const { componentName, libName } = splitName(childProps._component)
|
||||||
|
@ -45,10 +41,8 @@ export const attachChildren = initialiseOpts => (htmlElement, options) => {
|
||||||
props: childProps,
|
props: childProps,
|
||||||
parentNode: treeNode,
|
parentNode: treeNode,
|
||||||
ComponentConstructor,
|
ComponentConstructor,
|
||||||
uiFunctions,
|
|
||||||
htmlElement,
|
htmlElement,
|
||||||
anchor,
|
anchor,
|
||||||
getCurrentState,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
for (let childNode of childNodesThisIteration) {
|
for (let childNode of childNodesThisIteration) {
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
|
import { appStore } from "../state/store"
|
||||||
|
import mustache from "mustache"
|
||||||
|
|
||||||
export const prepareRenderComponent = ({
|
export const prepareRenderComponent = ({
|
||||||
ComponentConstructor,
|
ComponentConstructor,
|
||||||
uiFunctions,
|
|
||||||
htmlElement,
|
htmlElement,
|
||||||
anchor,
|
anchor,
|
||||||
props,
|
props,
|
||||||
parentNode,
|
parentNode,
|
||||||
getCurrentState,
|
|
||||||
}) => {
|
}) => {
|
||||||
const func = props._id ? uiFunctions[props._id] : undefined
|
|
||||||
|
|
||||||
const parentContext = (parentNode && parentNode.context) || {}
|
const parentContext = (parentNode && parentNode.context) || {}
|
||||||
|
|
||||||
let nodesToRender = []
|
let nodesToRender = []
|
||||||
|
@ -39,16 +38,27 @@ export const prepareRenderComponent = ({
|
||||||
if (props._id && thisNode.rootElement) {
|
if (props._id && thisNode.rootElement) {
|
||||||
thisNode.rootElement.classList.add(`${componentName}-${props._id}`)
|
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()
|
createNodeAndRender()
|
||||||
}
|
|
||||||
|
|
||||||
return nodesToRender
|
return nodesToRender
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import regexparam from "regexparam"
|
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 => {
|
const makeRootedPath = url => {
|
||||||
if (appRootPath) {
|
if (appRootPath) {
|
||||||
if (url) return `${appRootPath}${url.startsWith("/") ? "" : "/"}${url}`
|
if (url) return `${appRootPath}${url.startsWith("/") ? "" : "/"}${url}`
|
||||||
|
@ -40,13 +40,14 @@ export const screenRouter = (screens, onScreenSelected, appRootPath) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const storeInitial = {}
|
routerStore.update(state => {
|
||||||
storeInitial["##routeParams"] = params
|
state["##routeParams"] = params
|
||||||
const store = writable(storeInitial)
|
return state
|
||||||
|
})
|
||||||
|
|
||||||
const screenIndex = current !== -1 ? current : fallback
|
const screenIndex = current !== -1 ? current : fallback
|
||||||
|
|
||||||
onScreenSelected(screens[screenIndex], store, _url)
|
onScreenSelected(screens[screenIndex], _url)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
!url.state && history.pushState(_url, null, _url)
|
!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("popstate", route)
|
||||||
addEventListener("pushstate", route)
|
addEventListener("pushstate", route)
|
||||||
addEventListener("click", click)
|
|
||||||
|
|
||||||
return route
|
return route
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
import { getStateOrValue } from "./getState"
|
import { setState } from "./setState"
|
||||||
import { setState, setStateFromBinding } from "./setState"
|
|
||||||
import { trimSlash } from "../common/trimSlash"
|
|
||||||
import { isBound } from "./parseBinding"
|
|
||||||
import { attachChildren } from "../render/attachChildren"
|
import { attachChildren } from "../render/attachChildren"
|
||||||
import { getContext, setContext } from "./getSetContext"
|
import { getContext, setContext } from "./getSetContext"
|
||||||
|
|
||||||
|
export const trimSlash = str => str.replace(/^\/+|\/+$/g, "")
|
||||||
|
|
||||||
export const bbFactory = ({
|
export const bbFactory = ({
|
||||||
store,
|
store,
|
||||||
getCurrentState,
|
|
||||||
frontendDefinition,
|
frontendDefinition,
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
uiFunctions,
|
|
||||||
onScreenSlotRendered,
|
onScreenSlotRendered,
|
||||||
}) => {
|
}) => {
|
||||||
const relativeUrl = url => {
|
const relativeUrl = url => {
|
||||||
|
@ -51,11 +48,9 @@ export const bbFactory = ({
|
||||||
return (treeNode, setupState) => {
|
return (treeNode, setupState) => {
|
||||||
const attachParams = {
|
const attachParams = {
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
uiFunctions,
|
|
||||||
treeNode,
|
treeNode,
|
||||||
onScreenSlotRendered,
|
onScreenSlotRendered,
|
||||||
setupState,
|
setupState,
|
||||||
getCurrentState,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -63,17 +58,12 @@ export const bbFactory = ({
|
||||||
context: treeNode.context,
|
context: treeNode.context,
|
||||||
props: treeNode.props,
|
props: treeNode.props,
|
||||||
call: safeCallEvent,
|
call: safeCallEvent,
|
||||||
setStateFromBinding: (binding, value) =>
|
setState,
|
||||||
setStateFromBinding(store, binding, value),
|
|
||||||
setState: (path, value) => setState(store, path, value),
|
|
||||||
getStateOrValue: (prop, currentContext) =>
|
|
||||||
getStateOrValue(getCurrentState(), prop, currentContext),
|
|
||||||
getContext: getContext(treeNode),
|
getContext: getContext(treeNode),
|
||||||
setContext: setContext(treeNode),
|
setContext: setContext(treeNode),
|
||||||
store: store,
|
store: store,
|
||||||
relativeUrl,
|
relativeUrl,
|
||||||
api,
|
api,
|
||||||
isBound,
|
|
||||||
parent,
|
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 EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
|
||||||
|
|
||||||
export const eventHandlers = (store, rootPath, routeTo) => {
|
export const eventHandlers = (rootPath, routeTo) => {
|
||||||
const handler = (parameters, execute) => ({
|
const handler = (parameters, execute) => ({
|
||||||
execute,
|
execute,
|
||||||
parameters,
|
parameters,
|
||||||
})
|
})
|
||||||
|
|
||||||
const setStateWithStore = (path, value) => setState(store, path, value)
|
|
||||||
|
|
||||||
let currentState
|
|
||||||
store.subscribe(state => {
|
|
||||||
currentState = state
|
|
||||||
})
|
|
||||||
|
|
||||||
const api = createApi({
|
const api = createApi({
|
||||||
rootPath,
|
rootPath,
|
||||||
setState: setStateWithStore,
|
setState,
|
||||||
getState: (path, fallback) => getState(currentState, path, fallback),
|
getState: (path, fallback) => getState(path, fallback),
|
||||||
})
|
})
|
||||||
|
|
||||||
const setStateHandler = ({ path, value }) => setState(store, path, value)
|
const setStateHandler = ({ path, value }) => setState(path, value)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"Set State": handler(["path", "value"], setStateHandler),
|
"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)),
|
"Navigate To": handler(["url"], param => routeTo(param && param.url)),
|
||||||
|
"Trigger Workflow": handler(["workflow"], api.triggerWorkflow),
|
||||||
Authenticate: handler(["username", "password"], api.authenticate),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,46 +1,9 @@
|
||||||
import { isUndefined, isObject } from "lodash/fp"
|
import { get } from "svelte/store"
|
||||||
import { parseBinding, isStoreBinding } from "./parseBinding"
|
import getOr from "lodash/fp/getOr"
|
||||||
|
import { appStore } from "./store"
|
||||||
|
|
||||||
export const getState = (s, path, fallback) => {
|
export const getState = (path, fallback) => {
|
||||||
if (!s) return fallback
|
|
||||||
if (!path || path.length === 0) return fallback
|
if (!path || path.length === 0) return fallback
|
||||||
|
|
||||||
if (path === "$") return s
|
return getOr(fallback, path, get(appStore))
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 set from "lodash/fp/set"
|
||||||
import { parseBinding } from "./parseBinding"
|
import { appStore } from "./store"
|
||||||
|
|
||||||
export const setState = (store, path, value) => {
|
export const setState = (path, value) => {
|
||||||
if (!path || path.length === 0) return
|
if (!path || path.length === 0) return
|
||||||
|
|
||||||
const pathParts = path.split(".")
|
appStore.update(state => {
|
||||||
|
state = set(path, value, state)
|
||||||
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)
|
|
||||||
return 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,
|
EVENT_TYPE_MEMBER_NAME,
|
||||||
} from "./eventHandlers"
|
} from "./eventHandlers"
|
||||||
import { bbFactory } from "./bbComponentApi"
|
import { bbFactory } from "./bbComponentApi"
|
||||||
import { getState } from "./getState"
|
import mustache from "mustache"
|
||||||
import { attachChildren } from "../render/attachChildren"
|
import { get } from "svelte/store"
|
||||||
|
import { appStore } from "./store"
|
||||||
import { parseBinding } from "./parseBinding"
|
|
||||||
|
|
||||||
const doNothing = () => {}
|
const doNothing = () => {}
|
||||||
doNothing.isPlaceholder = true
|
doNothing.isPlaceholder = true
|
||||||
|
@ -18,182 +17,62 @@ const isMetaProp = propName =>
|
||||||
propName === "_id" ||
|
propName === "_id" ||
|
||||||
propName === "_style" ||
|
propName === "_style" ||
|
||||||
propName === "_code" ||
|
propName === "_code" ||
|
||||||
propName === "_codeMeta"
|
propName === "_codeMeta" ||
|
||||||
|
propName === "_styles"
|
||||||
|
|
||||||
export const createStateManager = ({
|
export const createStateManager = ({
|
||||||
store,
|
|
||||||
appRootPath,
|
appRootPath,
|
||||||
frontendDefinition,
|
frontendDefinition,
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
uiFunctions,
|
|
||||||
onScreenSlotRendered,
|
onScreenSlotRendered,
|
||||||
routeTo,
|
routeTo,
|
||||||
}) => {
|
}) => {
|
||||||
let handlerTypes = eventHandlers(store, appRootPath, routeTo)
|
let handlerTypes = eventHandlers(appRootPath, routeTo)
|
||||||
let currentState
|
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 getCurrentState = () => currentState
|
||||||
const registerBindings = _registerBindings(
|
|
||||||
nodesBoundByProps,
|
|
||||||
nodesWithCodeBoundChildren
|
|
||||||
)
|
|
||||||
const bb = bbFactory({
|
const bb = bbFactory({
|
||||||
store,
|
store: appStore,
|
||||||
getCurrentState,
|
getCurrentState,
|
||||||
frontendDefinition,
|
frontendDefinition,
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
uiFunctions,
|
|
||||||
onScreenSlotRendered,
|
onScreenSlotRendered,
|
||||||
})
|
})
|
||||||
|
|
||||||
const setup = _setup(handlerTypes, getCurrentState, registerBindings, bb)
|
const setup = _setup({ handlerTypes, getCurrentState, bb, store: appStore })
|
||||||
|
|
||||||
const unsubscribe = store.subscribe(
|
|
||||||
onStoreStateUpdated({
|
|
||||||
setCurrentState: s => (currentState = s),
|
|
||||||
getCurrentState,
|
|
||||||
nodesWithCodeBoundChildren,
|
|
||||||
nodesBoundByProps,
|
|
||||||
uiFunctions,
|
|
||||||
componentLibraries,
|
|
||||||
onScreenSlotRendered,
|
|
||||||
setupState: setup,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
setup,
|
setup,
|
||||||
destroy: () => unsubscribe(),
|
destroy: () => {},
|
||||||
getCurrentState,
|
getCurrentState,
|
||||||
store,
|
store: appStore,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onStoreStateUpdated = ({
|
const _setup = ({ handlerTypes, getCurrentState, bb, store }) => node => {
|
||||||
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 props = node.props
|
const props = node.props
|
||||||
const context = node.context || {}
|
const context = node.context || {}
|
||||||
const initialProps = { ...props }
|
const initialProps = { ...props }
|
||||||
const storeBoundProps = []
|
const currentStoreState = get(appStore)
|
||||||
const currentStoreState = getCurrentState()
|
|
||||||
|
|
||||||
for (let propName in props) {
|
for (let propName in props) {
|
||||||
if (isMetaProp(propName)) continue
|
if (isMetaProp(propName)) continue
|
||||||
|
|
||||||
const propValue = props[propName]
|
const propValue = props[propName]
|
||||||
|
|
||||||
const binding = parseBinding(propValue)
|
// A little bit of a hack - won't bind if the string doesn't start with {{
|
||||||
const isBound = !!binding
|
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") {
|
if (!node.stateBound) {
|
||||||
storeBoundProps.push(binding)
|
node.stateBound = true
|
||||||
|
|
||||||
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)) {
|
if (isEventType(propValue)) {
|
||||||
|
@ -203,33 +82,24 @@ const _setup = (
|
||||||
handlerType: event[EVENT_TYPE_MEMBER_NAME],
|
handlerType: event[EVENT_TYPE_MEMBER_NAME],
|
||||||
parameters: event.parameters,
|
parameters: event.parameters,
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedParams = {}
|
const resolvedParams = {}
|
||||||
for (let paramName in handlerInfo.parameters) {
|
for (let paramName in handlerInfo.parameters) {
|
||||||
const paramValue = handlerInfo.parameters[paramName]
|
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] = () =>
|
resolvedParams[paramName] = () =>
|
||||||
getState(paramValueSource, paramBinding.path, paramBinding.fallback)
|
mustache.render(paramValue, {
|
||||||
|
state: getCurrentState(),
|
||||||
|
context,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handlerInfo.parameters = resolvedParams
|
handlerInfo.parameters = resolvedParams
|
||||||
handlersInfos.push(handlerInfo)
|
handlersInfos.push(handlerInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handlersInfos.length === 0) initialProps[propName] = doNothing
|
if (handlersInfos.length === 0) {
|
||||||
else {
|
initialProps[propName] = doNothing
|
||||||
|
} else {
|
||||||
initialProps[propName] = async context => {
|
initialProps[propName] = async context => {
|
||||||
for (let handlerInfo of handlersInfos) {
|
for (let handlerInfo of handlersInfos) {
|
||||||
const handler = makeHandler(handlerTypes, handlerInfo)
|
const handler = makeHandler(handlerTypes, handlerInfo)
|
||||||
|
@ -240,9 +110,7 @@ const _setup = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registerBindings(node, storeBoundProps)
|
const setup = _setup({ handlerTypes, getCurrentState, bb, store })
|
||||||
|
|
||||||
const setup = _setup(handlerTypes, getCurrentState, registerBindings, bb)
|
|
||||||
initialProps._bb = bb(node, setup)
|
initialProps._bb = bb(node, setup)
|
||||||
|
|
||||||
return initialProps
|
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)
|
autoAssignIds(s.props)
|
||||||
}
|
}
|
||||||
setAppDef(dom.window, page, screens)
|
setAppDef(dom.window, page, screens)
|
||||||
addWindowGlobals(dom.window, page, screens, appRootPath, uiFunctions, {
|
addWindowGlobals(dom.window, page, screens, appRootPath, {
|
||||||
hierarchy: {},
|
hierarchy: {},
|
||||||
actions: [],
|
actions: [],
|
||||||
triggers: [],
|
triggers: [],
|
||||||
|
@ -27,13 +27,12 @@ export const load = async (page, screens, url, appRootPath) => {
|
||||||
return { dom, app }
|
return { dom, app }
|
||||||
}
|
}
|
||||||
|
|
||||||
const addWindowGlobals = (window, page, screens, appRootPath, uiFunctions) => {
|
const addWindowGlobals = (window, page, screens, appRootPath) => {
|
||||||
window["##BUDIBASE_FRONTEND_DEFINITION##"] = {
|
window["##BUDIBASE_FRONTEND_DEFINITION##"] = {
|
||||||
page,
|
page,
|
||||||
screens,
|
screens,
|
||||||
appRootPath,
|
appRootPath,
|
||||||
}
|
}
|
||||||
window["##BUDIBASE_FRONTEND_FUNCTIONS##"] = uiFunctions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makePage = props => ({ props })
|
export const makePage = props => ({ props })
|
||||||
|
@ -184,28 +183,3 @@ const maketestlib = window => ({
|
||||||
opts.target.appendChild(node)
|
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,
|
DatatableCell,
|
||||||
DatatableRow,
|
DatatableRow,
|
||||||
} from "./Datatable"
|
} from "./Datatable"
|
||||||
export { default as indexDatatable } from "./Templates/indexDatatable"
|
|
||||||
export { default as recordForm } from "./Templates/recordForm"
|
|
||||||
export { List, ListItem } from "./List"
|
export { List, ListItem } from "./List"
|
||||||
export { Menu } from "./Menu"
|
export { Menu } from "./Menu"
|
||||||
export { Select } from "./Select"
|
export { Select } from "./Select"
|
||||||
|
|
|
@ -44,6 +44,7 @@
|
||||||
"@budibase/client": "^0.0.32",
|
"@budibase/client": "^0.0.32",
|
||||||
"@budibase/core": "^0.0.32",
|
"@budibase/core": "^0.0.32",
|
||||||
"@koa/router": "^8.0.0",
|
"@koa/router": "^8.0.0",
|
||||||
|
"@sendgrid/mail": "^7.1.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"electron-is-dev": "^1.2.0",
|
"electron-is-dev": "^1.2.0",
|
||||||
|
@ -59,6 +60,7 @@
|
||||||
"koa-session": "^5.12.0",
|
"koa-session": "^5.12.0",
|
||||||
"koa-static": "^5.0.0",
|
"koa-static": "^5.0.0",
|
||||||
"lodash": "^4.17.13",
|
"lodash": "^4.17.13",
|
||||||
|
"mustache": "^4.0.1",
|
||||||
"pino-pretty": "^4.0.0",
|
"pino-pretty": "^4.0.0",
|
||||||
"pouchdb": "^7.2.1",
|
"pouchdb": "^7.2.1",
|
||||||
"pouchdb-all-dbs": "^1.0.2",
|
"pouchdb-all-dbs": "^1.0.2",
|
||||||
|
|
|
@ -24,14 +24,15 @@ exports.authenticate = async ctx => {
|
||||||
|
|
||||||
// Check the user exists in the instance DB by username
|
// Check the user exists in the instance DB by username
|
||||||
const instanceDb = new CouchDB(instanceId)
|
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.`)
|
let dbUser
|
||||||
|
try {
|
||||||
const dbUser = rows[0].doc
|
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
|
// authenticate
|
||||||
if (await bcrypt.compare(password, dbUser.password)) {
|
if (await bcrypt.compare(password, dbUser.password)) {
|
||||||
|
|
|
@ -29,6 +29,16 @@ exports.create = async function(ctx) {
|
||||||
emit([doc.type], doc._id)
|
emit([doc.type], doc._id)
|
||||||
}.toString(),
|
}.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"
|
record.type = "record"
|
||||||
const response = await db.post(record)
|
const response = await db.post(record)
|
||||||
record._rev = response.rev
|
record._rev = response.rev
|
||||||
|
|
||||||
|
ctx.eventEmitter &&
|
||||||
|
ctx.eventEmitter.emit(`record:save`, {
|
||||||
|
record,
|
||||||
|
instanceId: ctx.params.instanceId,
|
||||||
|
})
|
||||||
ctx.body = record
|
ctx.body = record
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.message = `${model.name} created successfully`
|
ctx.message = `${model.name} created successfully`
|
||||||
|
@ -81,6 +87,7 @@ exports.destroy = async function(ctx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId)
|
ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId)
|
||||||
|
ctx.eventEmitter && ctx.eventEmitter.emit(`record:delete`, record)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.validate = async function(ctx) {
|
exports.validate = async function(ctx) {
|
||||||
|
|
|
@ -11,7 +11,8 @@ const controller = {
|
||||||
if (
|
if (
|
||||||
!name.startsWith("all") &&
|
!name.startsWith("all") &&
|
||||||
name !== "by_type" &&
|
name !== "by_type" &&
|
||||||
name !== "by_username"
|
name !== "by_username" &&
|
||||||
|
name !== "by_workflow_trigger"
|
||||||
) {
|
) {
|
||||||
response.push({
|
response.push({
|
||||||
name,
|
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 CouchDB = require("../../../db")
|
||||||
const newid = require("../../db/newid")
|
const newid = require("../../../db/newid")
|
||||||
|
|
||||||
exports.create = async function(ctx) {
|
exports.create = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.params.instanceId)
|
const db = new CouchDB(ctx.params.instanceId)
|
||||||
|
@ -7,24 +7,6 @@ exports.create = async function(ctx) {
|
||||||
|
|
||||||
workflow._id = newid()
|
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"
|
workflow.type = "workflow"
|
||||||
const response = await db.post(workflow)
|
const response = await db.post(workflow)
|
||||||
workflow._rev = response.rev
|
workflow._rev = response.rev
|
||||||
|
@ -41,23 +23,51 @@ exports.create = async function(ctx) {
|
||||||
|
|
||||||
exports.update = async function(ctx) {
|
exports.update = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.params.instanceId)
|
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) {
|
exports.fetch = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.params.instanceId)
|
const db = new CouchDB(ctx.params.instanceId)
|
||||||
const response = await db.query(`database/by_type`, {
|
const response = await db.query(`database/by_type`, {
|
||||||
type: "workflow",
|
key: ["workflow"],
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})
|
})
|
||||||
ctx.body = response.rows.map(row => row.doc)
|
ctx.body = response.rows.map(row => row.doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.find = async function(ctx) {
|
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)
|
ctx.body = await db.get(ctx.params.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.executeAction = async function(ctx) {
|
||||||
|
const { args, action } = ctx.request.body
|
||||||
|
const workflowAction = require(`./actions/${action}`)
|
||||||
|
const response = await workflowAction({
|
||||||
|
args,
|
||||||
|
instanceId: ctx.user.instanceId,
|
||||||
|
})
|
||||||
|
ctx.body = response
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.fetchActionScript = async function(ctx) {
|
||||||
|
const workflowAction = require(`./actions/${ctx.action}`)
|
||||||
|
ctx.body = workflowAction
|
||||||
|
}
|
||||||
|
|
||||||
exports.destroy = async function(ctx) {
|
exports.destroy = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.params.instanceId)
|
const db = new CouchDB(ctx.params.instanceId)
|
||||||
ctx.body = await db.remove(ctx.params.id, ctx.params.rev)
|
ctx.body = await db.remove(ctx.params.id, ctx.params.rev)
|
|
@ -1,9 +1,11 @@
|
||||||
const {
|
const {
|
||||||
createClientDatabase,
|
createClientDatabase,
|
||||||
createApplication,
|
createApplication,
|
||||||
|
createInstance,
|
||||||
destroyClientDatabase,
|
destroyClientDatabase,
|
||||||
|
builderEndpointShouldBlockNormalUsers,
|
||||||
supertest,
|
supertest,
|
||||||
defaultHeaders
|
defaultHeaders,
|
||||||
} = require("./couchTestUtils")
|
} = require("./couchTestUtils")
|
||||||
|
|
||||||
describe("/applications", () => {
|
describe("/applications", () => {
|
||||||
|
@ -37,6 +39,18 @@ describe("/applications", () => {
|
||||||
expect(res.res.statusMessage).toEqual("Application My App created successfully")
|
expect(res.res.statusMessage).toEqual("Application My App created successfully")
|
||||||
expect(res.body._id).toBeDefined()
|
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", () => {
|
describe("fetch", () => {
|
||||||
|
@ -53,6 +67,17 @@ describe("/applications", () => {
|
||||||
|
|
||||||
expect(res.body.length).toBe(2)
|
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 { create, destroy } = require("../../../db/clientDb")
|
||||||
const supertest = require("supertest")
|
const supertest = require("supertest")
|
||||||
const app = require("../../../app")
|
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"
|
const TEST_CLIENT_ID = "test-client-id"
|
||||||
|
|
||||||
|
@ -82,8 +85,8 @@ exports.createInstance = async (request, appId) => {
|
||||||
exports.createUser = async (
|
exports.createUser = async (
|
||||||
request,
|
request,
|
||||||
instanceId,
|
instanceId,
|
||||||
username = "bill",
|
username = "babs",
|
||||||
password = "bills_password"
|
password = "babs_password"
|
||||||
) => {
|
) => {
|
||||||
const res = await request
|
const res = await request
|
||||||
.post(`/api/${instanceId}/users`)
|
.post(`/api/${instanceId}/users`)
|
||||||
|
@ -97,6 +100,150 @@ exports.createUser = async (
|
||||||
return res.body
|
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) => {
|
exports.insertDocument = async (databaseId, document) => {
|
||||||
const { id, ...documentFields } = document
|
const { id, ...documentFields } = document
|
||||||
return await new CouchDB(databaseId).put({ _id: id, ...documentFields })
|
return await new CouchDB(databaseId).put({ _id: id, ...documentFields })
|
||||||
|
|
|
@ -4,7 +4,8 @@ const {
|
||||||
supertest,
|
supertest,
|
||||||
createClientDatabase,
|
createClientDatabase,
|
||||||
createApplication ,
|
createApplication ,
|
||||||
defaultHeaders
|
defaultHeaders,
|
||||||
|
builderEndpointShouldBlockNormalUsers
|
||||||
} = require("./couchTestUtils")
|
} = require("./couchTestUtils")
|
||||||
|
|
||||||
describe("/models", () => {
|
describe("/models", () => {
|
||||||
|
@ -48,6 +49,22 @@ describe("/models", () => {
|
||||||
done();
|
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", () => {
|
describe("fetch", () => {
|
||||||
|
@ -71,6 +88,16 @@ describe("/models", () => {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should apply authorization to endpoint", async () => {
|
||||||
|
await builderEndpointShouldBlockNormalUsers({
|
||||||
|
request,
|
||||||
|
method: "GET",
|
||||||
|
url: `/api/${instance._id}/models`,
|
||||||
|
instanceId: instance._id,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("destroy", () => {
|
describe("destroy", () => {
|
||||||
|
@ -92,5 +119,15 @@ describe("/models", () => {
|
||||||
done();
|
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