Merge pull request #420 from mjashanks/bugfixes

Bugfixes from #387
This commit is contained in:
Michael Shanks 2020-07-07 21:09:36 +01:00 committed by GitHub
commit cf82bf9e1c
14 changed files with 322 additions and 71 deletions

View File

@ -103,10 +103,12 @@
class="budibase__input" class="budibase__input"
bind:value={$backendUiStore.draftModel.name} /> bind:value={$backendUiStore.draftModel.name} />
</div> </div>
<!-- dont have this capability yet..
<div class="titled-input"> <div class="titled-input">
<header>Import Data</header> <header>Import Data</header>
<Button wide secondary>Import CSV</Button> <Button wide secondary>Import CSV</Button>
</div> </div>
-->
{/if} {/if}
<footer> <footer>
<Button disabled={!edited} green={edited} wide on:click={saveModel}> <Button disabled={!edited} green={edited} wide on:click={saveModel}>

View File

@ -34,6 +34,11 @@ export default `<html>
background: #e9e6ff44; background: #e9e6ff44;
height: 100%; height: 100%;
} }
.container-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

@ -303,6 +303,16 @@ export default {
key: "logo", key: "logo",
control: Input, control: Input,
}, },
{
label: "Title",
key: "title",
control: Input,
},
{
label: "Button Text",
key: "buttonText",
control: Input,
},
], ],
}, },
}, },
@ -335,6 +345,16 @@ export default {
key: "model", key: "model",
control: ModelSelect, control: ModelSelect,
}, },
{
label: "Title",
key: "title",
control: Input,
},
{
label: "Button Text",
key: "buttonText",
control: Input,
},
], ],
}, },
template: { template: {
@ -355,6 +375,16 @@ export default {
key: "model", key: "model",
control: ModelSelect, control: ModelSelect,
}, },
{
label: "Title",
key: "title",
control: Input,
},
{
label: "Button Text",
key: "buttonText",
control: Input,
},
], ],
}, },
}, },

View File

@ -38,15 +38,21 @@
// Loop through each ID // Loop through each ID
ids.forEach(id => { ids.forEach(id => {
// Find ID and select it // Find ID
componentToSelect = currentChildren.find(child => child._id === id) const component = currentChildren.find(child => child._id === id)
// If it does not exist, ignore (use last valid route)
if (!component) return
componentToSelect = component
// Update childrens array to selected components children // Update childrens array to selected components children
currentChildren = componentToSelect._children currentChildren = componentToSelect._children
}) })
// Select Component! // Select Component!
store.selectComponent(componentToSelect) if (componentToSelect)
store.selectComponent(componentToSelect)
} }
</script> </script>

View File

@ -30,34 +30,33 @@ export const attachChildren = initialiseOpts => (htmlElement, options) => {
} }
} }
const contextArray = Array.isArray(context) ? context : [context]
const childNodes = [] const childNodes = []
for (let childProps of treeNode.props._children) {
const { componentName, libName } = splitName(childProps._component)
if (!componentName || !libName) return for (let context of contextArray) {
for (let childProps of treeNode.props._children) {
const { componentName, libName } = splitName(childProps._component)
const ComponentConstructor = componentLibraries[libName][componentName] if (!componentName || !libName) return
const prepareNodes = ctx => { const ComponentConstructor = componentLibraries[libName][componentName]
const childNodesThisIteration = prepareRenderComponent({
props: childProps,
parentNode: treeNode,
ComponentConstructor,
htmlElement,
anchor,
context: ctx,
})
for (let childNode of childNodesThisIteration) { const prepareNodes = ctx => {
childNodes.push(childNode) const childNodesThisIteration = prepareRenderComponent({
props: childProps,
parentNode: treeNode,
ComponentConstructor,
htmlElement,
anchor,
context: ctx,
})
for (let childNode of childNodesThisIteration) {
childNodes.push(childNode)
}
} }
}
if (Array.isArray(context)) {
for (let singleCtx of context) {
prepareNodes(singleCtx)
}
} else {
prepareNodes(context) prepareNodes(context)
} }
} }
@ -100,7 +99,10 @@ const areTreeNodesEqual = (children1, children2) => {
let isEqual = false let isEqual = false
for (let i = 0; i < children1.length; i++) { for (let i = 0; i < children1.length; i++) {
isEqual = deepEqual(children1[i].context, children2[i].context) // same context and same children, then nothing has changed
isEqual =
deepEqual(children1[i].context, children2[i].context) &&
areTreeNodesEqual(children1[i].children, children2[i].children)
if (!isEqual) return false if (!isEqual) return false
if (isScreenSlot(children1[i].parentNode.props._component)) { if (isScreenSlot(children1[i].parentNode.props._component)) {
isEqual = deepEqual(children1[i].props, children2[i].props) isEqual = deepEqual(children1[i].props, children2[i].props)

View File

@ -78,13 +78,14 @@ export const createTreeNode = () => ({
get destroy() { get destroy() {
const node = this const node = this
return () => { return () => {
if (node.unsubscribe) node.unsubscribe()
if (node.component && node.component.$destroy) node.component.$destroy()
if (node.children) { if (node.children) {
// destroy children first - from leaf nodes up
for (let child of node.children) { for (let child of node.children) {
child.destroy() child.destroy()
} }
} }
if (node.unsubscribe) node.unsubscribe()
if (node.component && node.component.$destroy) node.component.$destroy()
for (let onDestroyItem of node.onDestroy) { for (let onDestroyItem of node.onDestroy) {
onDestroyItem() onDestroyItem()
} }

View File

@ -1,5 +1,5 @@
import regexparam from "regexparam" import regexparam from "regexparam"
import { routerStore } from "../state/store" import { appStore } from "../state/store"
import { getAppId } from "./getAppId" import { getAppId } from "./getAppId"
export const screenRouter = ({ screens, onScreenSelected, window }) => { export const screenRouter = ({ screens, onScreenSelected, window }) => {
@ -49,7 +49,7 @@ export const screenRouter = ({ screens, onScreenSelected, window }) => {
}) })
} }
routerStore.update(state => { appStore.update(state => {
state["##routeParams"] = params state["##routeParams"] = params
return state return state
}) })

View File

@ -8,6 +8,7 @@ export const bbFactory = ({
store, store,
componentLibraries, componentLibraries,
onScreenSlotRendered, onScreenSlotRendered,
getCurrentState,
}) => { }) => {
const apiCall = method => (url, body) => { const apiCall = method => (url, body) => {
return fetch(url, { return fetch(url, {
@ -53,6 +54,8 @@ export const bbFactory = ({
store: store, store: store,
api, api,
parent, parent,
// these parameters are populated by screenRouter
routeParams: () => getCurrentState()["##routeParams"],
} }
} }
} }

View File

@ -26,8 +26,14 @@ export const createStateManager = ({
routeTo, routeTo,
}) => { }) => {
let handlerTypes = eventHandlers(routeTo) let handlerTypes = eventHandlers(routeTo)
let currentState
// creating a reference to the current state
// this avoids doing store.get() ... which is expensive on
// hot paths, according to the svelte docs.
// the state object reference never changes (although it's internals do)
// so this should work fine for us
let currentState
appStore.subscribe(s => (currentState = s))
const getCurrentState = () => currentState const getCurrentState = () => currentState
const bb = bbFactory({ const bb = bbFactory({

View File

@ -28,7 +28,8 @@
"_instanceName": "Login", "_instanceName": "Login",
"inputClass": "", "inputClass": "",
"_children": [], "_children": [],
"name": "{{ name }}", "title": "Login to {{ name }}",
"buttonText": "Login",
"logo": "" "logo": ""
} }
], ],

View File

@ -66,7 +66,7 @@
"props": { "props": {
"logo": "asset", "logo": "asset",
"loginRedirect": "string", "loginRedirect": "string",
"name": "string", "title": "string",
"usernameLabel": { "usernameLabel": {
"type": "string", "type": "string",
"default": "Username" "default": "Username"
@ -80,7 +80,8 @@
"default": "Login" "default": "Login"
}, },
"buttonClass": "string", "buttonClass": "string",
"inputClass": "string" "inputClass": "string",
"buttonText": "string"
}, },
"tags": [ "tags": [
"login", "login",
@ -206,14 +207,18 @@
"description": "an HTML table that fetches data from a table or view and displays it.", "description": "an HTML table that fetches data from a table or view and displays it.",
"data": true, "data": true,
"props": { "props": {
"model": "models" "model": "models",
"title": "string",
"buttonText": "string"
} }
}, },
"dataformwide": { "dataformwide": {
"description": "an HTML table that fetches data from a table or view and displays it.", "description": "an HTML table that fetches data from a table or view and displays it.",
"data": true, "data": true,
"props": { "props": {
"model": "models" "model": "models",
"title": "string",
"buttonText": "string"
} }
}, },
"datalist": { "datalist": {

View File

@ -1,8 +1,11 @@
<script> <script>
import { onMount } from "svelte" import { onMount } from "svelte"
import { fade } from "svelte/transition"
export let _bb export let _bb
export let model export let model
export let title
export let buttonText
const TYPE_MAP = { const TYPE_MAP = {
string: "text", string: "text",
@ -10,14 +13,16 @@
number: "number", number: "number",
} }
let username let record
let password
let newModel = {
modelId: model,
}
let store = _bb.store let store = _bb.store
let schema = {} let schema = {}
let modelDef = {} let modelDef = {}
let saved = false
let saving = false
let recordId
let isNew = true
let inputElements = {}
$: if (model && model.length !== 0) { $: if (model && model.length !== 0) {
fetchModel() fetchModel()
@ -25,6 +30,8 @@
$: fields = Object.keys(schema) $: fields = Object.keys(schema)
$: Object.values(inputElements).length && setForm(record)
async function fetchModel() { async function fetchModel() {
const FETCH_MODEL_URL = `/api/models/${model}` const FETCH_MODEL_URL = `/api/models/${model}`
const response = await _bb.api.get(FETCH_MODEL_URL) const response = await _bb.api.get(FETCH_MODEL_URL)
@ -33,14 +40,61 @@
} }
async function save() { async function save() {
// prevent double clicking firing multiple requests
if (saving) return
saving = true
const SAVE_RECORD_URL = `/api/${model}/records` const SAVE_RECORD_URL = `/api/${model}/records`
const response = await _bb.api.post(SAVE_RECORD_URL, newModel) const response = await _bb.api.post(SAVE_RECORD_URL, record)
const json = await response.json() const json = await response.json()
store.update(state => { if (response.status === 200) {
state[model] = state[model] ? [...state[model], json] : [json] store.update(state => {
return state state[model] = state[model] ? [...state[model], json] : [json]
}) return state
})
// wipe form, if new record, otherwise update
// model to get new _rev
if (isNew) {
resetForm()
} else {
record = json
}
// set saved, and unset after 1 second
// i.e. make the success notifier appear, then disappear again after time
saved = true
setTimeout(() => {
saved = false
}, 1000)
}
saving = false
}
// we cannot use svelte bind on these inputs, as it does not allow
// bind, when the input type is dynamic
const resetForm = () => {
for (let el of Object.values(inputElements)) {
el.value = ""
if (el.checked) {
el.checked = false
}
}
record = {
modelId: model
}
}
const setForm = rec => {
if (isNew || !rec) return
for (let fieldName in inputElements) {
if (typeof rec[fieldName] === "boolean") {
inputElements[fieldName].checked = rec[fieldName]
} else {
inputElements[fieldName].value = rec[fieldName]
}
}
} }
const handleInput = field => event => { const handleInput = field => event => {
@ -48,36 +102,57 @@
if (event.target.type === "checkbox") { if (event.target.type === "checkbox") {
value = event.target.checked value = event.target.checked
newModel[field] = value record[field] = value
return return
} }
if (event.target.type === "number") { if (event.target.type === "number") {
value = parseInt(event.target.value) value = parseInt(event.target.value)
newModel[field] = value record[field] = value
return return
} }
value = event.target.value value = event.target.value
newModel[field] = value record[field] = value
} }
onMount(() => {
const routeParams = _bb.routeParams()
recordId = Object.keys(routeParams).length > 0 && (routeParams.id || routeParams[0])
isNew = !recordId || recordId === "new"
if (isNew) {
record = { modelId: model }
} else {
const GET_RECORD_URL = `/api/${model}/records/${recordId}`
_bb.api.get(GET_RECORD_URL)
.then(response => response.json())
.then(rec => {
record = rec
setForm(rec)
})
}
});
</script> </script>
<form class="form" on:submit|preventDefault> <form class="form" on:submit|preventDefault>
<h1>{modelDef.name} Form</h1> {#if title}
<h1>{title}</h1>
{/if}
<hr /> <hr />
<div class="form-content"> <div class="form-content">
{#each fields as field} {#each fields as field}
<div class="form-item"> <div class="form-item">
<label class="form-label" for="form-stacked-text">{field}</label> <label class="form-label" for="form-stacked-text">{field}</label>
{#if schema[field].type === 'string' && schema[field].constraints.inclusion} {#if schema[field].type === 'string' && schema[field].constraints.inclusion}
<select on:blur={handleInput(field)}> <select on:blur={handleInput(field)} bind:this={inputElements[field]}>
{#each schema[field].constraints.inclusion as opt} {#each schema[field].constraints.inclusion as opt}
<option>{opt}</option> <option>{opt}</option>
{/each} {/each}
</select> </select>
{:else} {:else}
<input <input
bind:this={inputElements[field]}
class="input" class="input"
type={TYPE_MAP[schema[field].type]} type={TYPE_MAP[schema[field].type]}
on:change={handleInput(field)} /> on:change={handleInput(field)} />
@ -86,7 +161,17 @@
<hr /> <hr />
{/each} {/each}
<div class="button-block"> <div class="button-block">
<button on:click={save}>Submit Form</button> <button on:click={save} class:saved>
{#if saved}
<div in:fade>
<span class:saved style="margin-right: 5px">🎉</span>Success<span class:saved style="margin-left: 5px">🎉</span>
</div>
{:else}
<div>
{buttonText || "Submit Form"}
</div>
{/if}
</button>
</div> </div>
</div> </div>
</form> </form>
@ -156,6 +241,10 @@
text-align: center; text-align: center;
} }
button.saved {
background-color: green;
}
button:hover { button:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05); 0 4px 6px -2px rgba(0, 0, 0, 0.05);

View File

@ -1,7 +1,11 @@
<script> <script>
import { onMount } from "svelte" import { onMount } from "svelte"
import { fade } from "svelte/transition"
export let _bb export let _bb
export let model export let model
export let title
export let buttonText
const TYPE_MAP = { const TYPE_MAP = {
string: "text", string: "text",
@ -9,65 +13,146 @@
number: "number", number: "number",
} }
let username let record
let password
let newModel = {
modelId: model,
}
let store = _bb.store let store = _bb.store
let schema = {} let schema = {}
let modelDef = {} let modelDef = {}
let saved = false
let saving = false
let recordId
let isNew = true
let inputElements = {}
$: if (model && model.length !== 0) { $: if (model && model.length !== 0) {
fetchModel() fetchModel()
} }
$: fields = Object.keys(schema) $: fields = Object.keys(schema)
$: Object.values(inputElements).length && setForm(record)
async function fetchModel() { async function fetchModel() {
const FETCH_MODEL_URL = `/api/models/${model}` const FETCH_MODEL_URL = `/api/models/${model}`
const response = await _bb.api.get(FETCH_MODEL_URL) const response = await _bb.api.get(FETCH_MODEL_URL)
modelDef = await response.json() modelDef = await response.json()
schema = modelDef.schema schema = modelDef.schema
} }
async function save() { async function save() {
// prevent double clicking firing multiple requests
if (saving) return
saving = true
const SAVE_RECORD_URL = `/api/${model}/records` const SAVE_RECORD_URL = `/api/${model}/records`
const response = await _bb.api.post(SAVE_RECORD_URL, newModel) const response = await _bb.api.post(SAVE_RECORD_URL, record)
const json = await response.json() const json = await response.json()
store.update(state => {
state[model] = state[model] ? [...state[model], json] : [json] if (response.status === 200) {
return state store.update(state => {
}) state[model] = state[model] ? [...state[model], json] : [json]
return state
})
// wipe form, if new record, otherwise update
// model to get new _rev
if (isNew) {
resetForm()
} else {
record = json
}
// set saved, and unset after 1 second
// i.e. make the success notifier appear, then disappear again after time
saved = true
setTimeout(() => {
saved = false
}, 1000)
}
saving = false
} }
// we cannot use svelte bind on these inputs, as it does not allow
// bind, when the input type is dynamic
const resetForm = () => {
for (let el of Object.values(inputElements)) {
el.value = ""
if (el.checked) {
el.checked = false
}
}
record = {
modelId: model
}
}
const setForm = rec => {
if (isNew || !rec) return
for (let fieldName in inputElements) {
if (typeof rec[fieldName] === "boolean") {
inputElements[fieldName].checked = rec[fieldName]
} else {
inputElements[fieldName].value = rec[fieldName]
}
}
}
const handleInput = field => event => { const handleInput = field => event => {
let value let value
if (event.target.type === "checkbox") { if (event.target.type === "checkbox") {
value = event.target.checked value = event.target.checked
newModel[field] = value record[field] = value
return return
} }
if (event.target.type === "number") { if (event.target.type === "number") {
value = parseInt(event.target.value) value = parseInt(event.target.value)
newModel[field] = value record[field] = value
return return
} }
value = event.target.value value = event.target.value
newModel[field] = value record[field] = value
} }
onMount(() => {
const routeParams = _bb.routeParams()
recordId = Object.keys(routeParams).length > 0 && (routeParams.id || routeParams[0])
isNew = !recordId || recordId === "new"
if (isNew) {
record = { modelId: model }
} else {
const GET_RECORD_URL = `/api/${model}/records/${recordId}`
_bb.api.get(GET_RECORD_URL)
.then(response => response.json())
.then(rec => {
record = rec
setForm(rec)
})
}
});
</script> </script>
<form class="form" on:submit|preventDefault> <form class="form" on:submit|preventDefault>
<h1>{modelDef.name} Form</h1> {#if title}
<h1>{title}</h1>
{/if}
<hr /> <hr />
<div class="form-content"> <div class="form-content">
{#each fields as field} {#each fields as field}
<div class="form-item"> <div class="form-item">
<label class="form-label" for="form-stacked-text">{field}</label> <label class="form-label" for="form-stacked-text">{field}</label>
{#if schema[field].type === 'string' && schema[field].constraints.inclusion} {#if schema[field].type === 'string' && schema[field].constraints.inclusion}
<select on:blur={handleInput(field)}> <select on:blur={handleInput(field)} bind:this={inputElements[field]}>
{#each schema[field].constraints.inclusion as opt} {#each schema[field].constraints.inclusion as opt}
<option>{opt}</option> <option>{opt}</option>
{/each} {/each}
</select> </select>
{:else} {:else}
<input <input
bind:this={inputElements[field]}
class="input" class="input"
type={TYPE_MAP[schema[field].type]} type={TYPE_MAP[schema[field].type]}
on:change={handleInput(field)} /> on:change={handleInput(field)} />
@ -76,7 +161,17 @@
<hr /> <hr />
{/each} {/each}
<div class="button-block"> <div class="button-block">
<button on:click={save}>Submit Form</button> <button on:click={save} class:saved>
{#if saved}
<div in:fade>
<span class:saved style="margin-right: 5px">🎉</span>Success<span class:saved style="margin-left: 5px">🎉</span>
</div>
{:else}
<div>
{buttonText || "Submit Form"}
</div>
{/if}
</button>
</div> </div>
</div> </div>
</form> </form>
@ -137,6 +232,10 @@
text-align: center; text-align: center;
} }
button.saved {
background-color: green;
}
button:hover { button:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05); 0 4px 6px -2px rgba(0, 0, 0, 0.05);

View File

@ -1,9 +1,9 @@
<script> <script>
import Button from "./Button.svelte" import Button from "./Button.svelte"
export let loginButtonLabel = "Login" export let buttonText = "Login"
export let logo = "" export let logo = ""
export let name = "" export let title = ""
export let buttonClass = "" export let buttonClass = ""
export let inputClass = "" export let inputClass = ""
@ -47,7 +47,9 @@
</div> </div>
{/if} {/if}
<h1 class="header-content">Log in to {name}</h1> {#if title}
<h1 class="header-content">{title}</h1>
{/if}
<div class="form-root"> <div class="form-root">
<div class="control"> <div class="control">
@ -69,7 +71,7 @@
<div class="login-button-container"> <div class="login-button-container">
<button disabled={loading} on:click={login} class={_buttonClass}> <button disabled={loading} on:click={login} class={_buttonClass}>
Log in to {name} {buttonText || "Login"}
</button> </button>
</div> </div>