1
0
Fork 0
mirror of synced 2024-06-28 11:00:55 +12:00

Merge branch 'component-sdk' of github.com:Budibase/budibase into feature/page-refactor

This commit is contained in:
mike12345567 2020-11-23 11:30:15 +00:00
commit b63ca545bf
16 changed files with 96 additions and 201 deletions

View file

@ -24,7 +24,7 @@ import { cloneDeep, difference } from "lodash/fp"
* @returns {Array.<BindableProperty>} * @returns {Array.<BindableProperty>}
*/ */
export default function({ componentInstanceId, screen, components, tables }) { export default function({ componentInstanceId, screen, components, tables }) {
const walkResult = walk({ const result = walk({
// cloning so we are free to mutate props (e.g. by adding _contexts) // cloning so we are free to mutate props (e.g. by adding _contexts)
instance: cloneDeep(screen.props), instance: cloneDeep(screen.props),
targetId: componentInstanceId, targetId: componentInstanceId,
@ -33,13 +33,10 @@ export default function({ componentInstanceId, screen, components, tables }) {
}) })
return [ return [
...walkResult.bindableInstances ...result.bindableInstances
.filter(isInstanceInSharedContext(walkResult)) .filter(isInstanceInSharedContext(result))
.map(componentInstanceToBindable(walkResult)), .map(componentInstanceToBindable),
...(result.target?._contexts.map(contextToBindables(tables)).flat() ?? []),
...(walkResult.target?._contexts
.map(contextToBindables(tables, walkResult))
.flat() ?? []),
] ]
} }
@ -53,26 +50,18 @@ const isInstanceInSharedContext = walkResult => i =>
// 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 = walkResult => i => { const componentInstanceToBindable = i => {
const lastContext =
i.instance._contexts.length &&
i.instance._contexts[i.instance._contexts.length - 1]
const contextParentPath = lastContext
? getParentPath(walkResult, lastContext)
: ""
return { return {
type: "instance", type: "instance",
instance: i.instance, instance: i.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: `${contextParentPath}${i.instance._id}.${i.prop}`, runtimeBinding: `${i.instance._id}`,
// how the binding exressions looks to the user of the builder // how the binding exressions looks to the user of the builder
readableBinding: `${i.instance._instanceName}`, readableBinding: `${i.instance._instanceName}`,
} }
} }
const contextToBindables = (tables, walkResult) => context => { const contextToBindables = tables => context => {
const contextParentPath = getParentPath(walkResult, context)
const tableId = context.table?.tableId ?? context.table const tableId = context.table?.tableId ?? context.table
const table = tables.find(table => table._id === tableId) const table = tables.find(table => table._id === tableId)
let schema = let schema =
@ -98,7 +87,7 @@ const contextToBindables = (tables, walkResult) => context => {
fieldSchema, fieldSchema,
instance: context.instance, instance: context.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: `${contextParentPath}data.${runtimeBoundKey}`, runtimeBinding: `${context.instance._id}.${runtimeBoundKey}`,
// how the binding expressions looks to the user of the builder // how the binding expressions looks to the user of the builder
readableBinding: `${context.instance._instanceName}.${table.name}.${key}`, readableBinding: `${context.instance._instanceName}.${table.name}.${key}`,
// table / view info // table / view info
@ -118,20 +107,6 @@ const contextToBindables = (tables, walkResult) => context => {
) )
} }
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, tables, result }) => { const walk = ({ instance, targetId, components, tables, result }) => {
if (!result) { if (!result) {
result = { result = {

View file

@ -12,10 +12,7 @@ export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
return boundValue === `{{ ${readableBinding} }}` return boundValue === `{{ ${readableBinding} }}`
}) })
if (binding) { if (binding) {
result = textWithBindings.replace( result = result.replace(boundValue, `{{ ${binding.runtimeBinding} }}`)
boundValue,
`{{ ${binding.runtimeBinding} }}`
)
} }
}) })
return result return result

View file

