Merge branch 'master' into event-panel

This commit is contained in:
Michael Shanks 2020-09-11 12:04:25 +01:00
commit ac72852563
4 changed files with 112 additions and 198 deletions

View File

@ -21,7 +21,7 @@
"publishdev": "lerna run publishdev",
"publishnpm": "yarn build && lerna publish --force-publish",
"clean": "lerna clean",
"dev": "node ./scripts/symlinkDev.js && lerna run --parallel --stream dev:builder",
"dev": "node ./scripts/symlinkDev.js && lerna run --parallel dev:builder",
"test": "lerna run test",
"lint": "eslint packages",
"lint:fix": "eslint --fix packages",

View File

@ -41,6 +41,7 @@
"d3-selection": "^1.4.2",
"fast-sort": "^2.2.0",
"fusioncharts": "^3.15.1-sr.1",
"lodash.debounce": "^4.0.8",
"svelte-flatpickr": "^2.4.0",
"svelte-fusioncharts": "^1.0.0"
}

View File

@ -1,7 +1,8 @@
<script>
import { onMount } from "svelte"
import { fade } from "svelte/transition"
import { Label } from "@budibase/bbui"
import { Label, DatePicker } from "@budibase/bbui"
import debounce from "lodash.debounce"
export let _bb
export let model
@ -14,60 +15,49 @@
number: "number",
}
const DEFAULTS_FOR_TYPE = {
string: "",
boolean: false,
number: null,
link: [],
}
let record
let store = _bb.store
let schema = {}
let modelDef = {}
let saved = false
let saving = false
let recordId
let isNew = true
let inputElements = {}
let errors = {}
$: if (model && model.length !== 0) {
fetchModel()
}
$: fields = Object.keys(schema)
$: fields = schema ? Object.keys(schema) : []
$: Object.values(inputElements).length && setForm(record)
const createBlankRecord = () => {
if (!schema) return
const newrecord = {
modelId: model,
}
for (let fieldName in schema) {
const field = schema[fieldName]
// defaulting to first one, as a blank value will fail validation
if (
field.type === "string" &&
field.constraints &&
field.constraints.inclusion &&
field.constraints.inclusion.length > 0
) {
newrecord[fieldName] = field.constraints.inclusion[0]
} else if (field.type === "number") newrecord[fieldName] = null
else if (field.type === "boolean") newrecord[fieldName] = false
else if (field.type === "link") newrecord[fieldName] = []
else newrecord[fieldName] = ""
}
return newrecord
}
$: errorMessages = Object.entries(errors).map(
([field, message]) => `${field} ${message}`
)
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
record = createBlankRecord()
record = {
modelId: model,
}
}
const save = debounce(async () => {
for (let field of fields) {
// Assign defaults to empty fields to prevent validation issues
if (!(field in record))
record[field] = DEFAULTS_FOR_TYPE[schema[field].type]
}
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, record)
@ -79,13 +69,11 @@
return state
})
errors = {}
// wipe form, if new record, otherwise update
// model to get new _rev
if (isNew) {
resetForm()
} else {
record = json
}
record = isNew ? { modelId: model } : json
// set saved, and unset after 1 second
// i.e. make the success notifier appear, then disappear again after time
@ -94,69 +82,27 @@
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 = createBlankRecord()
if (response.status === 400) {
errors = json.errors
}
})
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
record[field] = value
return
}
if (event.target.type === "number") {
value = parseInt(event.target.value)
record[field] = value
return
}
value = event.target.value
record[field] = value
}
onMount(() => {
onMount(async () => {
const routeParams = _bb.routeParams()
recordId =
Object.keys(routeParams).length > 0 && (routeParams.id || routeParams[0])
isNew = !recordId || recordId === "new"
if (isNew) {
record = createBlankRecord()
} 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)
})
record = { modelId: model }
return
}
const GET_RECORD_URL = `/api/${model}/records/${recordId}`
const response = await _bb.api.get(GET_RECORD_URL)
const json = await response.json()
record = json
})
</script>
@ -164,23 +110,28 @@
{#if title}
<h1>{title}</h1>
{/if}
{#each errorMessages as error}
<p class="error">{error}</p>
{/each}
<hr />
<div class="form-content">
{#each fields as field}
<div class="form-item">
<Label small forAttr={'form-stacked-text'}>{field}</Label>
{#if schema[field].type === 'string' && schema[field].constraints.inclusion}
<select on:blur={handleInput(field)} bind:this={inputElements[field]}>
<select bind:value={record[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)} />
{:else if schema[field].type === 'datetime'}
<DatePicker bind:value={record[field]} />
{:else if schema[field].type === 'boolean'}
<input class="input" type="checkbox" bind:checked={record[field]} />
{:else if schema[field].type === 'number'}
<input class="input" type="number" bind:value={record[field]} />
{:else if schema[field].type === 'string'}
<input class="input" type="text" bind:value={record[field]} />
{/if}
</div>
<hr />
@ -302,4 +253,9 @@
background-position: right 17px top 1.5em, right 10px top 1.5em;
background-size: 7px 7px, 7px 7px;
}
.error {
color: red;
font-weight: 500;
}
</style>

View File

@ -1,7 +1,8 @@
<script>
import { onMount } from "svelte"
import { fade } from "svelte/transition"
import { Label } from "@budibase/bbui"
import { Label, DatePicker } from "@budibase/bbui"
import debounce from "lodash.debounce"
export let _bb
export let model
@ -14,60 +15,50 @@
number: "number",
}
const DEFAULTS_FOR_TYPE = {
string: "",
boolean: false,
number: null,
link: [],
}
let record
let store = _bb.store
let schema = {}
let modelDef = {}
let saved = false
let saving = false
let recordId
let isNew = true
let inputElements = {}
let errors = {}
$: if (model && model.length !== 0) {
fetchModel()
}
$: fields = Object.keys(schema)
$: fields = schema ? Object.keys(schema) : []
$: Object.values(inputElements).length && setForm(record)
$: errorMessages = Object.entries(errors).map(
([field, message]) => `${field} ${message}`
)
const createBlankRecord = () => {
if (!schema) return
const newrecord = {
modelId: model,
}
for (let fieldName in schema) {
const field = schema[fieldName]
// defaulting to first one, as a blank value will fail validation
if (
field.type === "string" &&
field.constraints &&
field.constraints.inclusion &&
field.constraints.inclusion.length > 0
) {
newrecord[fieldName] = field.constraints.inclusion[0]
} else if (field.type === "number") newrecord[fieldName] = null
else if (field.type === "boolean") newrecord[fieldName] = false
else if (field.type === "link") newrecord[fieldName] = []
else newrecord[fieldName] = ""
}
return newrecord
}
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
record = createBlankRecord()
record = {
modelId: model,
}
}
const save = debounce(async () => {
for (let field of fields) {
// Assign defaults to empty fields to prevent validation issues
if (!(field in record))
record[field] = DEFAULTS_FOR_TYPE[schema[field].type]
}
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, record)
@ -79,13 +70,11 @@
return state
})
errors = {}
// wipe form, if new record, otherwise update
// model to get new _rev
if (isNew) {
resetForm()
} else {
record = json
}
record = isNew ? { modelId: model } : json
// set saved, and unset after 1 second
// i.e. make the success notifier appear, then disappear again after time
@ -94,69 +83,27 @@
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 = createBlankRecord()
if (response.status === 400) {
errors = json.errors
}
})
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
record[field] = value
return
}
if (event.target.type === "number") {
value = parseInt(event.target.value)
record[field] = value
return
}
value = event.target.value
record[field] = value
}
onMount(() => {
onMount(async () => {
const routeParams = _bb.routeParams()
recordId =
Object.keys(routeParams).length > 0 && (routeParams.id || routeParams[0])
isNew = !recordId || recordId === "new"
if (isNew) {
record = createBlankRecord()
} 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)
})
record = { modelId: model }
return
}
const GET_RECORD_URL = `/api/${model}/records/${recordId}`
const response = await _bb.api.get(GET_RECORD_URL)
const json = await response.json()
record = json
})
</script>
@ -164,23 +111,28 @@
{#if title}
<h1>{title}</h1>
{/if}
{#each errorMessages as error}
<p class="error">{error}</p>
{/each}
<hr />
<div class="form-content">
{#each fields as field}
<div class="form-item">
<Label small forAttr={'form-stacked-text'}>{field}</Label>
{#if schema[field].type === 'string' && schema[field].constraints.inclusion}
<select on:blur={handleInput(field)} bind:this={inputElements[field]}>
<select bind:value={record[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)} />
{:else if schema[field].type === 'datetime'}
<DatePicker bind:value={record[field]} />
{:else if schema[field].type === 'boolean'}
<input class="input" type="checkbox" bind:checked={record[field]} />
{:else if schema[field].type === 'number'}
<input class="input" type="number" bind:value={record[field]} />
{:else if schema[field].type === 'string'}
<input class="input" type="text" bind:value={record[field]} />
{/if}
</div>
<hr />
@ -293,4 +245,9 @@
background-position: right 17px top 1.5em, right 10px top 1.5em;
background-size: 7px 7px, 7px 7px;
}
.error {
color: red;
font-weight: 500;
}
</style>