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

This commit is contained in:
mike12345567 2020-11-25 15:04:23 +00:00
commit f2fc4f1a4c
50 changed files with 461 additions and 337 deletions

View File

@ -3,57 +3,17 @@
import { store } from "builderStore" import { store } from "builderStore"
import iframeTemplate from "./iframeTemplate" import iframeTemplate from "./iframeTemplate"
import { Screen } from "builderStore/store/screenTemplates/utils/Screen" import { Screen } from "builderStore/store/screenTemplates/utils/Screen"
import { Component } from "builderStore/store/screenTemplates/utils/Component"
let iframe let iframe
// Styles for screenslot placeholder // Create screen slot placeholder for use when a page is selected rather
const headingStyle = { // than a screen
width: "500px",
padding: "8px",
}
const textStyle = {
...headingStyle,
"max-width": "",
"text-align": "left",
}
const heading = new Component("@budibase/standard-components/heading")
.normalStyle(headingStyle)
.type("h1")
.text("Screen Slot")
.instanceName("Heading")
const textScreenDisplay = new Component("@budibase/standard-components/text")
.normalStyle(textStyle)
.instanceName("Text")
.type("none")
.text(
"The screens that you create will be displayed inside this box. This box is just a placeholder, to show you the position of screens."
)
const container = new Component("@budibase/standard-components/container")
.normalStyle({
display: "flex",
"flex-direction": "column",
"align-items": "center",
flex: "1 1 auto",
})
.type("div")
.instanceName("Container")
.addChild(heading)
.addChild(textScreenDisplay)
const screenPlaceholder = new Screen() const screenPlaceholder = new Screen()
.name("Screen Placeholder") .name("Screen Placeholder")
.route("*") .route("*")
.component("@budibase/standard-components/container") .component("@budibase/standard-components/screenslotplaceholder")
.mainType("div")
.instanceName("Content Placeholder") .instanceName("Content Placeholder")
.normalStyle({
flex: "1 1 auto",
})
.addChild(container)
.json() .json()
// TODO: this ID is attached to how the screen slot is rendered, confusing, would be better a type etc
screenPlaceholder.props._id = "screenslot-placeholder"
// Extract data to pass to the iframe // Extract data to pass to the iframe
$: page = $store.layouts[$store.currentPageName] $: page = $store.layouts[$store.currentPageName]
@ -75,7 +35,7 @@
} }
} }
// Refrech the preview when required // Refresh the preview when required
$: refreshContent(previewData) $: refreshContent(previewData)
// Initialise the app when mounted // Initialise the app when mounted

View File

