forked from tsai/budibase
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> |
<script> |
||||
import IconButton from "../common/IconButton.svelte" |
import IconButton from "../common/IconButton.svelte" |
||||
import Input from "../common/Input.svelte" |
import Input from "../common/Input.svelte" |
||||
|
import PropertyCascader from "./PropertyCascader.svelte" |
||||
import { isBinding, getBinding, setBinding } from "../common/binding" |
import { isBinding, getBinding, setBinding } from "../common/binding" |
||||
|
|
||||
export let value = "" |
export let value = "" |
||||
export let onChanged = () => {} |
export let onChanged = () => {} |
||||
export let type = "" |
export let type = "" |
||||
export let options = [] |
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> |
</script> |
||||
|
|
||||
{#if isBound} |
<div class="unbound-container"> |
||||
<div> |
{#if type === 'bool'} |
||||
<div class="bound-header"> |
<div> |
||||
<div>{isExpanded ? '' : bindingPath}</div> |
|
||||
<IconButton |
<IconButton |
||||
icon={isExpanded ? 'chevron-up' : 'chevron-down'} |
icon={value == true ? 'check-square' : 'square'} |
||||
size="12" |
size="19" |
||||
on:click={() => (isExpanded = !isExpanded)} /> |
on:click={() => onChanged(!value)} /> |
||||
{#if !canOnlyBind} |
|
||||
<IconButton icon="trash" size="12" on:click={clearBinding} /> |
|
||||
{/if} |
|
||||
</div> |
</div> |
||||
{#if isExpanded} |
{:else if type === 'options'} |
||||
<div> |
<select |
||||
<div class="binding-prop-label">Binding Path</div> |
class="uk-select uk-form-small" |
||||
<input |
{value} |
||||
class="uk-input uk-form-small" |
on:change={ev => onChanged(ev.target.value)}> |
||||
value={bindingPath} |
{#each options as option} |
||||
on:change={setBindingPath} /> |
<option value={option}>{option}</option> |
||||
<div class="binding-prop-label">Fallback Value</div> |
{/each} |
||||
<input |
</select> |
||||
class="uk-input uk-form-small" |
{:else} |
||||
value={bindingFallbackValue} |
<PropertyCascader {onChanged} {value} /> |
||||
on:change={setBindingFallback} /> |
{/if} |
||||
<div class="binding-prop-label">Binding Source</div> |
</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} |
|
||||
|
|
||||
<style> |
<style> |
||||
.unbound-container { |
.unbound-container { |
||||
display: flex; |
display: flex; |
||||
} |
} |
||||
|
|
||||
.bound-header { |
|
||||
display: flex; |
|
||||
} |
|
||||
|
|
||||
.bound-header > div:nth-child(1) { |
.bound-header > div:nth-child(1) { |
||||
flex: 1 0 auto; |
flex: 1 0 auto; |
||||
width: 30px; |
width: 30px; |
||||
color: var(--secondary50); |
color: var(--secondary50); |
||||
padding-left: 5px; |
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> |
</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