fetchBindableProperties - complete
This commit is contained in:
parent
c13bb8fa83
commit
9bfdf6f8e5
|
@ -1,3 +1,5 @@
|
||||||
|
import { cloneDeep, difference, fill } from "lodash"
|
||||||
|
|
||||||
const stubBindings = [
|
const stubBindings = [
|
||||||
{
|
{
|
||||||
// type: instance represents a bindable property of a component
|
// type: instance represents a bindable property of a component
|
||||||
|
@ -10,7 +12,9 @@ const stubBindings = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "context",
|
type: "context",
|
||||||
instance: { /** a component instance **/},
|
instance: {
|
||||||
|
/** a component instance **/
|
||||||
|
},
|
||||||
// how the binding expression persists, and is used in the app at runtime
|
// how the binding expression persists, and is used in the app at runtime
|
||||||
runtimeBinding: "context._parent.<key of model/record>",
|
runtimeBinding: "context._parent.<key of model/record>",
|
||||||
// how the binding exressions looks to the user of the builder
|
// how the binding exressions looks to the user of the builder
|
||||||
|
@ -19,74 +23,97 @@ const stubBindings = [
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function({ componentInstanceId, screen, components, models }) {
|
export default function({ componentInstanceId, screen, components, models }) {
|
||||||
const { target, targetAncestors, bindableInstances, bindableContexts } = walk(
|
const walkResult = walk({
|
||||||
{
|
// cloning so we are free to mutate props (e.g. by adding _contexts)
|
||||||
instance: screen.props,
|
instance: cloneDeep(screen.props),
|
||||||
targetId: componentInstanceId,
|
targetId: componentInstanceId,
|
||||||
components,
|
components,
|
||||||
models,
|
models,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...bindableInstances
|
...walkResult.bindableInstances
|
||||||
.filter(isComponentInstanceAvailable)
|
.filter(isInstanceInSharedContext(walkResult))
|
||||||
.map(componentInstanceToBindable),
|
.map(componentInstanceToBindable(walkResult)),
|
||||||
...bindableContexts.map(contextToBindables),
|
|
||||||
|
...walkResult.target._contexts.map(contextToBindables(walkResult)).flat(),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const isComponentInstanceAvailable = i => true
|
const isInstanceInSharedContext = walkResult => i =>
|
||||||
|
// should cover
|
||||||
|
// - neither are in any context
|
||||||
|
// - both in same context
|
||||||
|
// - instance is in ancestor context of target
|
||||||
|
i.instance._contexts.length <= walkResult.target._contexts.length &&
|
||||||
|
difference(i.instance._contexts, walkResult.target._contexts).length === 0
|
||||||
|
|
||||||
// turns a component instance prop into binding expressions
|
// turns a component instance prop into binding expressions
|
||||||
// used by the UI
|
// used by the UI
|
||||||
const componentInstanceToBindable = i => ({
|
const componentInstanceToBindable = walkResult => i => {
|
||||||
type: "instance",
|
const lastContext =
|
||||||
instance: i.instance,
|
i.instance._contexts.length &&
|
||||||
// how the binding expression persists, and is used in the app at runtime
|
i.instance._contexts[i.instance._contexts.length - 1]
|
||||||
runtimeBinding: `state.${i.instance._id}.${i.prop}`,
|
const contextParentPath = lastContext
|
||||||
// how the binding exressions looks to the user of the builder
|
? getParentPath(walkResult, lastContext)
|
||||||
readableBinding: `${i.instance._instanceName}`,
|
: ""
|
||||||
})
|
|
||||||
|
|
||||||
const contextToBindables = c => {
|
// if component is inside context, then the component lives
|
||||||
const contextParentNumber = 0
|
// in context at runtime (otherwise, in state)
|
||||||
const contextParentPath = Array[contextParentNumber]
|
const stateOrContext = lastContext ? "context" : "state"
|
||||||
.map(() => "_parent")
|
return {
|
||||||
.join(".")
|
type: "instance",
|
||||||
return Object.keys(c.schema).map(k => ({
|
instance: i.instance,
|
||||||
|
// how the binding expression persists, and is used in the app at runtime
|
||||||
|
runtimeBinding: `${stateOrContext}.${contextParentPath}${i.instance._id}.${i.prop}`,
|
||||||
|
// how the binding exressions looks to the user of the builder
|
||||||
|
readableBinding: `${i.instance._instanceName}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const contextToBindables = walkResult => c => {
|
||||||
|
const contextParentPath = getParentPath(walkResult, c)
|
||||||
|
|
||||||
|
return Object.keys(c.model.schema).map(k => ({
|
||||||
type: "context",
|
type: "context",
|
||||||
instance: c.instance,
|
instance: c.instance,
|
||||||
// how the binding expression persists, and is used in the app at runtime
|
// how the binding expression persists, and is used in the app at runtime
|
||||||
runtimeBinding: `context.${contextParentPath}.${k}`,
|
runtimeBinding: `context.${contextParentPath}data.${k}`,
|
||||||
// how the binding exressions looks to the user of the builder
|
// how the binding exressions looks to the user of the builder
|
||||||
readableBinding: `${c.instance._instanceName}.${c.schema.name}.${k}`,
|
readableBinding: `${c.instance._instanceName}.${c.model.name}.${k}`,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getParentPath = (walkResult, context) => {
|
||||||
|
// describes the number of "_parent" in the path
|
||||||
|
// clone array first so original array is not mtated
|
||||||
|
const contextParentNumber = [...walkResult.target._contexts]
|
||||||
|
.reverse()
|
||||||
|
.indexOf(context)
|
||||||
|
|
||||||
|
return (
|
||||||
|
new Array(contextParentNumber).fill("_parent").join(".") +
|
||||||
|
// trailing . if has parents
|
||||||
|
(contextParentNumber ? "." : "")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const walk = ({ instance, targetId, components, models, result }) => {
|
const walk = ({ instance, targetId, components, models, result }) => {
|
||||||
if (!result) {
|
if (!result) {
|
||||||
result = {
|
result = {
|
||||||
currentAncestors: [],
|
|
||||||
currentContexts: [],
|
|
||||||
target: null,
|
target: null,
|
||||||
targetAncestors: [],
|
|
||||||
bindableInstances: [],
|
bindableInstances: [],
|
||||||
bindableContexts: [],
|
allContexts: [],
|
||||||
parentMap: {},
|
currentContexts: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!instance._contexts) instance._contexts = []
|
||||||
|
|
||||||
// "component" is the component definition (object in component.json)
|
// "component" is the component definition (object in component.json)
|
||||||
const component = components[instance._component]
|
const component = components[instance._component]
|
||||||
const parentInstance =
|
|
||||||
result.currentAncestors.length > 0 &&
|
|
||||||
result.currentAncestors[result.currentAncestors.length - 1]
|
|
||||||
|
|
||||||
if (instance._id === targetId) {
|
if (instance._id === targetId) {
|
||||||
// set currentParents to be result parents
|
|
||||||
result.targetAncestors = result.currentAncestors
|
|
||||||
result.bindableContexts = result.currentContexts
|
|
||||||
// found it
|
// found it
|
||||||
result.target = instance
|
result.target = instance
|
||||||
} else {
|
} else {
|
||||||
|
@ -102,22 +129,24 @@ const walk = ({ instance, targetId, components, models, result }) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// a component that provides context to it's children
|
// a component that provides context to it's children
|
||||||
const contextualInstance = component.context && instance[component.context]
|
const contextualInstance = component.context && instance[component.context]
|
||||||
|
|
||||||
if (contextualInstance) {
|
if (contextualInstance) {
|
||||||
// add to currentContexts (ancestory of context)
|
// add to currentContexts (ancestory of context)
|
||||||
// before walking children
|
// before walking children
|
||||||
const schema = models.find(m => m._id === instance[component.context])
|
const model = models.find(m => m._id === instance[component.context])
|
||||||
.schema
|
result.currentContexts.push({ instance, model })
|
||||||
result.currentContexts.push({ instance, schema })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentContexts = [...result.currentContexts]
|
||||||
for (let child of instance._children || []) {
|
for (let child of instance._children || []) {
|
||||||
result.parentMap[child._id] = parentInstance._id
|
// attaching _contexts of components, for eas comparison later
|
||||||
result.currentAncestors.push(instance)
|
// these have been deep cloned above, so shouln't modify the
|
||||||
|
// original component instances
|
||||||
|
child._contexts = currentContexts
|
||||||
walk({ instance: child, targetId, components, models, result })
|
walk({ instance: child, targetId, components, models, result })
|
||||||
result.currentAncestors.pop()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contextualInstance) {
|
if (contextualInstance) {
|
||||||
|
|
|
@ -6,9 +6,7 @@ describe("fetch bindable properties", () => {
|
||||||
componentInstanceId: "heading-id",
|
componentInstanceId: "heading-id",
|
||||||
...testData()
|
...testData()
|
||||||
})
|
})
|
||||||
const componentBinding = result.find(r => r.instance._id === "search-input-id")
|
const componentBinding = result.find(r => r.instance._id === "search-input-id" && r.type === "instance")
|
||||||
console.debug(componentBinding)
|
|
||||||
console.debug("result:" + JSON.stringify(result))
|
|
||||||
expect(componentBinding).toBeDefined()
|
expect(componentBinding).toBeDefined()
|
||||||
expect(componentBinding.type).toBe("instance")
|
expect(componentBinding.type).toBe("instance")
|
||||||
expect(componentBinding.runtimeBinding).toBe("state.search-input-id.value")
|
expect(componentBinding.runtimeBinding).toBe("state.search-input-id.value")
|
||||||
|
@ -24,19 +22,73 @@ describe("fetch bindable properties", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should return model schema, when inside a context", () => {
|
it("should return model schema, when inside a context", () => {
|
||||||
|
const result = fetchBindableProperties({
|
||||||
|
componentInstanceId: "list-item-input-id",
|
||||||
|
...testData()
|
||||||
|
})
|
||||||
|
const contextBindings = result.filter(r => r.instance._id === "list-id" && r.type==="context")
|
||||||
|
expect(contextBindings.length).toBe(2)
|
||||||
|
|
||||||
|
const namebinding = contextBindings.find(b => b.runtimeBinding === "context.data.name")
|
||||||
|
expect(namebinding).toBeDefined()
|
||||||
|
expect(namebinding.readableBinding).toBe("list-name.Test Model.name")
|
||||||
|
|
||||||
|
const descriptionbinding = contextBindings.find(b => b.runtimeBinding === "context.data.description")
|
||||||
|
expect(descriptionbinding).toBeDefined()
|
||||||
|
expect(descriptionbinding.readableBinding).toBe("list-name.Test Model.description")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should return model schema, for grantparent context", () => {
|
it("should return model schema, for grantparent context", () => {
|
||||||
|
const result = fetchBindableProperties({
|
||||||
|
componentInstanceId: "child-list-item-input-id",
|
||||||
|
...testData()
|
||||||
|
})
|
||||||
|
const contextBindings = result.filter(r => r.type==="context")
|
||||||
|
expect(contextBindings.length).toBe(4)
|
||||||
|
|
||||||
|
const namebinding_parent = contextBindings.find(b => b.runtimeBinding === "context._parent.data.name")
|
||||||
|
expect(namebinding_parent).toBeDefined()
|
||||||
|
expect(namebinding_parent.readableBinding).toBe("list-name.Test Model.name")
|
||||||
|
|
||||||
|
const descriptionbinding_parent = contextBindings.find(b => b.runtimeBinding === "context._parent.data.description")
|
||||||
|
expect(descriptionbinding_parent).toBeDefined()
|
||||||
|
expect(descriptionbinding_parent.readableBinding).toBe("list-name.Test Model.description")
|
||||||
|
|
||||||
|
const namebinding_own = contextBindings.find(b => b.runtimeBinding === "context.data.name")
|
||||||
|
expect(namebinding_own).toBeDefined()
|
||||||
|
expect(namebinding_own.readableBinding).toBe("child-list-name.Test Model.name")
|
||||||
|
|
||||||
|
const descriptionbinding_own = contextBindings.find(b => b.runtimeBinding === "context.data.description")
|
||||||
|
expect(descriptionbinding_own).toBeDefined()
|
||||||
|
expect(descriptionbinding_own.readableBinding).toBe("child-list-name.Test Model.description")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should return bindable component props, from components in same context", () => {
|
it("should return bindable component props, from components in same context", () => {
|
||||||
|
const result = fetchBindableProperties({
|
||||||
|
componentInstanceId: "list-item-heading-id",
|
||||||
|
...testData()
|
||||||
|
})
|
||||||
|
const componentBinding = result.find(r => r.instance._id === "list-item-input-id" && r.type === "instance")
|
||||||
|
expect(componentBinding).toBeDefined()
|
||||||
|
expect(componentBinding.runtimeBinding).toBe("context.list-item-input-id.value")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should not return model props from child context", () => {
|
it("should not return components from child context", () => {
|
||||||
|
const result = fetchBindableProperties({
|
||||||
|
componentInstanceId: "list-item-heading-id",
|
||||||
|
...testData()
|
||||||
|
})
|
||||||
|
const componentBinding = result.find(r => r.instance._id === "child-list-item-input-id" && r.type === "instance")
|
||||||
|
expect(componentBinding).not.toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return bindable component props, from components in same context (when nested context)", () => {
|
||||||
|
const result = fetchBindableProperties({
|
||||||
|
componentInstanceId: "child-list-item-heading-id",
|
||||||
|
...testData()
|
||||||
|
})
|
||||||
|
const componentBinding = result.find(r => r.instance._id === "child-list-item-input-id" && r.type === "instance")
|
||||||
|
expect(componentBinding).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -83,6 +135,26 @@ const testData = () => {
|
||||||
_component: "@budibase/standard-components/input",
|
_component: "@budibase/standard-components/input",
|
||||||
value: "list item"
|
value: "list item"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
_id: "child-list-id",
|
||||||
|
_component: "@budibase/standard-components/list",
|
||||||
|
_instanceName: "child-list-name",
|
||||||
|
model: "test-model-id",
|
||||||
|
_children: [
|
||||||
|
{
|
||||||
|
_id: "child-list-item-heading-id",
|
||||||
|
_instanceName: "child list item heading",
|
||||||
|
_component: "@budibase/standard-components/heading",
|
||||||
|
text: "hello"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "child-list-item-input-id",
|
||||||
|
_instanceName: "Child List Item Input",
|
||||||
|
_component: "@budibase/standard-components/input",
|
||||||
|
value: "child list item"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in New Issue