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"
bind:value={$backendUiStore.draftModel.name} />
</div>
<!-- dont have this capability yet..
<div class="titled-input">
<header>Import Data</header>
<Button wide secondary>Import CSV</Button>
</div>
-->
{/if}
<footer>
<Button disabled={!edited} green={edited} wide on:click={saveModel}>

View File

@ -34,6 +34,11 @@ export default `<html>
background: #e9e6ff44;
height: 100%;
}
.container-screenslot-placeholder span {
display: block;
margin-bottom: 10px;
}
</style>
<script src='/assets/budibase-client.js'></script>
<script>

View File

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

View File

@ -38,14 +38,20 @@
// Loop through each ID
ids.forEach(id => {
// Find ID and select it
componentToSelect = currentChildren.find(child => child._id === id)
// Find 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
currentChildren = componentToSelect._children
})
// Select Component!
if (componentToSelect)
store.selectComponent(componentToSelect)
}
</script>

View File

@ -30,7 +30,11 @@ export const attachChildren = initialiseOpts => (htmlElement, options) => {
}
}
const contextArray = Array.isArray(context) ? context : [context]
const childNodes = []
for (let context of contextArray) {
for (let childProps of treeNode.props._children) {
const { componentName, libName } = splitName(childProps._component)
@ -53,11 +57,6 @@ export const attachChildren = initialiseOpts => (htmlElement, options) => {
}
}
if (Array.isArray(context)) {
for (let singleCtx of context) {
prepareNodes(singleCtx)
}
} else {
prepareNodes(context)
}
}
@ -100,7 +99,10 @@ const areTreeNodesEqual = (children1, children2) => {
let isEqual = false
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 (isScreenSlot(children1[i].parentNode.props._component)) {
isEqual = deepEqual(children1[i].props, children2[i].props)

View File

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

View File

@ -1,5 +1,5 @@
import regexparam from "regexparam"
import { routerStore } from "../state/store"
import { appStore } from "../state/store"
import { getAppId } from "./getAppId"
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
return state
})

View File

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

View File

@ -26,8 +26,14 @@ export const createStateManager = ({
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 bb = bbFactory({

View File

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

View File

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

View File

@ -1,8 +1,11 @@
<script>
import { onMount } from "svelte"
import { fade } from "svelte/transition"
export let _bb
export let model
export let title
export let buttonText
const TYPE_MAP = {
string: "text",
@ -10,14 +13,16 @@
number: "number",
}
let username
let password
let newModel = {
modelId: model,
}
let record
let store = _bb.store
let schema = {}
let modelDef = {}
let saved = false
let saving = false
let recordId
let isNew = true
let inputElements = {}
$: if (model && model.length !== 0) {
fetchModel()
@ -25,6 +30,8 @@
$: fields = Object.keys(schema)
$: Object.values(inputElements).length && setForm(record)
async function fetchModel() {
const FETCH_MODEL_URL = `/api/models/${model}`
const response = await _bb.api.get(FETCH_MODEL_URL)
@ -33,14 +40,61 @@
}
async function save() {
// prevent double clicking firing multiple requests
if (saving) return
saving = true
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()
if (response.status === 200) {
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 => {
@ -48,36 +102,57 @@
if (event.target.type === "checkbox") {
value = event.target.checked
newModel[field] = value
record[field] = value
return
}
if (event.target.type === "number") {
value = parseInt(event.target.value)
newModel[field] = value
record[field] = value
return
}
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>
<form class="form" on:submit|preventDefault>
<h1>{modelDef.name} Form</h1>
{#if title}
<h1>{title}</h1>
{/if}
<hr />
<div class="form-content">
{#each fields as field}
<div class="form-item">
<label class="form-label" for="form-stacked-text">{field}</label>
{#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}
<option>{opt}</option>
{/each}
</select>
{:else}
<input
bind:this={inputElements[field]}
class="input"
type={TYPE_MAP[schema[field].type]}
on:change={handleInput(field)} />
@ -86,7 +161,17 @@
<hr />
{/each}
<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>
</form>
@ -156,6 +241,10 @@
text-align: center;
}
button.saved {
background-color: green;
}
button:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);

View File

@ -1,7 +1,11 @@
<script>
import { onMount } from "svelte"
import { fade } from "svelte/transition"
export let _bb
export let model
export let title
export let buttonText
const TYPE_MAP = {
string: "text",
@ -9,65 +13,146 @@
number: "number",
}
let username
let password
let newModel = {
modelId: model,
}
let record
let store = _bb.store
let schema = {}
let modelDef = {}
let saved = false
let saving = false
let recordId
let isNew = true
let inputElements = {}
$: if (model && model.length !== 0) {
fetchModel()
}
$: fields = Object.keys(schema)
$: Object.values(inputElements).length && setForm(record)
async function fetchModel() {
const FETCH_MODEL_URL = `/api/models/${model}`
const response = await _bb.api.get(FETCH_MODEL_URL)
modelDef = await response.json()
schema = modelDef.schema
}
async function save() {
// prevent double clicking firing multiple requests
if (saving) return
saving = true
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()
if (response.status === 200) {
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 => {
let value
if (event.target.type === "checkbox") {
value = event.target.checked
newModel[field] = value
record[field] = value
return
}
if (event.target.type === "number") {
value = parseInt(event.target.value)
newModel[field] = value
record[field] = value
return
}
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>
<form class="form" on:submit|preventDefault>
<h1>{modelDef.name} Form</h1>
{#if title}
<h1>{title}</h1>
{/if}
<hr />
<div class="form-content">
{#each fields as field}
<div class="form-item">
<label class="form-label" for="form-stacked-text">{field}</label>
{#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}
<option>{opt}</option>
{/each}
</select>
{:else}
<input
bind:this={inputElements[field]}
class="input"
type={TYPE_MAP[schema[field].type]}
on:change={handleInput(field)} />
@ -76,7 +161,17 @@
<hr />
{/each}
<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>
</form>
@ -137,6 +232,10 @@
text-align: center;
}
button.saved {
background-color: green;
}
button:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);

View File

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