@ -11,22 +11,6 @@ export default `<html>
*, *:before, *:after { *, *:before, *:after {
box-sizing: border-box; box-sizing: border-box;
} }
[data-bb-id="screenslot-placeholder"] {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
text-align: center;
border-style: dashed !important;
border-width: 1px;
color: #000000;
background-color: rgba(0, 0, 0, 0.05);
flex: 1 1 auto;
}
[data-bb-id="screenslot-placeholder"] span {
display: block;
margin-bottom: 10px;
}
</style> </style>
<script src='/assets/budibase-client.js'></script> <script src='/assets/budibase-client.js'></script>
<script> <script>

View File

@ -40,13 +40,16 @@
$: links = bindableProperties $: links = bindableProperties
.filter(x => x.fieldSchema?.type === "link") .filter(x => x.fieldSchema?.type === "link")
.map(property => ({ .map(property => {
return {
providerId: property.instance._id,
label: property.readableBinding, label: property.readableBinding,
fieldName: property.fieldSchema.name, fieldName: property.fieldSchema.name,
name: `all_${property.fieldSchema.tableId}`, name: `all_${property.fieldSchema.tableId}`,
tableId: property.fieldSchema.tableId, tableId: property.fieldSchema.tableId,
type: "link", type: "link",
})) }
})
</script> </script>
<div <div

View File

@ -1,4 +1,4 @@
import { getAppId } from "../utils" import { getAppId } from "../utils/getAppId"
/** /**
* API cache for cached request responses. * API cache for cached request responses.

View File

@ -19,9 +19,10 @@ export const fetchDatasource = async (datasource, dataContext) => {
} else if (type === "view") { } else if (type === "view") {
rows = await fetchViewData(datasource) rows = await fetchViewData(datasource)
} else if (type === "link") { } else if (type === "link") {
const row = dataContext[datasource.providerId]
rows = await fetchRelationshipData({ rows = await fetchRelationshipData({
rowId: dataContext?.data?._id, rowId: row?._id,
tableId: dataContext?.data?.tableId, tableId: row?.tableId,
fieldName, fieldName,
}) })
} }

View File

@ -1,12 +1,14 @@
<script> <script>
import { writable } from "svelte/store"
import { setContext, onMount } from "svelte" import { setContext, onMount } from "svelte"
import Component from "./Component.svelte" import Component from "./Component.svelte"
import SDK from "../sdk" import SDK from "../sdk"
import { routeStore, screenStore, createDataContextStore } from "../store" import { createDataStore, routeStore, screenStore } from "../store"
// Provide contexts // Provide contexts
setContext("sdk", SDK) setContext("sdk", SDK)
setContext("data", createDataContextStore()) setContext("component", writable({}))
setContext("data", createDataStore())
let loaded = false let loaded = false

View File

@ -1,48 +1,43 @@
<script> <script>
import { getContext, setContext } from "svelte" import { getContext, setContext } from "svelte"
import { writable } from "svelte/store"
import * as ComponentLibrary from "@budibase/standard-components" import * as ComponentLibrary from "@budibase/standard-components"
import Router from "./Router.svelte" import Router from "./Router.svelte"
import { enrichProps } from "../utils/componentProps"
import { bindingStore } from "../store"
export let definition = {} export let definition = {}
// Extracts the actual component name from the library name // Get local data binding context
const extractComponentName = name => { const dataStore = getContext("data")
const split = name?.split("/")
return split?.[split.length - 1]
}
// Extracts valid props to pass to the real svelte component // Create component context
const extractValidProps = component => { const componentStore = writable({})
let props = {} setContext("component", componentStore)
Object.entries(component)
.filter(([name]) => !name.startsWith("_"))
.forEach(([key, value]) => {
props[key] = value
})
return props
}
// Gets the component constructor for the specified component
const getComponentConstructor = name => {
return name === "screenslot" ? Router : ComponentLibrary[componentName]
}
// Extract component definition info // Extract component definition info
const componentName = extractComponentName(definition._component) $: constructor = getComponentConstructor(definition._component)
const constructor = getComponentConstructor(componentName) $: children = definition._children
const componentProps = extractValidProps(definition) $: id = definition._id
const dataContext = getContext("data") $: enrichedProps = enrichProps(definition, $dataStore, $bindingStore)
const enrichedProps = dataContext.actions.enrichDataBindings(componentProps)
const children = definition._children
// Set contexts to be consumed by component // Update component context
setContext("style", { ...definition._styles, id: definition._id }) // ID is duplicated inside style so that the "styleable" helper can set
// an ID data tag for unique reference to components
$: componentStore.set({ id, styles: { ...definition._styles, id } })
// Gets the component constructor for the specified component
const getComponentConstructor = component => {
const split = component?.split("/")
const name = split?.[split.length - 1]
return name === "screenslot" ? Router : ComponentLibrary[name]
}
</script> </script>
{#if constructor} {#if constructor}
<svelte:component this={constructor} {...enrichedProps}> <svelte:component this={constructor} {...enrichedProps}>
{#if children && children.length} {#if children && children.length}
{#each children as child} {#each children as child (child._id)}
<svelte:self definition={child} /> <svelte:self definition={child} />
{/each} {/each}
{/if} {/if}

View File

@ -1,25 +1,15 @@
<script> <script>
import { onMount, getContext, setContext } from "svelte" import { getContext, setContext } from "svelte"
import { createDataContextStore } from "../store" import { createDataStore } from "../store"
export let row export let row
// Get current contexts // Clone and create new data context for this component tree
const dataContext = getContext("data") const data = getContext("data")
const { id } = getContext("style") const component = getContext("component")
const newData = createDataStore($data)
// Clone current context to this context setContext("data", newData)
const newDataContext = createDataContextStore($dataContext) $: newData.actions.addContext(row, $component.id)
setContext("data", newDataContext)
// Add additional layer to context
let loaded = false
onMount(() => {
newDataContext.actions.addContext(row, id)
loaded = true
})
</script> </script>
{#if loaded}
<slot /> <slot />
{/if}

View File

@ -1,11 +1,11 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
import Router from "svelte-spa-router" import Router from "svelte-spa-router"
import { routeStore, screenStore } from "../store" import { routeStore } from "../store"
import Screen from "./Screen.svelte" import Screen from "./Screen.svelte"
const { styleable } = getContext("sdk") const { styleable } = getContext("sdk")
const styles = getContext("style") const component = getContext("component")
$: routerConfig = getRouterConfig($routeStore.routes) $: routerConfig = getRouterConfig($routeStore.routes)
@ -26,7 +26,7 @@
</script> </script>
{#if routerConfig} {#if routerConfig}
<div use:styleable={styles}> <div use:styleable={$component.styles}>
<Router on:routeLoading={onRouteLoading} routes={routerConfig} /> <Router on:routeLoading={onRouteLoading} routes={routerConfig} />
</div> </div>
{/if} {/if}

View File

@ -11,8 +11,11 @@
// Redirect to home page if no matching route // Redirect to home page if no matching route
$: screenDefinition == null && routeStore.actions.navigate("/") $: screenDefinition == null && routeStore.actions.navigate("/")
// Make a screen array so we can use keying to properly re-render each screen
$: screens = screenDefinition ? [screenDefinition] : []
</script> </script>
{#if screenDefinition} {#each screens as screen (screen._id)}
<Component definition={screenDefinition} /> <Component definition={screen} />
{/if} {/each}

View File

@ -1,12 +1,23 @@
import ClientApp from "./components/ClientApp.svelte" import ClientApp from "./components/ClientApp.svelte"
import { builderStore } from "./store"
let app
// Initialise client app
const loadBudibase = () => { const loadBudibase = () => {
window.document.body.innerHTML = "" // Update builder store with any builder flags
new ClientApp({ builderStore.set({
inBuilder: !!window["##BUDIBASE_IN_BUILDER##"],
page: window["##BUDIBASE_PREVIEW_PAGE##"],
screen: window["##BUDIBASE_PREVIEW_SCREEN##"],
})
// Create app if one hasn't been created yet
if (!app) {
app = new ClientApp({
target: window.document.body, target: window.document.body,
}) })
} }
}
// Attach to window so the HTML template can call this when it loads // Attach to window so the HTML template can call this when it loads
window.loadBudibase = loadBudibase window.loadBudibase = loadBudibase

View File

@ -1,6 +1,7 @@
import * as API from "./api" import * as API from "./api"
import { authStore, routeStore, screenStore } from "./store" import { authStore, routeStore, screenStore, bindingStore } from "./store"
import { styleable, getAppId } from "./utils" import { styleable } from "./utils/styleable"
import { getAppId } from "./utils/getAppId"
import { link as linkable } from "svelte-spa-router" import { link as linkable } from "svelte-spa-router"
import DataProvider from "./components/DataProvider.svelte" import DataProvider from "./components/DataProvider.svelte"
@ -13,4 +14,5 @@ export default {
linkable, linkable,
getAppId, getAppId,
DataProvider, DataProvider,
setBindableValue: bindingStore.actions.setBindableValue,
} }

View File

@ -1,13 +1,10 @@
import * as API from "../api" import * as API from "../api"
import { getAppId } from "../utils" import { getAppId } from "../utils/getAppId"
import { writable } from "svelte/store" import { writable } from "svelte/store"
const createAuthStore = () => { const createAuthStore = () => {
const store = writable("") const store = writable("")
/**
* Logs a user in.
*/
const logIn = async ({ username, password }) => { const logIn = async ({ username, password }) => {
const user = await API.logIn({ username, password }) const user = await API.logIn({ username, password })
if (!user.error) { if (!user.error) {
@ -15,10 +12,6 @@ const createAuthStore = () => {
location.reload() location.reload()
} }
} }
/**
* Logs a user out.
*/
const logOut = () => { const logOut = () => {
store.set("") store.set("")
const appId = getAppId() const appId = getAppId()

View File

@ -0,0 +1,21 @@
import { writable } from "svelte/store"
const createBindingStore = () => {
const store = writable({})
const setBindableValue = (value, componentId) => {
store.update(state => {
if (componentId) {
state[componentId] = value
}
return state
})
}
return {
subscribe: store.subscribe,
actions: { setBindableValue },
}
}
export const bindingStore = createBindingStore()

View File

@ -0,0 +1,12 @@
import { writable } from "svelte/store"
const createBuilderStore = () => {
const initialState = {
inBuilder: false,
page: null,
screen: null,
}
return writable(initialState)
}
export const builderStore = createBuilderStore()

View File

@ -0,0 +1,26 @@
import { writable } from "svelte/store"
import { cloneDeep } from "lodash/fp"
export const createDataStore = existingContext => {
const store = writable({ ...existingContext })
// Adds a context layer to the data context tree
const addContext = (row, componentId) => {
store.update(state => {
if (componentId) {
state[componentId] = row
state[`${componentId}_draft`] = cloneDeep(row)
state.closestComponentId = componentId
}
return state
})
}
return {
subscribe: store.subscribe,
update: store.update,
actions: { addContext },
}
}
export const dataStore = createDataStore()

View File

@ -1,39 +0,0 @@
import { writable, get } from "svelte/store"
import { enrichDataBinding } from "../utils"
import { cloneDeep } from "lodash/fp"
const initialValue = {
data: null,
}
export const createDataContextStore = existingContext => {
const initial = existingContext ? cloneDeep(existingContext) : initialValue
const store = writable(initial)
// Adds a context layer to the data context tree
const addContext = (row, componentId) => {
store.update(state => {
if (row && componentId) {
state[componentId] = row
state.data = row
}
return state
})
}
// Enriches props by running mustache and filling in any data bindings present
// in the prop values
const enrichDataBindings = props => {
const state = get(store)
let enrichedProps = {}
Object.entries(props).forEach(([key, value]) => {
enrichedProps[key] = enrichDataBinding(value, state)
})
return enrichedProps
}
return {
subscribe: store.subscribe,
actions: { addContext, enrichDataBindings },
}
}

View File

@ -1,4 +1,8 @@
export { authStore } from "./auth" export { authStore } from "./auth"
export { routeStore } from "./routes" export { routeStore } from "./routes"
export { screenStore } from "./screens" export { screenStore } from "./screens"
export { createDataContextStore } from "./dataContext" export { builderStore } from "./builder"
export { bindingStore } from "./binding"
// Data stores are layered and duplicated, so it is not a singleton
export { createDataStore, dataStore } from "./data"

View File

@ -1,43 +1,45 @@
import { writable, derived } from "svelte/store" import { writable, derived } from "svelte/store"
import { routeStore } from "./routes" import { routeStore } from "./routes"
import { builderStore } from "./builder"
import * as API from "../api" import * as API from "../api"
import { getAppId } from "../utils" import { getAppId } from "../utils/getAppId"
const createScreenStore = () => { const createScreenStore = () => {
const config = writable({ const config = writable({
screens: [], screens: [],
page: {}, page: {},
}) })
const store = derived([config, routeStore], ([$config, $routeStore]) => { const store = derived(
const { screens, page } = $config [config, routeStore, builderStore],
const activeScreen = ([$config, $routeStore, $builderStore]) => {
screens.length === 1 let page
? screens[0] let activeScreen
: screens.find( if ($builderStore.inBuilder) {
// Use builder defined definitions if inside the builder preview
page = $builderStore.page
activeScreen = $builderStore.screen
} else {
// Otherwise find the correct screen by matching the current route
page = $config.page
const { screens } = $config
if (screens.length === 1) {
activeScreen = screens[0]
} else {
activeScreen = screens.find(
screen => screen.routing.route === $routeStore.activeRoute screen => screen.routing.route === $routeStore.activeRoute
) )
return {
screens,
page,
activeScreen,
} }
}) }
return { page, activeScreen }
}
)
const fetchScreens = async () => { const fetchScreens = async () => {
let screens
let page
const inBuilder = !!window["##BUDIBASE_IN_BUILDER##"]
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()) const appDefinition = await API.fetchAppDefinition(getAppId())
screens = appDefinition.screens config.set({
page = appDefinition.page screens: appDefinition.screens,
} page: appDefinition.page,
config.set({ screens, page }) })
} }
return { return {

View File

@ -0,0 +1,45 @@
import { enrichDataBinding } from "./enrichDataBinding"
import { routeStore } from "../store"
import { saveRow, deleteRow } from "../api"
const saveRowHandler = async (action, context) => {
let draft = context[`${action.parameters.contextPath}_draft`]
if (action.parameters.fields) {
Object.entries(action.parameters.fields).forEach(([key, entry]) => {
draft[key] = enrichDataBinding(entry.value, context)
})
}
await saveRow(draft)
}
const deleteRowHandler = async (action, context) => {
const { tableId, revId, rowId } = action.parameters
await deleteRow({
tableId: enrichDataBinding(tableId, context),
rowId: enrichDataBinding(rowId, context),
revId: enrichDataBinding(revId, context),
})
}
const navigationHandler = action => {
routeStore.actions.navigate(action.parameters.url)
}
const handlerMap = {
["Save Row"]: saveRowHandler,
["Delete Row"]: deleteRowHandler,
["Navigate To"]: navigationHandler,
}
/**
* Parses an array of actions and returns a function which will execute the
* actions in the current context.
*/
export const enrichButtonActions = (actions, context) => {
const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]])
return async () => {
for (let i = 0; i < handlers.length; i++) {
await handlers[i](actions[i], context)
}
}
}

View File

@ -0,0 +1,35 @@
import { enrichDataBindings } from "./enrichDataBinding"
import { enrichButtonActions } from "./buttonActions"
/**
* Enriches component props.
* Data bindings are enriched, and button actions are enriched.
*/
export const enrichProps = (props, dataContexts, dataBindings) => {
// Exclude all private props that start with an underscore
let validProps = {}
Object.entries(props)
.filter(([name]) => !name.startsWith("_"))
.forEach(([key, value]) => {
validProps[key] = value
})
// Create context of all bindings and data contexts
// Duplicate the closest context as "data" which the builder requires
const context = {
...dataContexts,
...dataBindings,
data: dataContexts[dataContexts.closestComponentId],
data_draft: dataContexts[`${dataContexts.closestComponentId}_draft`],
}
// Enrich all data bindings in top level props
let enrichedProps = enrichDataBindings(validProps, context)
// Enrich button actions if they exist
if (props._component.endsWith("/button") && enrichedProps.onClick) {
enrichedProps.onClick = enrichButtonActions(enrichedProps.onClick, context)
}
return enrichedProps
}

View File

@ -8,6 +8,9 @@ const entityMap = {
">": "&gt;", ">": "&gt;",
} }
mustache.escape = text => { mustache.escape = text => {
if (text == null || typeof text !== "string") {
return text
}
return text.replace(/[<>]/g, function fromEntityMap(s) { return text.replace(/[<>]/g, function fromEntityMap(s) {
return entityMap[s] || s return entityMap[s] || s
}) })
@ -30,3 +33,14 @@ export const enrichDataBinding = (input, context) => {
} }
return mustache.render(input, context) return mustache.render(input, context)
} }
/**
* Enriches each prop in a props object
*/
export const enrichDataBindings = (props, context) => {
let enrichedProps = {}
Object.entries(props).forEach(([key, value]) => {
enrichedProps[key] = enrichDataBinding(value, context)
})
return enrichedProps
}

View File

@ -1,3 +0,0 @@
export { getAppId } from "./getAppId"
export { styleable } from "./styleable"
export { enrichDataBinding } from "./enrichDataBinding"

View File

@ -1,7 +1,13 @@
import { getContext } from "svelte"
import { get } from "svelte/store"
/**
* Helper to build a CSS string from a style object
*/
const buildStyleString = styles => { const buildStyleString = styles => {
let str = "" let str = ""
Object.entries(styles).forEach(([style, value]) => { Object.entries(styles).forEach(([style, value]) => {
if (style && value) { if (style && value != null) {
str += `${style}: ${value}; ` str += `${style}: ${value}; `
} }
}) })
@ -12,35 +18,52 @@ const buildStyleString = styles => {
* Svelte action to apply correct component styles. * Svelte action to apply correct component styles.
*/ */
export const styleable = (node, styles = {}) => { export const styleable = (node, styles = {}) => {
const normalStyles = styles.normal || {} let applyNormalStyles
let applyHoverStyles
// Creates event listeners and applies initial styles
const setupStyles = newStyles => {
const normalStyles = newStyles.normal || {}
const hoverStyles = { const hoverStyles = {
...normalStyles, ...normalStyles,
...styles.hover, ...newStyles.hover,
} }
function applyNormalStyles() { applyNormalStyles = () => {
node.style = buildStyleString(normalStyles) node.style = buildStyleString(normalStyles)
} }
function applyHoverStyles() { applyHoverStyles = () => {
node.style = buildStyleString(hoverStyles) node.style = buildStyleString(hoverStyles)
} }
// Add listeners to toggle hover styles // Add listeners to toggle hover styles
node.addEventListener("mouseover", applyHoverStyles) node.addEventListener("mouseover", applyHoverStyles)
node.addEventListener("mouseout", applyNormalStyles) node.addEventListener("mouseout", applyNormalStyles)
node.setAttribute("data-bb-id", newStyles.id)
// Apply normal styles initially // Apply initial normal styles
applyNormalStyles() applyNormalStyles()
}
// Also apply data tags so we know how to reference each component // Removes the current event listeners
node.setAttribute("data-bb-id", styles.id) const removeListeners = () => {
return {
// Clean up event listeners when component is destroyed
destroy: () => {
node.removeEventListener("mouseover", applyHoverStyles) node.removeEventListener("mouseover", applyHoverStyles)
node.removeEventListener("mouseout", applyNormalStyles) node.removeEventListener("mouseout", applyNormalStyles)
}
// Apply initial styles
setupStyles(styles)
return {
// Clean up old listeners and apply new ones on update
update: newStyles => {
removeListeners()
setupStyles(newStyles)
},
// Clean up listeners when component is destroyed
destroy: () => {
removeListeners()
}, },
} }
} }

View File

@ -2,14 +2,19 @@
import { getContext } from "svelte" import { getContext } from "svelte"
const { styleable } = getContext("sdk") const { styleable } = getContext("sdk")
const styles = getContext("style") const component = getContext("component")
export let className = "default" export let className = "default"
export let disabled = false export let disabled = false
export let text export let text
export let onClick
</script> </script>
<button class="default" disabled={disabled || false} use:styleable={styles}> <button
class="default"
disabled={disabled || false}
use:styleable={$component.styles}
on:click={onClick}>
{text} {text}
</button> </button>

View File

@ -3,7 +3,7 @@
import { cssVars } from "./helpers" import { cssVars } from "./helpers"
const { styleable } = getContext("sdk") const { styleable } = getContext("sdk")
const styles = getContext("style") const component = getContext("component")
export const className = "" export const className = ""
export let imageUrl = "" export let imageUrl = ""
@ -26,7 +26,10 @@
$: showImage = !!imageUrl $: showImage = !!imageUrl
</script> </script>
<div use:cssVars={cssVariables} class="container" use:styleable={styles}> <div
use:cssVars={cssVariables}
class="container"
use:styleable={$component.styles}>
{#if showImage}<img class="image" src={imageUrl} alt="" />{/if} {#if showImage}<img class="image" src={imageUrl} alt="" />{/if}
<div class="content"> <div class="content">
<h2 class="heading">{heading}</h2> <h2 class="heading">{heading}</h2>

View File

@ -3,7 +3,7 @@
import { cssVars } from "./helpers" import { cssVars } from "./helpers"
const { styleable } = getContext("sdk") const { styleable } = getContext("sdk")
const styles = getContext("style") const component = getContext("component")
export const className = "" export const className = ""
export let imageUrl = "" export let imageUrl = ""
@ -29,7 +29,10 @@
$: showImage = !!imageUrl $: showImage = !!imageUrl
</script> </script>
<div use:cssVars={cssVariables} class="container" use:styleable={styles}> <div
use:cssVars={cssVariables}
class="container"
use:styleable={$component.styles}>
{#if showImage}<img class="image" src={imageUrl} alt="" />{/if} {#if showImage}<img class="image" src={imageUrl} alt="" />{/if}
<div class="content"> <div class="content">
<main> <main>

View File

@ -2,62 +2,62 @@
import { getContext } from "svelte" import { getContext } from "svelte"
const { styleable } = getContext("sdk") const { styleable } = getContext("sdk")
const styles = getContext("style") const component = getContext("component")
export let className = "" export let className = ""
export let type = "div" export let type = "div"
</script> </script>
{#if type === 'div'} {#if type === 'div'}
<div use:styleable={styles}> <div use:styleable={$component.styles}>
<slot /> <slot />
</div> </div>
{:else if type === 'header'} {:else if type === 'header'}
<header use:styleable={styles}> <header use:styleable={$component.styles}>
<slot /> <slot />
</header> </header>
{:else if type === 'main'} {:else if type === 'main'}
<main use:styleable={styles}> <main use:styleable={$component.styles}>
<slot /> <slot />
</main> </main>
{:else if type === 'footer'} {:else if type === 'footer'}
<footer use:styleable={styles}> <footer use:styleable={$component.styles}>
<slot /> <slot />
</footer> </footer>
{:else if type === 'aside'} {:else if type === 'aside'}
<aside use:styleable={styles}> <aside use:styleable={$component.styles}>
<slot /> <slot />
</aside> </aside>
{:else if type === 'summary'} {:else if type === 'summary'}
<summary use:styleable={styles}> <summary use:styleable={$component.styles}>
<slot /> <slot />
</summary> </summary>
{:else if type === 'details'} {:else if type === 'details'}
<details use:styleable={styles}> <details use:styleable={$component.styles}>
<slot /> <slot />
</details> </details>
{:else if type === 'article'} {:else if type === 'article'}
<article use:styleable={styles}> <article use:styleable={$component.styles}>
<slot /> <slot />
</article> </article>
{:else if type === 'nav'} {:else if type === 'nav'}
<nav use:styleable={styles}> <nav use:styleable={$component.styles}>
<slot /> <slot />
</nav> </nav>
{:else if type === 'mark'} {:else if type === 'mark'}
<mark use:styleable={styles}> <mark use:styleable={$component.styles}>
<slot /> <slot />
</mark> </mark>
{:else if type === 'figure'} {:else if type === 'figure'}
<figure use:styleable={styles}> <figure use:styleable={$component.styles}>
<slot /> <slot />
</figure> </figure>
{:else if type === 'figcaption'} {:else if type === 'figcaption'}
<figcaption use:styleable={styles}> <figcaption use:styleable={$component.styles}>
<slot /> <slot />
</figcaption> </figcaption>
{:else if type === 'paragraph'} {:else if type === 'paragraph'}
<p use:styleable={styles}> <p use:styleable={$component.styles}>
<slot /> <slot />
</p> </p>
{/if} {/if}

View File

@ -2,11 +2,13 @@
import { DatePicker } from "@budibase/bbui" import { DatePicker } from "@budibase/bbui"
import { getContext } from "svelte" import { getContext } from "svelte"
const { styleable } = getContext("sdk") const { styleable, setBindableValue } = getContext("sdk")
const styles = getContext("style") const component = getContext("component")
export let placeholder export let placeholder
export let value
let value
$: setBindableValue(value, $component.id)
function handleChange(event) { function handleChange(event) {
const [fullDate] = event.detail const [fullDate] = event.detail
@ -14,6 +16,6 @@
} }
</script> </script>
<div use:styleable={styles}> <div use:styleable={$component.styles}>
<DatePicker {placeholder} on:change={handleChange} {value} /> <DatePicker {placeholder} on:change={handleChange} {value} />
</div> </div>

View File

@ -2,12 +2,12 @@
import { getContext } from "svelte" import { getContext } from "svelte"
const { styleable } = getContext("sdk") const { styleable } = getContext("sdk")
const styles = getContext("style") const component = getContext("component")
export let embed export let embed
</script> </script>
<div use:styleable={styles}> <div use:styleable={$component.styles}>
{@html embed} {@html embed}
</div> </div>

View File

@ -5,9 +5,9 @@
import LinkedRowSelector from "./LinkedRowSelector.svelte" import LinkedRowSelector from "./LinkedRowSelector.svelte"
import { capitalise } from "./helpers" import { capitalise } from "./helpers"
const { styleable, screenStore, API } = getContext("sdk") const { styleable, API } = getContext("sdk")
const dataContextStore = getContext("data") const component = getContext("component")
const styles = getContext("style") const data = getContext("data")
export let wide = false export let wide = false
@ -15,19 +15,22 @@
let schema let schema
let fields = [] let fields = []
$: getContextDetails($dataContextStore) // Fetch info about the closest data context
$: getFormData($data[$data.closestComponentId])
const getContextDetails = async dataContext => { const getFormData = async context => {
row = dataContext?.data if (context) {
if (row) { const tableDefinition = await API.fetchTableDefinition(context.tableId)
const tableDefinition = await API.fetchTableDefinition(row.tableId)
schema = tableDefinition.schema schema = tableDefinition.schema
fields = Object.keys(schema) fields = Object.keys(schema)
// Use the draft version for editing
row = $data[`${$data.closestComponentId}_draft`]
} }
} }
</script> </script>
<div class="form-content" use:styleable={styles}> <div class="form-content" use:styleable={$component.styles}>
<!-- <ErrorsBox errors={$store.saveRowErrors || {}} />--> <!-- <ErrorsBox errors={$store.saveRowErrors || {}} />-->
{#each fields as field} {#each fields as field}
<div class="form-field" class:wide> <div class="form-field" class:wide>

View File

@ -2,7 +2,7 @@
import { getContext } from "svelte" import { getContext } from "svelte"
const { styleable } = getContext("sdk") const { styleable } = getContext("sdk")
const styles = getContext("style") const component = getContext("component")
export let className = "" export let className = ""
export let type export let type
@ -10,15 +10,15 @@
</script> </script>
{#if type === 'h1'} {#if type === 'h1'}
<h1 class={className} use:styleable={styles}>{text}</h1> <h1 class={className} use:styleable={$component.styles}>{text}</h1>
{:else if type === 'h2'} {:else if type === 'h2'}
<h2 class={className} use:styleable={styles}>{text}</h2> <h2 class={className} use:styleable={$component.styles}>{text}</h2>
{:else if type === 'h3'} {:else if type === 'h3'}
<h3 class={className} use:styleable={styles}>{text}</h3> <h3 class={className} use:styleable={$component.styles}>{text}</h3>
{:else if type === 'h4'} {:else if type === 'h4'}
<h4 class={className} use:styleable={styles}>{text}</h4> <h4 class={className} use:styleable={$component.styles}>{text}</h4>
{:else if type === 'h5'} {:else if type === 'h5'}
<h5 class={className} use:styleable={styles}>{text}</h5> <h5 class={className} use:styleable={$component.styles}>{text}</h5>
{:else if type === 'h6'} {:else if type === 'h6'}
<h6 class={className} use:styleable={styles}>{text}</h6> <h6 class={className} use:styleable={$component.styles}>{text}</h6>
{/if} {/if}

View File

@ -3,7 +3,7 @@
import { getContext } from "svelte" import { getContext } from "svelte"
const { styleable } = getContext("sdk") const { styleable } = getContext("sdk")
const styles = getContext("style") const component = getContext("component")
export let icon = "" export let icon = ""
export let size = "fa-lg" export let size = "fa-lg"
@ -13,4 +13,4 @@
<i <i
style={`color: ${color};`} style={`color: ${color};`}
class={`${icon} ${size}`} class={`${icon} ${size}`}
use:styleable={styles} /> use:styleable={$component.styles} />

View File

@ -2,7 +2,7 @@
import { getContext } from "svelte" import { getContext } from "svelte"
const { styleable } = getContext("sdk") const { styleable } = getContext("sdk")
const styles = getContext("style") const component = getContext("component")
export let className = "" export let className = ""
export let url = "" export let url = ""
@ -17,4 +17,4 @@
class={className} class={className}
src={url} src={url}
alt={description} alt={description}
use:styleable={styles} /> use:styleable={$component.styles} />

View File

@ -1,21 +1,12 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
const { styleable } = getContext("sdk") const { styleable, setBindableValue } = getContext("sdk")
const styles = getContext("style") const component = getContext("component")
export let value = "" // Keep bindable value up to date
export let className = "" let value
export let type = "text" $: setBindableValue(value, $component.id)
const onchange = ev => {
value = ev.target.value
}
</script> </script>
<input <input bind:value on:change={onchange} use:styleable={$component.styles} />
class={className}
{type}
{value}
on:change={onchange}
use:styleable={styles} />

View File

@ -2,7 +2,7 @@
import { getContext } from "svelte" import { getContext } from "svelte"
const { linkable, styleable } = getContext("sdk") const { linkable, styleable } = getContext("sdk")
const styles = getContext("style") const component = getContext("component")
export let url = "" export let url = ""
export let text = "" export let text = ""
@ -11,7 +11,7 @@
$: target = openInNewTab ? "_blank" : "_self" $: target = openInNewTab ? "_blank" : "_self"
</script> </script>
<a href={url} use:linkable {target} use:styleable={styles}> <a href={url} use:linkable {target} use:styleable={$component.styles}>
{text} {text}
<slot /> <slot />
</a> </a>

View File

@ -3,8 +3,8 @@
import { isEmpty } from "lodash/fp" import { isEmpty } from "lodash/fp"
const { API, styleable, DataProvider } = getContext("sdk") const { API, styleable, DataProvider } = getContext("sdk")
const dataContextStore = getContext("data") const component = getContext("component")
const styles = getContext("style") const data = getContext("data")
export let datasource = [] export let datasource = []
@ -12,12 +12,12 @@
onMount(async () => { onMount(async () => {
if (!isEmpty(datasource)) { if (!isEmpty(datasource)) {
rows = await API.fetchDatasource(datasource, $dataContextStore) rows = await API.fetchDatasource(datasource, $data)
} }
}) })
</script> </script>
<div use:styleable={styles}> <div use:styleable={$component.styles}>
{#each rows as row} {#each rows as row}
<DataProvider {row}> <DataProvider {row}>
<slot /> <slot />

View File

@ -2,7 +2,7 @@
import { getContext } from "svelte" import { getContext } from "svelte"
const { authStore, styleable } = getContext("sdk") const { authStore, styleable } = getContext("sdk")
const styles = getContext("style") const component = getContext("component")
export let buttonText = "Log In" export let buttonText = "Log In"
export let logo = "" export let logo = ""
@ -29,7 +29,7 @@
} }
</script> </script>
<div class="root" use:styleable={styles}> <div class="root" use:styleable={$component.styles}>
<div class="content"> <div class="content">
{#if logo} {#if logo}
<div class="logo-container"><img src={logo} alt="logo" /></div> <div class="logo-container"><img src={logo} alt="logo" /></div>

View File

@ -2,7 +2,7 @@
import { getContext } from "svelte" import { getContext } from "svelte"
const { authStore, linkable, styleable } = getContext("sdk") const { authStore, linkable, styleable } = getContext("sdk")
const styles = getContext("style") const component = getContext("component")
export let logoUrl export let logoUrl
export let title export let title
@ -12,7 +12,7 @@
} }
</script> </script>
<div class="nav" use:styleable={styles}> <div class="nav" use:styleable={$component.styles}>
<div class="nav__top"> <div class="nav__top">
<a href="/" use:linkable> <a href="/" use:linkable>
{#if logoUrl} {#if logoUrl}

View File

@ -23,6 +23,6 @@
} }
</script> </script>
<div use:styleable={styles}> <div use:styleable={$component.styles}>
<RichText bind:content={value} {options} /> <RichText bind:content={value} {options} />
</div> </div>

View File

@ -0,0 +1,31 @@
<script>
import { getContext } from "svelte"
const component = getContext("component")
const { styleable } = getContext("sdk")
</script>
<div use:styleable={$component.styles}>
<h1>Screen Slot</h1>
<span>
The screens that you create will be displayed inside this box.
<br />
This box is just a placeholder, to show you the position of screens.
</span>
</div>
<style>
div {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
text-align: center;
border-style: dashed !important;
border-width: 1px;
color: #000000;
background-color: rgba(0, 0, 0, 0.05);
flex: 1 1 auto;
}
</style>

View File

@ -2,7 +2,7 @@
import { getContext } from "svelte" import { getContext } from "svelte"
const { styleable } = getContext("sdk") const { styleable } = getContext("sdk")
const styles = getContext("style") const component = getContext("component")
export let imageUrl = "" export let imageUrl = ""
export let heading = "" export let heading = ""
@ -14,7 +14,7 @@
$: showImage = !!imageUrl $: showImage = !!imageUrl
</script> </script>
<div class="container" use:styleable={styles}> <div class="container" use:styleable={$component.styles}>
<a href={destinationUrl}> <a href={destinationUrl}>
<div class="content"> <div class="content">
{#if showImage} {#if showImage}

View File

@ -2,7 +2,7 @@
import { getContext } from "svelte" import { getContext } from "svelte"
const { styleable } = getContext("sdk") const { styleable } = getContext("sdk")
const styles = getContext("style") const component = getContext("component")
export let text = "" export let text = ""
export let className = "" export let className = ""
@ -12,28 +12,28 @@
</script> </script>
{#if isTag('none')} {#if isTag('none')}
<span use:styleable={styles}>{text}</span> <span use:styleable={$component.styles}>{text}</span>
{:else if isTag('bold')} {:else if isTag('bold')}
<b class={className} use:styleable={styles}>{text}</b> <b class={className} use:styleable={$component.styles}>{text}</b>
{:else if isTag('strong')} {:else if isTag('strong')}
<strong class={className} use:styleable={styles}>{text}</strong> <strong class={className} use:styleable={$component.styles}>{text}</strong>
{:else if isTag('italic')} {:else if isTag('italic')}
<i class={className} use:styleable={styles}>{text}</i> <i class={className} use:styleable={$component.styles}>{text}</i>
{:else if isTag('emphasis')} {:else if isTag('emphasis')}
<em class={className} use:styleable={styles}>{text}</em> <em class={className} use:styleable={$component.styles}>{text}</em>
{:else if isTag('mark')} {:else if isTag('mark')}
<mark class={className} use:styleable={styles}>{text}</mark> <mark class={className} use:styleable={$component.styles}>{text}</mark>
{:else if isTag('small')} {:else if isTag('small')}
<small class={className} use:styleable={styles}>{text}</small> <small class={className} use:styleable={$component.styles}>{text}</small>
{:else if isTag('del')} {:else if isTag('del')}
<del class={className} use:styleable={styles}>{text}</del> <del class={className} use:styleable={$component.styles}>{text}</del>
{:else if isTag('ins')} {:else if isTag('ins')}
<ins class={className} use:styleable={styles}>{text}</ins> <ins class={className} use:styleable={$component.styles}>{text}</ins>
{:else if isTag('sub')} {:else if isTag('sub')}
<sub class={className} use:styleable={styles}>{text}</sub> <sub class={className} use:styleable={$component.styles}>{text}</sub>
{:else if isTag('sup')} {:else if isTag('sup')}
<sup class={className} use:styleable={styles}>{text}</sup> <sup class={className} use:styleable={$component.styles}>{text}</sup>
{:else}<span use:styleable={styles}>{text}</span>{/if} {:else}<span use:styleable={$component.styles}>{text}</span>{/if}
<style> <style>
span { span {

View File

@ -3,15 +3,15 @@
import { chart } from "svelte-apexcharts" import { chart } from "svelte-apexcharts"
const { styleable } = getContext("sdk") const { styleable } = getContext("sdk")
const styles = getContext("style") const component = getContext("component")
export let options export let options
</script> </script>
{#if options} {#if options}
<div use:chart={options} use:styleable={styles} /> <div use:chart={options} use:styleable={$component.styles} />
{:else if options === false} {:else if options === false}
<div use:styleable={styles}>Invalid chart options</div> <div use:styleable={$component.styles}>Invalid chart options</div>
{/if} {/if}
<style> <style>

View File

@ -5,7 +5,6 @@
import { isEmpty } from "lodash/fp" import { isEmpty } from "lodash/fp"
const { API } = getContext("sdk") const { API } = getContext("sdk")
const dataContext = getContext("data")
export let title export let title
export let datasource export let datasource
@ -35,7 +34,7 @@
// Fetch, filter and sort data // Fetch, filter and sort data
const schema = (await API.fetchTableDefinition(datasource.tableId)).schema const schema = (await API.fetchTableDefinition(datasource.tableId)).schema
const result = await API.fetchDatasource(datasource, $dataContext) const result = await API.fetchDatasource(datasource)
const reducer = row => (valid, column) => valid && row[column] != null const reducer = row => (valid, column) => valid && row[column] != null
const hasAllColumns = row => allCols.reduce(reducer(row), true) const hasAllColumns = row => allCols.reduce(reducer(row), true)
const data = result const data = result

View File

@ -5,7 +5,6 @@
import { isEmpty } from "lodash/fp" import { isEmpty } from "lodash/fp"
const { API } = getContext("sdk") const { API } = getContext("sdk")
const dataContext = getContext("data")
export let title export let title
export let datasource export let datasource
@ -33,7 +32,7 @@
// Fetch, filter and sort data // Fetch, filter and sort data
const schema = (await API.fetchTableDefinition(datasource.tableId)).schema const schema = (await API.fetchTableDefinition(datasource.tableId)).schema
const result = await API.fetchDatasource(datasource, $dataContext) const result = await API.fetchDatasource(datasource)
const reducer = row => (valid, column) => valid && row[column] != null const reducer = row => (valid, column) => valid && row[column] != null
const hasAllColumns = row => allCols.reduce(reducer(row), true) const hasAllColumns = row => allCols.reduce(reducer(row), true)
const data = result const data = result

View File

@ -5,7 +5,6 @@
import { isEmpty } from "lodash/fp" import { isEmpty } from "lodash/fp"
const { API } = getContext("sdk") const { API } = getContext("sdk")
const dataContext = getContext("data")
// Common props // Common props
export let title export let title
@ -41,7 +40,7 @@
// Fetch, filter and sort data // Fetch, filter and sort data
const schema = (await API.fetchTableDefinition(datasource.tableId)).schema const schema = (await API.fetchTableDefinition(datasource.tableId)).schema
const result = await API.fetchDatasource(datasource, $dataContext) const result = await API.fetchDatasource(datasource)
const reducer = row => (valid, column) => valid && row[column] != null const reducer = row => (valid, column) => valid && row[column] != null
const hasAllColumns = row => allCols.reduce(reducer(row), true) const hasAllColumns = row => allCols.reduce(reducer(row), true)
const data = result const data = result

View File

@ -5,7 +5,6 @@
import { isEmpty } from "lodash/fp" import { isEmpty } from "lodash/fp"
const { API } = getContext("sdk") const { API } = getContext("sdk")
const dataContext = getContext("data")
export let title export let title
export let datasource export let datasource
@ -31,7 +30,7 @@
// Fetch, filter and sort data // Fetch, filter and sort data
const schema = (await API.fetchTableDefinition(datasource.tableId)).schema const schema = (await API.fetchTableDefinition(datasource.tableId)).schema
const result = await API.fetchDatasource(datasource, $dataContext) const result = await API.fetchDatasource(datasource)
const data = result const data = result
.filter(row => row[labelColumn] != null && row[valueColumn] != null) .filter(row => row[labelColumn] != null && row[valueColumn] != null)
.slice(0, 20) .slice(0, 20)

View File

@ -15,8 +15,7 @@
// These maps need to be set up to handle whatever types that are used in the tables. // These maps need to be set up to handle whatever types that are used in the tables.
const setters = new Map([["number", number]]) const setters = new Map([["number", number]])
const SDK = getContext("sdk") const SDK = getContext("sdk")
const dataContext = getContext("data") const component = getContext("component")
const styles = getContext("style")
const { API, styleable } = SDK const { API, styleable } = SDK
export let datasource = {} export let datasource = {}
@ -27,7 +26,13 @@
export let detailUrl export let detailUrl
// Add setting height as css var to allow grid to use correct height // Add setting height as css var to allow grid to use correct height
styles.normal["--grid-height"] = `${height}px` $: gridStyles = {
...$component.styles,
normal: {
...$component.styles.normal,
["--grid-height"]: `${height}px`,
},
}
// These can never change at runtime so don't need to be reactive // These can never change at runtime so don't need to be reactive
let canEdit = editable && datasource && datasource.type !== "view" let canEdit = editable && datasource && datasource.type !== "view"
@ -53,7 +58,7 @@
onMount(async () => { onMount(async () => {
if (!isEmpty(datasource)) { if (!isEmpty(datasource)) {
data = await API.fetchDatasource(datasource, $dataContext) data = await API.fetchDatasource(datasource)
let schema let schema
// Get schema for datasource // Get schema for datasource
@ -143,7 +148,7 @@
href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" /> href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" />
</svelte:head> </svelte:head>
<div class="container" use:styleable={styles}> <div class="container" use:styleable={gridStyles}>
{#if dataLoaded} {#if dataLoaded}
{#if canAddDelete} {#if canAddDelete}
<div class="controls"> <div class="controls">

View File

@ -23,4 +23,5 @@ export { default as rowdetail } from "./RowDetail.svelte"
export { default as newrow } from "./NewRow.svelte" export { default as newrow } from "./NewRow.svelte"
export { default as datepicker } from "./DatePicker.svelte" export { default as datepicker } from "./DatePicker.svelte"
export { default as icon } from "./Icon.svelte" export { default as icon } from "./Icon.svelte"
export { default as screenslotplaceholder } from "./ScreenSlotPlaceholder.svelte"
export * from "./charts" export * from "./charts"