@ -1,28 +1,13 @@
<script> <script>
import { store, backendUiStore } from "builderStore" import { onMount } from "svelte"
import { map, join } from "lodash/fp" import { store } from "builderStore"
import iframeTemplate from "./iframeTemplate" import iframeTemplate from "./iframeTemplate"
import { pipe } from "../../../helpers" import { Screen } from "builderStore/store/screenTemplates/utils/Screen"
import { Screen } from "../../../builderStore/store/screenTemplates/utils/Screen" import { Component } from "builderStore/store/screenTemplates/utils/Component"
import { Component } from "../../../builderStore/store/screenTemplates/utils/Component"
let iframe let iframe
let styles = ""
function transform_component(comp) {
const props = comp.props || comp
if (props && props._children && props._children.length) {
props._children = props._children.map(transform_component)
}
return props
}
const getComponentTypeName = component => {
let [componentName] = component._component.match(/[a-zA-Z]*$/)
return componentName || "element"
}
// Styles for screenslot placeholder
const headingStyle = { const headingStyle = {
width: "500px", width: "500px",
padding: "8px", padding: "8px",
@ -70,71 +55,39 @@
// TODO: this ID is attached to how the screen slot is rendered, confusing, would be better a type etc // TODO: this ID is attached to how the screen slot is rendered, confusing, would be better a type etc
screenPlaceholder.props._id = "screenslot-placeholder" screenPlaceholder.props._id = "screenslot-placeholder"
$: hasComponent = !!$store.currentPreviewItem // Extract data to pass to the iframe
$: page = $store.pages[$store.currentPageName]
$: { $: screen =
styles = "" $store.currentFrontEndType === "page"
// Apply the CSS from the currently selected page and its screens ? screenPlaceholder
const currentPage = $store.pages[$store.currentPageName] : $store.currentPreviewItem
styles += currentPage._css $: selectedComponentId = $store.currentComponentInfo?._id ?? ""
for (let screen of currentPage._screens) { $: previewData = {
styles += screen._css page,
} screen,
styles = styles selectedComponentId,
} }
$: stylesheetLinks = pipe($store.pages.stylesheets, [ // Update the iframe with the builder info to render the correct preview
map(s => `<link rel="stylesheet" href="${s}"/>`),
join("\n"),
])
$: screensExist =
$store.currentPreviewItem._screens &&
$store.currentPreviewItem._screens.length > 0
$: frontendDefinition = {
appId: $store.appId,
libraries: $store.libraries,
page: $store.pages[$store.currentPageName],
screens: [
$store.currentFrontEndType === "page"
? screenPlaceholder
: $store.currentPreviewItem,
],
}
$: selectedComponentType = getComponentTypeName($store.currentComponentInfo)
$: selectedComponentId = $store.currentComponentInfo
? $store.currentComponentInfo._id
: ""
const refreshContent = () => { const refreshContent = () => {
iframe.contentWindow.postMessage( if (iframe) {
JSON.stringify({ iframe.contentWindow.postMessage(JSON.stringify(previewData))
styles, }
stylesheetLinks,
selectedComponentType,
selectedComponentId,
frontendDefinition,
appId: $store.appId,
instanceId: $backendUiStore.selectedDatabase._id,
})
)
} }
$: if (iframe) // Refrech the preview when required
$: refreshContent(previewData)
// Initialise the app when mounted
onMount(() => {
iframe.contentWindow.addEventListener("bb-ready", refreshContent, { iframe.contentWindow.addEventListener("bb-ready", refreshContent, {
once: true, once: true,
}) })
})
$: if (iframe && frontendDefinition) {
refreshContent()
}
</script> </script>
<div class="component-container"> <div class="component-container">
{#if hasComponent && $store.currentPreviewItem} {#if $store.currentPreviewItem}
<iframe <iframe
style="height: 100%; width: 100%" style="height: 100%; width: 100%"
title="componentPreview" title="componentPreview"
@ -152,7 +105,6 @@
margin: auto; margin: auto;
height: 100%; height: 100%;
} }
.component-container iframe { .component-container iframe {
border: 0; border: 0;
left: 0; left: 0;

View file

@ -11,7 +11,7 @@ export default `<html>
*, *:before, *:after { *, *:before, *:after {
box-sizing: border-box; box-sizing: border-box;
} }
[data-bb-id="container-screenslot-placeholder"] { [data-bb-id="screenslot-placeholder"] {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -23,7 +23,7 @@ export default `<html>
background-color: rgba(0, 0, 0, 0.05); background-color: rgba(0, 0, 0, 0.05);
flex: 1 1 auto; flex: 1 1 auto;
} }
[data-bb-id="container-screenslot-placeholder"] span { [data-bb-id="screenslot-placeholder"] span {
display: block; display: block;
margin-bottom: 10px; margin-bottom: 10px;
} }
@ -31,45 +31,46 @@ export default `<html>
<script src='/assets/budibase-client.js'></script> <script src='/assets/budibase-client.js'></script>
<script> <script>
function receiveMessage(event) { function receiveMessage(event) {
if (!event.data) {
return
}
if (!event.data) return // Extract data from message
const { selectedComponentId, page, screen } = JSON.parse(event.data)
const data = JSON.parse(event.data)
// Update selected component style
try { if (selectedComponentStyle) {
if (styles) document.head.removeChild(styles) document.head.removeChild(selectedComponentStyle)
} catch(_) { } }
selectedComponentStyle = document.createElement("style");
try {
if (selectedComponentStyle) document.head.removeChild(selectedComponentStyle)
} catch(_) { }
selectedComponentStyle = document.createElement('style');
document.head.appendChild(selectedComponentStyle) document.head.appendChild(selectedComponentStyle)
var selectedCss = '[data-bb-id="' + data.selectedComponentType + '-' + data.selectedComponentId + '"]' + '{border:2px solid #0055ff !important;}' var selectedCss = '[data-bb-id="' + selectedComponentId + '"]' + '{border:2px solid #0055ff !important;}'
selectedComponentStyle.appendChild(document.createTextNode(selectedCss)) selectedComponentStyle.appendChild(document.createTextNode(selectedCss))
styles = document.createElement('style') // Set some flags so the app knows we're in the builder
document.head.appendChild(styles)
styles.appendChild(document.createTextNode(data.styles))
window["##BUDIBASE_IN_BUILDER##"] = true; window["##BUDIBASE_IN_BUILDER##"] = true;
window["##BUDIBASE_PREVIEW_PAGE##"] = page;
window["##BUDIBASE_PREVIEW_SCREEN##"] = screen;
// Initialise app
if (window.loadBudibase) { if (window.loadBudibase) {
loadBudibase() loadBudibase()
} }
} }
let styles
let selectedComponentStyle let selectedComponentStyle
document.addEventListener("click", function(e) { // Ignore clicks
e.preventDefault() ["click", "mousedown"].forEach(type => {
e.stopPropagation() document.addEventListener(type, function(e) {
return false; e.preventDefault()
}, true) e.stopPropagation()
return false
}, true)
})
window.addEventListener('message', receiveMessage) window.addEventListener("message", receiveMessage)
window.dispatchEvent(new Event('bb-ready')) window.dispatchEvent(new Event("bb-ready"))
</script> </script>
</head> </head>
<body> <body>

View file

@ -30,15 +30,13 @@
// Extract component definition info // Extract component definition info
const componentName = extractComponentName(definition._component) const componentName = extractComponentName(definition._component)
const constructor = getComponentConstructor(componentName) const constructor = getComponentConstructor(componentName)
const id = `${componentName}-${definition._id}`
const componentProps = extractValidProps(definition) const componentProps = extractValidProps(definition)
const dataContext = getContext("data") const dataContext = getContext("data")
const enrichedProps = dataContext.actions.enrichDataBindings(componentProps) const enrichedProps = dataContext.actions.enrichDataBindings(componentProps)
const children = definition._children const children = definition._children
// Set style context to be consumed by component // Set contexts to be consumed by component
setContext("style", { ...definition._styles, id }) setContext("style", { ...definition._styles, id: definition._id })
$: console.log("Rendering: " + componentName)
</script> </script>
{#if constructor} {#if constructor}

View file

@ -6,6 +6,7 @@
// Get current contexts // Get current contexts
const dataContext = getContext("data") const dataContext = getContext("data")
const { id } = getContext("style")
// Clone current context to this context // Clone current context to this context
const newDataContext = createDataContextStore($dataContext) const newDataContext = createDataContextStore($dataContext)
@ -14,7 +15,7 @@
// Add additional layer to context // Add additional layer to context
let loaded = false let loaded = false
onMount(() => { onMount(() => {
newDataContext.actions.addContext(row) newDataContext.actions.addContext(row, id)
loaded = true loaded = true
}) })
</script> </script>

View file

@ -4,7 +4,7 @@
// Keep route params up to date // Keep route params up to date
export let params export let params
$: routeStore.actions.setRouteParams(params) $: routeStore.actions.setRouteParams(params || {})
// Get the screen definition for the current route // Get the screen definition for the current route
$: screenDefinition = $screenStore.activeScreen?.props $: screenDefinition = $screenStore.activeScreen?.props

View file

@ -3,7 +3,6 @@ import { enrichDataBinding } from "../utils"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
const initialValue = { const initialValue = {
parent: null,
data: null, data: null,
} }
@ -12,15 +11,12 @@ export const createDataContextStore = existingContext => {
const store = writable(initial) const store = writable(initial)
// Adds a context layer to the data context tree // Adds a context layer to the data context tree
const addContext = row => { const addContext = (row, componentId) => {
store.update(state => { store.update(state => {
if (state.data) { if (row && componentId) {
state.parent = { state[componentId] = row
parent: state.parent, state.data = row
data: state.data,
}
} }
state.data = row
return state return state
}) })
} }

View file

@ -24,11 +24,20 @@ const createScreenStore = () => {
}) })
const fetchScreens = async () => { const fetchScreens = async () => {
const appDefinition = await API.fetchAppDefinition(getAppId()) let screens
config.set({ let page
screens: appDefinition.screens, const inBuilder = !!window["##BUDIBASE_IN_BUILDER##"]
page: appDefinition.page, if (inBuilder) {
}) // Load screen and page from the window object if in the builder
screens = [window["##BUDIBASE_PREVIEW_SCREEN##"]]
page = window["##BUDIBASE_PREVIEW_PAGE##"]
} else {
// Otherwise load from API
const appDefinition = await API.fetchAppDefinition(getAppId())
screens = appDefinition.screens
page = appDefinition.page
}
config.set({ screens, page })
} }
return { return {

View file

@ -28,8 +28,5 @@ export const enrichDataBinding = (input, context) => {
if (!looksLikeMustache.test(input)) { if (!looksLikeMustache.test(input)) {
return input return input
} }
console.log("====================================")
console.log(input)
console.log(context)
return mustache.render(input, context) return mustache.render(input, context)
} }

View file

@ -130,15 +130,6 @@
"form" "form"
] ]
}, },
"select": {
"name": "Select",
"bindable": "value",
"description": "An HTML <select> (dropdown)",
"props": {
"value": "string",
"className": "string"
}
},
"option": { "option": {
"name": "Option", "name": "Option",
"description": "An HTML <option>, to be used with <select>", "description": "An HTML <option>, to be used with <select>",
@ -181,28 +172,6 @@
"value": "string" "value": "string"
} }
}, },
"checkbox": {
"name": "Checkbox",
"bindable": "value",
"description": "A selectable checkbox component",
"props": {
"label": "string",
"checked": "bool",
"value": "string",
"onchange": "event"
}
},
"radiobutton": {
"name": "Radiobutton",
"bindable": "value",
"description": "A selectable radiobutton component",
"props": {
"label": "string",
"checked": "bool",
"value": "string",
"onchange": "event"
}
},
"icon": { "icon": {
"description": "A HTML icon tag", "description": "A HTML icon tag",
"props": { "props": {
@ -687,7 +656,8 @@
"description": "Date Picker", "description": "Date Picker",
"bindable": "value", "bindable": "value",
"props": { "props": {
"placeholder": "string" "placeholder": "string",
"value": "string"
} }
}, },
"link": { "link": {

View file

@ -23,7 +23,6 @@
// if srcdoc, then we assume this is the builder preview // if srcdoc, then we assume this is the builder preview
if ((pathParts.length === 0 || pathParts[0] === "srcdoc") && table) { if ((pathParts.length === 0 || pathParts[0] === "srcdoc") && table) {
console.log("getting first row")
row = await fetchFirstRow() row = await fetchFirstRow()
} else if (routeParamId) { } else if (routeParamId) {
row = await API.fetchRow({ tableId: table, rowId: routeParamId }) row = await API.fetchRow({ tableId: table, rowId: routeParamId })

View file

@ -89,4 +89,4 @@
}) })
</script> </script>
<ApexChart {options} {styles} /> <ApexChart {options} />

View file

@ -71,4 +71,4 @@
}) })
</script> </script>
<ApexChart {options} {styles} /> <ApexChart {options} />

View file

@ -97,4 +97,4 @@
}) })
</script> </script>
<ApexChart {options} {styles} /> <ApexChart {options} />

View file

@ -63,4 +63,4 @@
}) })
</script> </script>
<ApexChart {options} {styles} /> <ApexChart {options} />