commit
ab7eec0821
|
@ -4,13 +4,11 @@ import {
|
||||||
BB_STATE_BINDINGPATH,
|
BB_STATE_BINDINGPATH,
|
||||||
BB_STATE_FALLBACK,
|
BB_STATE_FALLBACK,
|
||||||
BB_STATE_BINDINGSOURCE,
|
BB_STATE_BINDINGSOURCE,
|
||||||
} from "@budibase/client/src/state/isState"
|
isBound,
|
||||||
|
parseBinding,
|
||||||
|
} from "@budibase/client/src/state/parseBinding"
|
||||||
|
|
||||||
export const isBinding = value =>
|
export const isBinding = isBound
|
||||||
!isString(value) &&
|
|
||||||
value &&
|
|
||||||
isString(value[BB_STATE_BINDINGPATH]) &&
|
|
||||||
value[BB_STATE_BINDINGPATH].length > 0
|
|
||||||
|
|
||||||
export const setBinding = ({ path, fallback, source }, binding = {}) => {
|
export const setBinding = ({ path, fallback, source }, binding = {}) => {
|
||||||
if (isNonEmptyString(path)) binding[BB_STATE_BINDINGPATH] = path
|
if (isNonEmptyString(path)) binding[BB_STATE_BINDINGPATH] = path
|
||||||
|
@ -19,10 +17,15 @@ export const setBinding = ({ path, fallback, source }, binding = {}) => {
|
||||||
return binding
|
return binding
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getBinding = binding => ({
|
export const getBinding = val => {
|
||||||
path: binding[BB_STATE_BINDINGPATH] || "",
|
const binding = parseBinding(val)
|
||||||
fallback: binding[BB_STATE_FALLBACK] || "",
|
return binding
|
||||||
source: binding[BB_STATE_BINDINGSOURCE] || "store",
|
? binding
|
||||||
})
|
: {
|
||||||
|
path: "",
|
||||||
|
source: "store",
|
||||||
|
fallback: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const isNonEmptyString = s => isString(s) && s.length > 0
|
const isNonEmptyString = s => isString(s) && s.length > 0
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers"
|
||||||
import {
|
import {
|
||||||
isBound,
|
isBound,
|
||||||
BB_STATE_BINDINGPATH,
|
BB_STATE_BINDINGPATH,
|
||||||
} from "@budibase/client/src/state/isState"
|
} from "@budibase/client/src/state/parseBinding"
|
||||||
|
|
||||||
const defaultDef = typeName => () => ({
|
const defaultDef = typeName => () => ({
|
||||||
type: typeName,
|
type: typeName,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { createProps } from "../src/userInterface/pagesParsing/createProps"
|
import { createProps } from "../src/userInterface/pagesParsing/createProps"
|
||||||
import { keys, some } from "lodash/fp"
|
import { keys, some } from "lodash/fp"
|
||||||
import { BB_STATE_BINDINGPATH } from "@budibase/client/src/state/isState"
|
import { BB_STATE_BINDINGPATH } from "@budibase/client/src/state/parseBinding"
|
||||||
import { stripStandardProps } from "./testData"
|
import { stripStandardProps } from "./testData"
|
||||||
|
|
||||||
describe("createDefaultProps", () => {
|
describe("createDefaultProps", () => {
|
||||||
|
|
|
@ -31,6 +31,7 @@ export const createApp = (
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
uiFunctions,
|
uiFunctions,
|
||||||
onScreenSlotRendered: () => {},
|
onScreenSlotRendered: () => {},
|
||||||
|
routeTo,
|
||||||
})
|
})
|
||||||
const getAttchChildrenParams = attachChildrenParams(stateManager)
|
const getAttchChildrenParams = attachChildrenParams(stateManager)
|
||||||
screenSlotNode.props._children = [screen.props]
|
screenSlotNode.props._children = [screen.props]
|
||||||
|
@ -44,7 +45,11 @@ export const createApp = (
|
||||||
currentUrl = url
|
currentUrl = url
|
||||||
}
|
}
|
||||||
|
|
||||||
routeTo = screenRouter(frontendDefinition.screens, onScreenSelected)
|
routeTo = screenRouter(
|
||||||
|
frontendDefinition.screens,
|
||||||
|
onScreenSelected,
|
||||||
|
frontendDefinition.appRootPath
|
||||||
|
)
|
||||||
routeTo(currentUrl || window.location.pathname)
|
routeTo(currentUrl || window.location.pathname)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,6 +74,8 @@ export const createApp = (
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
uiFunctions,
|
uiFunctions,
|
||||||
onScreenSlotRendered,
|
onScreenSlotRendered,
|
||||||
|
// seems weird, but the routeTo variable may not be available at this point
|
||||||
|
routeTo: url => routeTo(url),
|
||||||
})
|
})
|
||||||
|
|
||||||
const initialisePage = (page, target, urlPath) => {
|
const initialisePage = (page, target, urlPath) => {
|
||||||
|
|
|
@ -23,14 +23,14 @@ export const loadBudibase = async (opts) => {
|
||||||
temp: false,
|
temp: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootPath =
|
frontendDefinition.appRootPath =
|
||||||
frontendDefinition.appRootPath === ""
|
frontendDefinition.appRootPath === ""
|
||||||
? ""
|
? ""
|
||||||
: "/" + trimSlash(frontendDefinition.appRootPath)
|
: "/" + trimSlash(frontendDefinition.appRootPath)
|
||||||
|
|
||||||
if (!componentLibraries) {
|
if (!componentLibraries) {
|
||||||
|
|
||||||
const componentLibraryUrl = lib => rootPath + "/" + trimSlash(lib)
|
const componentLibraryUrl = lib => frontendDefinition.appRootPath + "/" + trimSlash(lib)
|
||||||
componentLibraries = {}
|
componentLibraries = {}
|
||||||
|
|
||||||
for (let lib of frontendDefinition.componentLibraries) {
|
for (let lib of frontendDefinition.componentLibraries) {
|
||||||
|
@ -52,7 +52,7 @@ export const loadBudibase = async (opts) => {
|
||||||
)
|
)
|
||||||
|
|
||||||
const route = _window.location
|
const route = _window.location
|
||||||
? _window.location.pathname.replace(rootPath, "")
|
? _window.location.pathname.replace(frontendDefinition.appRootPath, "")
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
import regexparam from "regexparam"
|
import regexparam from "regexparam"
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
export const screenRouter = (screens, onScreenSelected) => {
|
export const screenRouter = (screens, onScreenSelected, appRootPath) => {
|
||||||
const routes = screens.map(s => s.route)
|
const makeRootedPath = url => (appRootPath ? `${appRootPath}/${url}` : url)
|
||||||
|
|
||||||
|
const routes = screens.map(s => makeRootedPath(s.route))
|
||||||
let fallback = routes.findIndex(([p]) => p === "*")
|
let fallback = routes.findIndex(([p]) => p === "*")
|
||||||
if (fallback < 0) fallback = 0
|
if (fallback < 0) fallback = 0
|
||||||
|
|
||||||
let current
|
let current
|
||||||
|
|
||||||
function route(url) {
|
function route(url) {
|
||||||
|
const _url = makeRootedPath(url.state || url)
|
||||||
const _url = url.state || url
|
|
||||||
current = routes.findIndex(
|
current = routes.findIndex(
|
||||||
p => p !== "*" && new RegExp("^" + p + "$").test(_url)
|
p => p !== "*" && new RegExp("^" + p + "$").test(_url)
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { getStateOrValue } from "./getState"
|
import { getStateOrValue } from "./getState"
|
||||||
import { setState, setStateFromBinding } from "./setState"
|
import { setState, setStateFromBinding } from "./setState"
|
||||||
import { trimSlash } from "../common/trimSlash"
|
import { trimSlash } from "../common/trimSlash"
|
||||||
import { isBound } from "./isState"
|
import { isBound } from "./parseBinding"
|
||||||
import { attachChildren } from "../render/attachChildren"
|
import { attachChildren } from "../render/attachChildren"
|
||||||
|
|
||||||
export const bbFactory = ({
|
export const bbFactory = ({
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { getNewChildRecordToState, getNewRecordToState } from "./coreHandlers"
|
||||||
|
|
||||||
export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
|
export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
|
||||||
|
|
||||||
export const eventHandlers = (store, coreApi, rootPath) => {
|
export const eventHandlers = (store, coreApi, rootPath, routeTo) => {
|
||||||
const handler = (parameters, execute) => ({
|
const handler = (parameters, execute) => ({
|
||||||
execute,
|
execute,
|
||||||
parameters,
|
parameters,
|
||||||
|
@ -44,6 +44,8 @@ export const eventHandlers = (store, coreApi, rootPath) => {
|
||||||
getNewRecordToState(coreApi, setStateWithStore)
|
getNewRecordToState(coreApi, setStateWithStore)
|
||||||
),
|
),
|
||||||
|
|
||||||
|
"Navigate To": handler(["url"], param => routeTo(param && param.url)),
|
||||||
|
|
||||||
Authenticate: handler(["username", "password"], api.authenticate),
|
Authenticate: handler(["username", "password"], api.authenticate),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,5 @@
|
||||||
import { isUndefined, isObject } from "lodash/fp"
|
import { isUndefined, isObject } from "lodash/fp"
|
||||||
import {
|
import { parseBinding, isStoreBinding } from "./parseBinding"
|
||||||
isBound,
|
|
||||||
BB_STATE_BINDINGPATH,
|
|
||||||
BB_STATE_FALLBACK,
|
|
||||||
takeStateFromStore,
|
|
||||||
} from "./isState"
|
|
||||||
|
|
||||||
export const getState = (s, path, fallback) => {
|
export const getState = (s, path, fallback) => {
|
||||||
if (!s) return fallback
|
if (!s) return fallback
|
||||||
|
@ -39,20 +34,12 @@ export const getState = (s, path, fallback) => {
|
||||||
export const getStateOrValue = (globalState, prop, currentContext) => {
|
export const getStateOrValue = (globalState, prop, currentContext) => {
|
||||||
if (!prop) return prop
|
if (!prop) return prop
|
||||||
|
|
||||||
if (isBound(prop)) {
|
const binding = parseBinding(prop)
|
||||||
const stateToUse = takeStateFromStore(prop) ? globalState : currentContext
|
|
||||||
|
|
||||||
return getState(
|
if (binding) {
|
||||||
stateToUse,
|
const stateToUse = isStoreBinding(binding) ? globalState : currentContext
|
||||||
prop[BB_STATE_BINDINGPATH],
|
|
||||||
prop[BB_STATE_FALLBACK]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prop.path && prop.source) {
|
return getState(stateToUse, binding.path, binding.fallback)
|
||||||
const stateToUse = prop.source === "store" ? globalState : currentContext
|
|
||||||
|
|
||||||
return getState(stateToUse, prop.path, prop.fallback)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return prop
|
return prop
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
export const BB_STATE_BINDINGPATH = "##bbstate"
|
|
||||||
export const BB_STATE_BINDINGSOURCE = "##bbsource"
|
|
||||||
export const BB_STATE_FALLBACK = "##bbstatefallback"
|
|
||||||
|
|
||||||
export const isBound = prop =>
|
|
||||||
prop !== undefined && prop[BB_STATE_BINDINGPATH] !== undefined
|
|
||||||
|
|
||||||
export const takeStateFromStore = prop =>
|
|
||||||
prop[BB_STATE_BINDINGSOURCE] === undefined ||
|
|
||||||
prop[BB_STATE_BINDINGSOURCE] === "store"
|
|
||||||
|
|
||||||
export const takeStateFromContext = prop =>
|
|
||||||
prop[BB_STATE_BINDINGSOURCE] === "context"
|
|
||||||
|
|
||||||
export const takeStateFromEventParameters = prop =>
|
|
||||||
prop[BB_STATE_BINDINGSOURCE] === "event"
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
export const BB_STATE_BINDINGPATH = "##bbstate"
|
||||||
|
export const BB_STATE_BINDINGSOURCE = "##bbsource"
|
||||||
|
export const BB_STATE_FALLBACK = "##bbstatefallback"
|
||||||
|
|
||||||
|
export const isBound = prop => !!parseBinding(prop)
|
||||||
|
|
||||||
|
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("store.") ||
|
||||||
|
prop.startsWith("context.") ||
|
||||||
|
prop.startsWith("event.") ||
|
||||||
|
prop.startsWith("route."))
|
||||||
|
|
||||||
|
const parseBindingExpression = prop => {
|
||||||
|
let source = prop.split(".")[0]
|
||||||
|
let path = prop.replace(`${source}.`, "")
|
||||||
|
if (source === "route") {
|
||||||
|
source = "store"
|
||||||
|
path = `##routeParams.${path}`
|
||||||
|
}
|
||||||
|
const fallback = ""
|
||||||
|
return {
|
||||||
|
fallback,
|
||||||
|
source,
|
||||||
|
path,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import { isObject } from "lodash/fp"
|
import { isObject } from "lodash/fp"
|
||||||
import { BB_STATE_BINDINGPATH } from "./isState"
|
import { parseBinding } from "./parseBinding"
|
||||||
|
|
||||||
export const setState = (store, path, value) => {
|
export const setState = (store, path, value) => {
|
||||||
if (!path || path.length === 0) return
|
if (!path || path.length === 0) return
|
||||||
|
@ -30,5 +30,8 @@ export const setState = (store, path, value) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setStateFromBinding = (store, binding, value) =>
|
export const setStateFromBinding = (store, binding, value) => {
|
||||||
setState(store, binding[BB_STATE_BINDINGPATH], value)
|
const parsedBinding = parseBinding(binding)
|
||||||
|
if (!parsedBinding) return
|
||||||
|
return setState(store, parsedBinding.path, value)
|
||||||
|
}
|
||||||
|
|
|
@ -1,179 +0,0 @@
|
||||||
import {
|
|
||||||
isEventType,
|
|
||||||
eventHandlers,
|
|
||||||
EVENT_TYPE_MEMBER_NAME,
|
|
||||||
} from "./eventHandlers"
|
|
||||||
|
|
||||||
import { getState } from "./getState"
|
|
||||||
|
|
||||||
import {
|
|
||||||
isBound,
|
|
||||||
takeStateFromStore,
|
|
||||||
takeStateFromContext,
|
|
||||||
takeStateFromEventParameters,
|
|
||||||
BB_STATE_FALLBACK,
|
|
||||||
BB_STATE_BINDINGPATH,
|
|
||||||
BB_STATE_BINDINGSOURCE,
|
|
||||||
} from "./isState"
|
|
||||||
|
|
||||||
const doNothing = () => {}
|
|
||||||
doNothing.isPlaceholder = true
|
|
||||||
|
|
||||||
const isMetaProp = propName =>
|
|
||||||
propName === "_component" ||
|
|
||||||
propName === "_children" ||
|
|
||||||
propName === "_id" ||
|
|
||||||
propName === "_style" ||
|
|
||||||
propName === "_code"
|
|
||||||
|
|
||||||
export const setupBinding = (store, rootProps, coreApi, context, rootPath) => {
|
|
||||||
const rootInitialProps = { ...rootProps }
|
|
||||||
|
|
||||||
const getBindings = (props, initialProps) => {
|
|
||||||
const boundProps = []
|
|
||||||
const contextBoundProps = []
|
|
||||||
const componentEventHandlers = []
|
|
||||||
|
|
||||||
for (let propName in props) {
|
|
||||||
if (isMetaProp(propName)) continue
|
|
||||||
|
|
||||||
const val = props[propName]
|
|
||||||
|
|
||||||
if (isBound(val) && takeStateFromStore(val)) {
|
|
||||||
const binding = BindingPath(val)
|
|
||||||
const source = BindingSource(val)
|
|
||||||
const fallback = BindingFallback(val)
|
|
||||||
|
|
||||||
boundProps.push({
|
|
||||||
path: binding,
|
|
||||||
fallback,
|
|
||||||
propName,
|
|
||||||
source,
|
|
||||||
})
|
|
||||||
|
|
||||||
initialProps[propName] = fallback
|
|
||||||
} else if (isBound(val) && takeStateFromContext(val)) {
|
|
||||||
const binding = BindingPath(val)
|
|
||||||
const fallback = BindingFallback(val)
|
|
||||||
const source = BindingSource(val)
|
|
||||||
|
|
||||||
contextBoundProps.push({
|
|
||||||
path: binding,
|
|
||||||
fallback,
|
|
||||||
propName,
|
|
||||||
source,
|
|
||||||
})
|
|
||||||
|
|
||||||
initialProps[propName] = !context
|
|
||||||
? val
|
|
||||||
: getState(context, binding, fallback, source)
|
|
||||||
} else if (isEventType(val)) {
|
|
||||||
const handlers = { propName, handlers: [] }
|
|
||||||
componentEventHandlers.push(handlers)
|
|
||||||
|
|
||||||
for (let e of val) {
|
|
||||||
handlers.handlers.push({
|
|
||||||
handlerType: e[EVENT_TYPE_MEMBER_NAME],
|
|
||||||
parameters: e.parameters,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
initialProps[propName] = doNothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
contextBoundProps,
|
|
||||||
boundProps,
|
|
||||||
componentEventHandlers,
|
|
||||||
initialProps,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const bind = rootBindings => component => {
|
|
||||||
if (
|
|
||||||
rootBindings.boundProps.length === 0 &&
|
|
||||||
rootBindings.componentEventHandlers.length === 0
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
const handlerTypes = eventHandlers(store, coreApi, rootPath)
|
|
||||||
|
|
||||||
const unsubscribe = store.subscribe(rootState => {
|
|
||||||
const getPropsFromBindings = (s, bindings) => {
|
|
||||||
const { boundProps, componentEventHandlers } = bindings
|
|
||||||
const newProps = { ...bindings.initialProps }
|
|
||||||
|
|
||||||
for (let boundProp of boundProps) {
|
|
||||||
const val = getState(s, boundProp.path, boundProp.fallback)
|
|
||||||
|
|
||||||
if (val === undefined && newProps[boundProp.propName] !== undefined) {
|
|
||||||
delete newProps[boundProp.propName]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (val !== undefined) {
|
|
||||||
newProps[boundProp.propName] = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let boundHandler of componentEventHandlers) {
|
|
||||||
const closuredHandlers = []
|
|
||||||
for (let h of boundHandler.handlers) {
|
|
||||||
const handlerType = handlerTypes[h.handlerType]
|
|
||||||
closuredHandlers.push(eventContext => {
|
|
||||||
const parameters = {}
|
|
||||||
for (let pname in h.parameters) {
|
|
||||||
const p = h.parameters[pname]
|
|
||||||
parameters[pname] = !isBound(p)
|
|
||||||
? p
|
|
||||||
: takeStateFromStore(p)
|
|
||||||
? getState(s, p[BB_STATE_BINDINGPATH], p[BB_STATE_FALLBACK])
|
|
||||||
: takeStateFromEventParameters(p)
|
|
||||||
? getState(
|
|
||||||
eventContext,
|
|
||||||
p[BB_STATE_BINDINGPATH],
|
|
||||||
p[BB_STATE_FALLBACK]
|
|
||||||
)
|
|
||||||
: takeStateFromContext(p)
|
|
||||||
? getState(
|
|
||||||
context,
|
|
||||||
p[BB_STATE_BINDINGPATH],
|
|
||||||
p[BB_STATE_FALLBACK]
|
|
||||||
)
|
|
||||||
: p[BB_STATE_FALLBACK]
|
|
||||||
}
|
|
||||||
handlerType.execute(parameters)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
newProps[boundHandler.propName] = async context => {
|
|
||||||
for (let runHandler of closuredHandlers) {
|
|
||||||
await runHandler(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newProps
|
|
||||||
}
|
|
||||||
|
|
||||||
const rootNewProps = getPropsFromBindings(rootState, rootBindings)
|
|
||||||
|
|
||||||
component.$set(rootNewProps)
|
|
||||||
})
|
|
||||||
|
|
||||||
return unsubscribe
|
|
||||||
}
|
|
||||||
|
|
||||||
const bindings = getBindings(rootProps, rootInitialProps)
|
|
||||||
|
|
||||||
return {
|
|
||||||
initialProps: rootInitialProps,
|
|
||||||
bind: bind(bindings),
|
|
||||||
boundProps: bindings.boundProps,
|
|
||||||
contextBoundProps: bindings.contextBoundProps,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const BindingPath = prop => prop[BB_STATE_BINDINGPATH]
|
|
||||||
const BindingFallback = prop => prop[BB_STATE_FALLBACK]
|
|
||||||
const BindingSource = prop => prop[BB_STATE_BINDINGSOURCE]
|
|
|
@ -7,15 +7,7 @@ import { bbFactory } from "./bbComponentApi"
|
||||||
import { getState } from "./getState"
|
import { getState } from "./getState"
|
||||||
import { attachChildren } from "../render/attachChildren"
|
import { attachChildren } from "../render/attachChildren"
|
||||||
|
|
||||||
import {
|
import { parseBinding } from "./parseBinding"
|
||||||
isBound,
|
|
||||||
takeStateFromStore,
|
|
||||||
takeStateFromContext,
|
|
||||||
takeStateFromEventParameters,
|
|
||||||
BB_STATE_FALLBACK,
|
|
||||||
BB_STATE_BINDINGPATH,
|
|
||||||
BB_STATE_BINDINGSOURCE,
|
|
||||||
} from "./isState"
|
|
||||||
|
|
||||||
const doNothing = () => {}
|
const doNothing = () => {}
|
||||||
doNothing.isPlaceholder = true
|
doNothing.isPlaceholder = true
|
||||||
|
@ -36,8 +28,9 @@ export const createStateManager = ({
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
uiFunctions,
|
uiFunctions,
|
||||||
onScreenSlotRendered,
|
onScreenSlotRendered,
|
||||||
|
routeTo,
|
||||||
}) => {
|
}) => {
|
||||||
let handlerTypes = eventHandlers(store, coreApi, rootPath)
|
let handlerTypes = eventHandlers(store, coreApi, rootPath, routeTo)
|
||||||
let currentState
|
let currentState
|
||||||
|
|
||||||
// any nodes that have props that are bound to the store
|
// any nodes that have props that are bound to the store
|
||||||
|
@ -180,35 +173,25 @@ const _setup = (
|
||||||
|
|
||||||
const val = props[propName]
|
const val = props[propName]
|
||||||
|
|
||||||
if (isBound(val) && takeStateFromStore(val)) {
|
const binding = parseBinding(val)
|
||||||
const path = BindingPath(val)
|
const isBound = !!binding
|
||||||
const source = BindingSource(val)
|
if (isBound) binding.propName = propName
|
||||||
const fallback = BindingFallback(val)
|
|
||||||
|
|
||||||
storeBoundProps.push({
|
if (isBound && binding.source === "store") {
|
||||||
path,
|
storeBoundProps.push(binding)
|
||||||
fallback,
|
|
||||||
propName,
|
|
||||||
source,
|
|
||||||
})
|
|
||||||
|
|
||||||
initialProps[propName] = !currentStoreState
|
initialProps[propName] = !currentStoreState
|
||||||
? fallback
|
? binding.fallback
|
||||||
: getState(
|
: getState(
|
||||||
currentStoreState,
|
currentStoreState,
|
||||||
BindingPath(val),
|
binding.path,
|
||||||
BindingFallback(val),
|
binding.fallback,
|
||||||
BindingSource(val)
|
binding.source
|
||||||
)
|
)
|
||||||
} else if (isBound(val) && takeStateFromContext(val)) {
|
} else if (isBound && binding.source === "context") {
|
||||||
initialProps[propName] = !context
|
initialProps[propName] = !context
|
||||||
? val
|
? val
|
||||||
: getState(
|
: getState(context, binding.path, binding.fallback, binding.source)
|
||||||
context,
|
|
||||||
BindingPath(val),
|
|
||||||
BindingFallback(val),
|
|
||||||
BindingSource(val)
|
|
||||||
)
|
|
||||||
} else if (isEventType(val)) {
|
} else if (isEventType(val)) {
|
||||||
const handlersInfos = []
|
const handlersInfos = []
|
||||||
for (let e of val) {
|
for (let e of val) {
|
||||||
|
@ -219,31 +202,28 @@ const _setup = (
|
||||||
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]
|
||||||
if (!isBound(paramValue)) {
|
const paramBinding = parseBinding(paramValue)
|
||||||
|
if (!paramBinding) {
|
||||||
resolvedParams[paramName] = () => paramValue
|
resolvedParams[paramName] = () => paramValue
|
||||||
continue
|
continue
|
||||||
} else if (takeStateFromContext(paramValue)) {
|
} else if (paramBinding.source === "context") {
|
||||||
const val = getState(
|
const val = getState(
|
||||||
context,
|
context,
|
||||||
paramValue[BB_STATE_BINDINGPATH],
|
paramBinding.path,
|
||||||
paramValue[BB_STATE_FALLBACK]
|
paramBinding.fallback
|
||||||
)
|
)
|
||||||
resolvedParams[paramName] = () => val
|
resolvedParams[paramName] = () => val
|
||||||
} else if (takeStateFromStore(paramValue)) {
|
} else if (paramBinding.source === "store") {
|
||||||
resolvedParams[paramName] = () =>
|
resolvedParams[paramName] = () =>
|
||||||
getState(
|
getState(
|
||||||
getCurrentState(),
|
getCurrentState(),
|
||||||
paramValue[BB_STATE_BINDINGPATH],
|
paramBinding.path,
|
||||||
paramValue[BB_STATE_FALLBACK]
|
paramBinding.fallback
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
} else if (takeStateFromEventParameters(paramValue)) {
|
} else if (paramBinding.source === "event") {
|
||||||
resolvedParams[paramName] = eventContext => {
|
resolvedParams[paramName] = eventContext => {
|
||||||
getState(
|
getState(eventContext, paramBinding.path, paramBinding.fallback)
|
||||||
eventContext,
|
|
||||||
paramValue[BB_STATE_BINDINGPATH],
|
|
||||||
paramValue[BB_STATE_FALLBACK]
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -282,7 +262,3 @@ const makeHandler = (handlerTypes, handlerInfo) => {
|
||||||
handlerType.execute(parameters)
|
handlerType.execute(parameters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const BindingPath = prop => prop[BB_STATE_BINDINGPATH]
|
|
||||||
const BindingFallback = prop => prop[BB_STATE_FALLBACK]
|
|
||||||
const BindingSource = prop => prop[BB_STATE_BINDINGSOURCE]
|
|
||||||
|
|
|
@ -39,6 +39,23 @@ describe("initialiseApp (binding)", () => {
|
||||||
expect(rootDiv.className.includes("newvalue")).toBe(true)
|
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: "store.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 () => {
|
it("should populate child component with store value", async () => {
|
||||||
const { dom } = await load(
|
const { dom } = await load(
|
||||||
makePage({
|
makePage({
|
||||||
|
|
|
@ -16,6 +16,20 @@ describe("screenRouting", () => {
|
||||||
expect(screenRoot.children[0].children[0].innerText).toBe("screen 2")
|
expect(screenRoot.children[0].children[0].innerText).toBe("screen 2")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should load correct screen, for initial URL, when appRootPath is something", async () => {
|
||||||
|
const { page, screens } = pageWith3Screens()
|
||||||
|
const { dom } = await load(page, screens, "/testApp/screen2", "/testApp")
|
||||||
|
|
||||||
|
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(1)
|
||||||
|
expect(screenRoot.children[0].children[0].innerText).toBe("screen 2")
|
||||||
|
})
|
||||||
|
|
||||||
it("should be able to route to the correct screen", async () => {
|
it("should be able to route to the correct screen", async () => {
|
||||||
const { page, screens } = pageWith3Screens()
|
const { page, screens } = pageWith3Screens()
|
||||||
const { dom, app } = await load(page, screens, "/screen2")
|
const { dom, app } = await load(page, screens, "/screen2")
|
||||||
|
@ -31,6 +45,26 @@ describe("screenRouting", () => {
|
||||||
expect(screenRoot.children[0].children[0].innerText).toBe("screen 3")
|
expect(screenRoot.children[0].children[0].innerText).toBe("screen 3")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should be able to route to the correct screen, when appRootPath is something", async () => {
|
||||||
|
const { page, screens } = pageWith3Screens()
|
||||||
|
const { dom, app } = await load(
|
||||||
|
page,
|
||||||
|
screens,
|
||||||
|
"/testApp/screen2",
|
||||||
|
"/testApp"
|
||||||
|
)
|
||||||
|
|
||||||
|
app.routeTo()("/screen3")
|
||||||
|
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(1)
|
||||||
|
expect(screenRoot.children[0].children[0].innerText).toBe("screen 3")
|
||||||
|
})
|
||||||
|
|
||||||
it("should destroy and unsubscribe all components on a screen whe screen is changed", async () => {
|
it("should destroy and unsubscribe all components on a screen whe screen is changed", async () => {
|
||||||
const { page, screens } = pageWith3Screens()
|
const { page, screens } = pageWith3Screens()
|
||||||
const { app } = await load(page, screens, "/screen2")
|
const { app } = await load(page, screens, "/screen2")
|
||||||
|
|
|
@ -1,177 +0,0 @@
|
||||||
import { setupBinding } from "../src/state/stateBinding"
|
|
||||||
import {
|
|
||||||
BB_STATE_BINDINGPATH,
|
|
||||||
BB_STATE_FALLBACK,
|
|
||||||
BB_STATE_BINDINGSOURCE,
|
|
||||||
} from "../src/state/isState"
|
|
||||||
import { EVENT_TYPE_MEMBER_NAME } from "../src/state/eventHandlers"
|
|
||||||
import { writable } from "svelte/store"
|
|
||||||
import { isFunction } from "lodash/fp"
|
|
||||||
|
|
||||||
describe("setupBinding", () => {
|
|
||||||
it("should correctly create initials props, including fallback values", () => {
|
|
||||||
const { store, props, component } = testSetup()
|
|
||||||
|
|
||||||
const { initialProps } = testSetupBinding(store, props, component)
|
|
||||||
|
|
||||||
expect(initialProps.boundWithFallback).toBe("Bob")
|
|
||||||
expect(initialProps.boundNoFallback).toBeUndefined()
|
|
||||||
expect(initialProps.unbound).toBe("hello")
|
|
||||||
|
|
||||||
expect(isFunction(initialProps.eventBound)).toBeTruthy()
|
|
||||||
initialProps.eventBound()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should update component bound props when store is updated", () => {
|
|
||||||
const { component, store, props } = testSetup()
|
|
||||||
|
|
||||||
const { bind } = testSetupBinding(store, props, component)
|
|
||||||
bind(component)
|
|
||||||
|
|
||||||
store.update(s => {
|
|
||||||
s.FirstName = "Bobby"
|
|
||||||
s.LastName = "Thedog"
|
|
||||||
s.Customer = {
|
|
||||||
Name: "ACME inc",
|
|
||||||
Address: "",
|
|
||||||
}
|
|
||||||
s.addressToSet = "123 Main Street"
|
|
||||||
return s
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(component.props.boundWithFallback).toBe("Bobby")
|
|
||||||
expect(component.props.boundNoFallback).toBe("Thedog")
|
|
||||||
expect(component.props.multiPartBound).toBe("ACME inc")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should not update unbound props when store is updated", () => {
|
|
||||||
const { component, store, props } = testSetup()
|
|
||||||
|
|
||||||
const { bind } = testSetupBinding(store, props, component)
|
|
||||||
bind(component)
|
|
||||||
|
|
||||||
store.update(s => {
|
|
||||||
s.FirstName = "Bobby"
|
|
||||||
s.LastName = "Thedog"
|
|
||||||
s.Customer = {
|
|
||||||
Name: "ACME inc",
|
|
||||||
Address: "",
|
|
||||||
}
|
|
||||||
s.addressToSet = "123 Main Street"
|
|
||||||
return s
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(component.props.unbound).toBe("hello")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should update event handlers on state change", () => {
|
|
||||||
const { component, store, props } = testSetup()
|
|
||||||
|
|
||||||
const { bind } = testSetupBinding(store, props, component)
|
|
||||||
bind(component)
|
|
||||||
|
|
||||||
expect(component.props.boundToEventOutput).toBe("initial address")
|
|
||||||
component.props.eventBound()
|
|
||||||
expect(component.props.boundToEventOutput).toBe("event fallback address")
|
|
||||||
|
|
||||||
store.update(s => {
|
|
||||||
s.addressToSet = "123 Main Street"
|
|
||||||
return s
|
|
||||||
})
|
|
||||||
|
|
||||||
component.props.eventBound()
|
|
||||||
expect(component.props.boundToEventOutput).toBe("123 Main Street")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("event handlers should recognise event parameter", () => {
|
|
||||||
const { component, store, props } = testSetup()
|
|
||||||
|
|
||||||
const { bind } = testSetupBinding(store, props, component)
|
|
||||||
bind(component)
|
|
||||||
|
|
||||||
expect(component.props.boundToEventOutput).toBe("initial address")
|
|
||||||
component.props.eventBoundUsingEventParam({
|
|
||||||
addressOverride: "Overridden Address",
|
|
||||||
})
|
|
||||||
expect(component.props.boundToEventOutput).toBe("Overridden Address")
|
|
||||||
|
|
||||||
store.update(s => {
|
|
||||||
s.addressToSet = "123 Main Street"
|
|
||||||
return s
|
|
||||||
})
|
|
||||||
|
|
||||||
component.props.eventBound()
|
|
||||||
expect(component.props.boundToEventOutput).toBe("123 Main Street")
|
|
||||||
|
|
||||||
component.props.eventBoundUsingEventParam({
|
|
||||||
addressOverride: "Overridden Address",
|
|
||||||
})
|
|
||||||
expect(component.props.boundToEventOutput).toBe("Overridden Address")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should bind initial props to supplied context", () => {
|
|
||||||
const { component, store, props } = testSetup()
|
|
||||||
|
|
||||||
const { bind } = testSetupBinding(store, props, component, {
|
|
||||||
ContextValue: "Real Context Value",
|
|
||||||
})
|
|
||||||
bind(component)
|
|
||||||
|
|
||||||
expect(component.props.boundToContext).toBe("Real Context Value")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
const testSetupBinding = (store, props, component, context) => {
|
|
||||||
const setup = setupBinding(store, props, undefined, context)
|
|
||||||
component.props = setup.initialProps // svelte does this for us in real life
|
|
||||||
return setup
|
|
||||||
}
|
|
||||||
const testSetup = () => {
|
|
||||||
const c = {}
|
|
||||||
|
|
||||||
c.props = {}
|
|
||||||
c.$set = propsToSet => {
|
|
||||||
for (let pname in propsToSet) c.props[pname] = propsToSet[pname]
|
|
||||||
}
|
|
||||||
|
|
||||||
const binding = (path, fallback, source) => {
|
|
||||||
const b = {}
|
|
||||||
b[BB_STATE_BINDINGPATH] = path
|
|
||||||
b[BB_STATE_FALLBACK] = fallback
|
|
||||||
b[BB_STATE_BINDINGSOURCE] = source || "store"
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
const event = (handlerType, parameters) => {
|
|
||||||
const e = {}
|
|
||||||
e[EVENT_TYPE_MEMBER_NAME] = handlerType
|
|
||||||
e.parameters = parameters
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = {
|
|
||||||
boundWithFallback: binding("FirstName", "Bob"),
|
|
||||||
boundNoFallback: binding("LastName"),
|
|
||||||
unbound: "hello",
|
|
||||||
multiPartBound: binding("Customer.Name", "ACME"),
|
|
||||||
boundToEventOutput: binding("Customer.Address", "initial address"),
|
|
||||||
boundToContext: binding("ContextValue", "context fallback", "context"),
|
|
||||||
eventBound: [
|
|
||||||
event("Set State", {
|
|
||||||
path: "Customer.Address",
|
|
||||||
value: binding("addressToSet", "event fallback address"),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
eventBoundUsingEventParam: [
|
|
||||||
event("Set State", {
|
|
||||||
path: "Customer.Address",
|
|
||||||
value: binding("addressOverride", "", "event"),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
component: c,
|
|
||||||
store: writable({}),
|
|
||||||
props,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,10 @@
|
||||||
import { JSDOM } from "jsdom"
|
import { JSDOM } from "jsdom"
|
||||||
import { loadBudibase } from "../src/index"
|
import { loadBudibase } from "../src/index"
|
||||||
|
|
||||||
export const load = async (page, screens = [], url = "/") => {
|
export const load = async (page, screens, url, appRootPath) => {
|
||||||
|
screens = screens || []
|
||||||
|
url = url || "/"
|
||||||
|
appRootPath = appRootPath || ""
|
||||||
const dom = new JSDOM("<!DOCTYPE html><html><body></body><html>", {
|
const dom = new JSDOM("<!DOCTYPE html><html><body></body><html>", {
|
||||||
url: `http://test${url}`,
|
url: `http://test${url}`,
|
||||||
})
|
})
|
||||||
|
@ -10,7 +13,7 @@ export const load = async (page, screens = [], url = "/") => {
|
||||||
autoAssignIds(s.props)
|
autoAssignIds(s.props)
|
||||||
}
|
}
|
||||||
setAppDef(dom.window, page, screens)
|
setAppDef(dom.window, page, screens)
|
||||||
addWindowGlobals(dom.window, page, screens, uiFunctions, {
|
addWindowGlobals(dom.window, page, screens, appRootPath, uiFunctions, {
|
||||||
hierarchy: {},
|
hierarchy: {},
|
||||||
actions: [],
|
actions: [],
|
||||||
triggers: [],
|
triggers: [],
|
||||||
|
@ -28,6 +31,7 @@ const addWindowGlobals = (
|
||||||
window,
|
window,
|
||||||
page,
|
page,
|
||||||
screens,
|
screens,
|
||||||
|
appRootPath,
|
||||||
uiFunctions,
|
uiFunctions,
|
||||||
appDefinition
|
appDefinition
|
||||||
) => {
|
) => {
|
||||||
|
@ -35,7 +39,7 @@ const addWindowGlobals = (
|
||||||
window["##BUDIBASE_FRONTEND_DEFINITION##"] = {
|
window["##BUDIBASE_FRONTEND_DEFINITION##"] = {
|
||||||
page,
|
page,
|
||||||
screens,
|
screens,
|
||||||
appRootPath: "",
|
appRootPath,
|
||||||
}
|
}
|
||||||
window["##BUDIBASE_FRONTEND_FUNCTIONS##"] = uiFunctions
|
window["##BUDIBASE_FRONTEND_FUNCTIONS##"] = uiFunctions
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
export default ({ indexes, helpers }) =>
|
export default ({ indexes, helpers }) =>
|
||||||
indexes.map(i => ({
|
indexes.map(i => ({
|
||||||
name: `Table based on index: ${i.name} `,
|
name: `Table based on index: ${i.name} `,
|
||||||
props: tableProps(i, helpers.indexSchema(i)),
|
props: tableProps(
|
||||||
|
i,
|
||||||
|
helpers.indexSchema(i).filter(c => !excludedColumns.includes(c.name))
|
||||||
|
),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const excludedColumns = ["id", "key", "sortKey", "type", "isNew"]
|
||||||
|
|
||||||
const tableProps = (index, indexSchema) => ({
|
const tableProps = (index, indexSchema) => ({
|
||||||
_component: "@budibase/materialdesign-components/Datatable",
|
_component: "@budibase/materialdesign-components/Datatable",
|
||||||
_children: [
|
_children: [
|
||||||
|
|
Loading…
Reference in New Issue