forked from tsai/budibase
50 changed files with 460 additions and 336 deletions
@ -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} |
|
||||
|
|||||
@ -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() |
||||
@ -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() |
||||
@ -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() |
||||
@ -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 }, |
|
||||
} |
|
||||
} |
|
||||
@ -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" |
||||
|
|||||
@ -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) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
} |
||||
@ -1,3 +0,0 @@ |
|||||
export { getAppId } from "./getAppId" |
|
||||
export { styleable } from "./styleable" |
|
||||
export { enrichDataBinding } from "./enrichDataBinding" |
|
||||
@ -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} /> |
|
||||
|
|||||
@ -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> |
||||
Loading…
Reference in new issue