mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
18 changed files with 368 additions and 815 deletions
@ -0,0 +1,27 @@ |
|||
/** |
|||
* buildStateOrigins |
|||
* |
|||
* Builds an object that details all the bound state in the application, and what updates it. |
|||
* |
|||
* @param screenDefinition - the screen definition metadata. |
|||
* @returns {Object} an object with the client state values and how they are managed. |
|||
*/ |
|||
export const buildStateOrigins = screenDefinition => { |
|||
const origins = {}; |
|||
|
|||
function traverse(propValue) { |
|||
for (let key in propValue) { |
|||
if (!Array.isArray(propValue[key])) continue; |
|||
|
|||
if (key === "_children") propValue[key].forEach(traverse); |
|||
|
|||
for (let element of propValue[key]) { |
|||
if (element["##eventHandlerType"] === "Set State") origins[element.parameters.path] = element; |
|||
} |
|||
} |
|||
} |
|||
|
|||
traverse(screenDefinition.props); |
|||
|
|||
return origins; |
|||
}; |
|||
@ -0,0 +1,142 @@ |
|||
<script> |
|||
import { ArrowDownIcon } from "../common/Icons/"; |
|||
import { store } from "../builderStore"; |
|||
import { buildStateOrigins } from "../builderStore/buildStateOrigins"; |
|||
import { isBinding, getBinding, setBinding } from "../common/binding"; |
|||
|
|||
export let onChanged = () => {}; |
|||
export let value = ""; |
|||
|
|||
let isOpen = false; |
|||
let stateBindings = []; |
|||
|
|||
let bindingPath = ""; |
|||
let bindingFallbackValue = ""; |
|||
let bindingSource = "store"; |
|||
let bindingValue = ""; |
|||
|
|||
const bind = (path, fallback, source) => { |
|||
if (!path) { |
|||
onChanged(fallback); |
|||
return; |
|||
} |
|||
const binding = setBinding({ path, fallback, source }); |
|||
onChanged(binding); |
|||
}; |
|||
|
|||
const setBindingPath = value => |
|||
bind(value, bindingFallbackValue, bindingSource); |
|||
|
|||
const setBindingFallback = value => bind(bindingPath, value, bindingSource); |
|||
|
|||
const setBindingSource = value => |
|||
bind(bindingPath, bindingFallbackValue, value); |
|||
|
|||
$: { |
|||
const binding = getBinding(value); |
|||
if (bindingPath !== binding.path) isOpen = false; |
|||
bindingPath = binding.path; |
|||
bindingValue = typeof value === "object" ? "" : value; |
|||
bindingFallbackValue = binding.fallback || bindingValue; |
|||
|
|||
const currentScreen = $store.screens.find( |
|||
({ name }) => name === $store.currentPreviewItem.name |
|||
); |
|||
stateBindings = currentScreen ? Object.keys(buildStateOrigins(currentScreen)) : []; |
|||
} |
|||
</script> |
|||
|
|||
<div class="cascader"> |
|||
<div class="input-box"> |
|||
<input |
|||
class:bold={!bindingFallbackValue && bindingPath} |
|||
class="uk-input uk-form-small" |
|||
value={bindingFallbackValue || bindingPath} |
|||
on:change={e => { |
|||
setBindingFallback(e.target.value); |
|||
onChanged(e.target.value); |
|||
}} /> |
|||
<button on:click={() => (isOpen = !isOpen)}> |
|||
<div |
|||
class="icon" |
|||
class:highlighted={bindingPath} |
|||
style={`transform: rotate(${isOpen ? 0 : 90}deg);`}> |
|||
<ArrowDownIcon size={36} /> |
|||
</div> |
|||
</button> |
|||
</div> |
|||
{#if isOpen} |
|||
<ul class="options"> |
|||
{#each stateBindings as stateBinding} |
|||
<li |
|||
class:bold={stateBinding === bindingPath} |
|||
on:click={() => { |
|||
setBindingPath(stateBinding === bindingPath ? null : stateBinding); |
|||
}}> |
|||
{stateBinding} |
|||
</li> |
|||
{/each} |
|||
</ul> |
|||
{/if} |
|||
</div> |
|||
|
|||
<style> |
|||
.bold { |
|||
font-weight: bold; |
|||
} |
|||
|
|||
.highlighted { |
|||
color: rgba(0, 85, 255, 0.8); |
|||
} |
|||
|
|||
button { |
|||
cursor: pointer; |
|||
outline: none; |
|||
border: none; |
|||
border-radius: 5px; |
|||
background: rgba(249, 249, 249, 1); |
|||
|
|||
font-size: 1.6rem; |
|||
font-weight: 700; |
|||
color: rgba(22, 48, 87, 1); |
|||
} |
|||
|
|||
.cascader { |
|||
position: relative; |
|||
width: 100%; |
|||
} |
|||
|
|||
.input-box { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.options { |
|||
width: 172px; |
|||
margin: 0; |
|||
position: absolute; |
|||
top: 35px; |
|||
padding: 10px; |
|||
z-index: 1; |
|||
background: rgba(249, 249, 249, 1); |
|||
min-height: 50px; |
|||
border-radius: 2px; |
|||
} |
|||
|
|||
li { |
|||
list-style-type: none; |
|||
} |
|||
|
|||
li:hover { |
|||
cursor: pointer; |
|||
font-weight: bold; |
|||
} |
|||
|
|||
input { |
|||
margin-right: 5px; |
|||
border: 1px solid #dbdbdb; |
|||
border-radius: 2px; |
|||
opacity: 0.5; |
|||
height: 40px; |
|||
} |
|||
</style> |
|||
@ -1,160 +1,46 @@ |
|||
<script> |
|||
import IconButton from "../common/IconButton.svelte" |
|||
import Input from "../common/Input.svelte" |
|||
import PropertyCascader from "./PropertyCascader.svelte" |
|||
import { isBinding, getBinding, setBinding } from "../common/binding" |
|||
|
|||
export let value = "" |
|||
export let onChanged = () => {} |
|||
export let type = "" |
|||
export let options = [] |
|||
|
|||
let isBound = false |
|||
let bindingPath = "" |
|||
let bindingFallbackValue = "" |
|||
let bindingSource = "store" |
|||
let isExpanded = false |
|||
let forceIsBound = false |
|||
let canOnlyBind = false |
|||
|
|||
$: { |
|||
canOnlyBind = type === "state" |
|||
if (!forceIsBound && canOnlyBind) forceIsBound = true |
|||
|
|||
isBound = forceIsBound || isBinding(value) |
|||
|
|||
if (isBound) { |
|||
const binding = getBinding(value) |
|||
bindingPath = binding.path |
|||
bindingFallbackValue = binding.fallback |
|||
bindingSource = binding.source || "store" |
|||
} else { |
|||
bindingPath = "" |
|||
bindingFallbackValue = "" |
|||
bindingSource = "store" |
|||
} |
|||
} |
|||
|
|||
const clearBinding = () => { |
|||
forceIsBound = false |
|||
onChanged("") |
|||
} |
|||
|
|||
const bind = (path, fallback, source) => { |
|||
if (!path) { |
|||
clearBinding("") |
|||
return |
|||
} |
|||
const binding = setBinding({ path, fallback, source }) |
|||
onChanged(binding) |
|||
} |
|||
|
|||
const setBindingPath = ev => { |
|||
forceIsBound = canOnlyBind |
|||
bind(ev.target.value, bindingFallbackValue, bindingSource) |
|||
} |
|||
|
|||
const setBindingFallback = ev => { |
|||
bind(bindingPath, ev.target.value, bindingSource) |
|||
} |
|||
|
|||
const setBindingSource = ev => { |
|||
bind(bindingPath, bindingFallbackValue, ev.target.value) |
|||
} |
|||
</script> |
|||
|
|||
{#if isBound} |
|||
<div> |
|||
<div class="bound-header"> |
|||
<div>{isExpanded ? '' : bindingPath}</div> |
|||
<div class="unbound-container"> |
|||
{#if type === 'bool'} |
|||
<div> |
|||
<IconButton |
|||
icon={isExpanded ? 'chevron-up' : 'chevron-down'} |
|||
size="12" |
|||
on:click={() => (isExpanded = !isExpanded)} /> |
|||
{#if !canOnlyBind} |
|||
<IconButton icon="trash" size="12" on:click={clearBinding} /> |
|||
{/if} |
|||
icon={value == true ? 'check-square' : 'square'} |
|||
size="19" |
|||
on:click={() => onChanged(!value)} /> |
|||
</div> |
|||
{#if isExpanded} |
|||
<div> |
|||
<div class="binding-prop-label">Binding Path</div> |
|||
<input |
|||
class="uk-input uk-form-small" |
|||
value={bindingPath} |
|||
on:change={setBindingPath} /> |
|||
<div class="binding-prop-label">Fallback Value</div> |
|||
<input |
|||
class="uk-input uk-form-small" |
|||
value={bindingFallbackValue} |
|||
on:change={setBindingFallback} /> |
|||
<div class="binding-prop-label">Binding Source</div> |
|||
<select |
|||
class="uk-select uk-form-small" |
|||
value={bindingSource} |
|||
on:change={setBindingSource}> |
|||
|
|||
<option>store</option> |
|||
<option>context</option> |
|||
|
|||
</select> |
|||
</div> |
|||
{/if} |
|||
|
|||
</div> |
|||
{:else} |
|||
<div class="unbound-container"> |
|||
|
|||
{#if type === 'bool'} |
|||
<div> |
|||
<IconButton |
|||
icon={value == true ? 'check-square' : 'square'} |
|||
size="19" |
|||
on:click={() => onChanged(!value)} /> |
|||
</div> |
|||
{:else if type === 'options'} |
|||
<select |
|||
class="uk-select uk-form-small" |
|||
{value} |
|||
on:change={ev => onChanged(ev.target.value)}> |
|||
{#each options as option} |
|||
<option value={option}>{option}</option> |
|||
{/each} |
|||
</select> |
|||
{:else} |
|||
<Input on:change={ev => onChanged(ev.target.value)} bind:value /> |
|||
{/if} |
|||
|
|||
</div> |
|||
{/if} |
|||
{:else if type === 'options'} |
|||
<select |
|||
class="uk-select uk-form-small" |
|||
{value} |
|||
on:change={ev => onChanged(ev.target.value)}> |
|||
{#each options as option} |
|||
<option value={option}>{option}</option> |
|||
{/each} |
|||
</select> |
|||
{:else} |
|||
<PropertyCascader {onChanged} {value} /> |
|||
{/if} |
|||
</div> |
|||
|
|||
<style> |
|||
.unbound-container { |
|||
display: flex; |
|||
} |
|||
|
|||
.bound-header { |
|||
display: flex; |
|||
} |
|||
|
|||
.bound-header > div:nth-child(1) { |
|||
flex: 1 0 auto; |
|||
width: 30px; |
|||
color: var(--secondary50); |
|||
padding-left: 5px; |
|||
} |
|||
|
|||
.binding-prop-label { |
|||
color: var(--secondary50); |
|||
} |
|||
|
|||
input { |
|||
font-size: 12px; |
|||
font-weight: 700; |
|||
color: #163057; |
|||
opacity: 0.7; |
|||
padding: 5px 10px; |
|||
box-sizing: border-box; |
|||
border: 1px solid #dbdbdb; |
|||
border-radius: 2px; |
|||
outline: none; |
|||
} |
|||
</style> |
|||
|
|||
@ -0,0 +1,29 @@ |
|||
import { buildStateOrigins } from "../src/builderStore/buildStateOrigins"; |
|||
|
|||
it("builds the correct stateOrigins object from a screen definition with handlers", () => { |
|||
expect(buildStateOrigins({ |
|||
"name": "screen1", |
|||
"description": "", |
|||
"props": { |
|||
"_component": "@budibase/standard-components/div", |
|||
"className": "", |
|||
"onClick": [ |
|||
{ |
|||
"##eventHandlerType": "Set State", |
|||
"parameters": { |
|||
"path": "testKey", |
|||
"value": "value" |
|||
} |
|||
} |
|||
] |
|||
} |
|||
})).toEqual({ |
|||
"testKey": { |
|||
"##eventHandlerType": "Set State", |
|||
"parameters": { |
|||
"path": "testKey", |
|||
"value": "value" |
|||
} |
|||
} |
|||
}); |
|||
}); |
|||
File diff suppressed because it is too large
@ -0,0 +1,2 @@ |
|||
window['##BUDIBASE_FRONTEND_DEINITION##'] = {"componentLibraries":[{"importPath":"/lib/customComponents/index.js","libName":"./customComponents"},{"importPath":"/lib/moreCustomComponents/index.js","libName":"./moreCustomComponents"}],"appRootPath":"","page":{"title":"Test App","favicon":"./_shared/favicon.png","stylesheets":["my-styles.css"],"componentLibraries":["./customComponents","./moreCustomComponents"],"props":{"_component":"@budibase/standard-components/div"}},"screens":[{"name":"screen1","description":"","props":{"_component":"@budibase/standard-components/div","className":""},"_css":"/css/d121e1ecc6cf44f433213222e9ff5d40.css"},{"name":"screen2","description":"","props":{"_component":"@budibase/standard-components/div","className":""},"_css":"/css/7b7c05b78e05c06eb8d69475caadfea3.css"}]}; |
|||
window['##BUDIBASE_FRONTEND_FUNCTIONS##'] = {'1234':() => 'test return'} |
|||
File diff suppressed because one or more lines are too long
Loading…
Reference in new issue