Add linked records to dataforms and use BBUI components and pretty errors

This commit is contained in:
Andrew Kingston 2020-10-06 16:46:23 +01:00
parent 7207bc275e
commit e15e86b9e1
4 changed files with 160 additions and 139 deletions

View File

@ -1,9 +1,19 @@
<script> <script>
import { onMount } from "svelte" import { onMount } from "svelte"
import { fade } from "svelte/transition" import { fade } from "svelte/transition"
import { Label, DatePicker } from "@budibase/bbui" import {
Label,
DatePicker,
Input,
Select,
Button,
Toggle,
} from "@budibase/bbui"
import Dropzone from "./attachments/Dropzone.svelte" import Dropzone from "./attachments/Dropzone.svelte"
import LinkedRecordSelector from "./LinkedRecordSelector.svelte"
import debounce from "lodash.debounce" import debounce from "lodash.debounce"
import ErrorsBox from "./ErrorsBox.svelte"
import { capitalise } from "./helpers"
export let _bb export let _bb
export let model export let model
@ -32,16 +42,11 @@
let isNew = true let isNew = true
let errors = {} let errors = {}
$: fields = schema ? Object.keys(schema) : []
$: if (model && model.length !== 0) { $: if (model && model.length !== 0) {
fetchModel() fetchModel()
} }
$: fields = schema ? Object.keys(schema) : []
$: errorMessages = Object.entries(errors).map(
([field, message]) => `${field} ${message}`
)
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)
@ -82,11 +87,13 @@
saved = true saved = true
setTimeout(() => { setTimeout(() => {
saved = false saved = false
}, 1000) }, 3000)
} }
if (response.status === 400) { if (response.status === 400) {
errors = json.errors errors = Object.keys(json.errors)
.map(k => ({ dataPath: k, message: json.errors[k] }))
.flat()
} }
}) })
@ -103,8 +110,7 @@
const GET_RECORD_URL = `/api/${model}/records/${recordId}` const GET_RECORD_URL = `/api/${model}/records/${recordId}`
const response = await _bb.api.get(GET_RECORD_URL) const response = await _bb.api.get(GET_RECORD_URL)
const json = await response.json() record = await response.json()
record = json
}) })
</script> </script>
@ -112,44 +118,52 @@
{#if title} {#if title}
<h1>{title}</h1> <h1>{title}</h1>
{/if} {/if}
{#each errorMessages as error}
<p class="error">{error}</p>
{/each}
<hr />
<div class="form-content"> <div class="form-content">
<ErrorsBox {errors} />
{#each fields as field} {#each fields as field}
<div class="form-item"> {#if schema[field].type === 'options'}
<Label small forAttr={'form-stacked-text'}>{field}</Label> <Select
{#if schema[field].type === 'string' && schema[field].constraints.inclusion} secondary
<select bind:value={record[field]}> label={capitalise(schema[field].name)}
{#each schema[field].constraints.inclusion as opt} bind:value={record[field]}>
<option>{opt}</option> <option value="">Choose an option</option>
{/each} {#each schema[field].constraints.inclusion as opt}
</select> <option>{opt}</option>
{:else if schema[field].type === 'datetime'} {/each}
<DatePicker bind:value={record[field]} /> </Select>
{:else if schema[field].type === 'boolean'} {:else if schema[field].type === 'datetime'}
<input class="input" type="checkbox" bind:checked={record[field]} /> <DatePicker
{:else if schema[field].type === 'number'} label={capitalise(schema[field].name)}
<input class="input" type="number" bind:value={record[field]} /> bind:value={record[field]} />
{:else if schema[field].type === 'string'} {:else if schema[field].type === 'boolean'}
<input class="input" type="text" bind:value={record[field]} /> <Toggle
{:else if schema[field].type === 'attachment'} text={capitalise(schema[field].name)}
bind:checked={record[field]} />
{:else if schema[field].type === 'number'}
<Input
label={capitalise(schema[field].name)}
type="number"
bind:value={record[field]} />
{:else if schema[field].type === 'string'}
<Input
label={capitalise(schema[field].name)}
bind:value={record[field]} />
{:else if schema[field].type === 'attachment'}
<div>
<Label extraSmall grey>{schema[field].name}</Label>
<Dropzone bind:files={record[field]} /> <Dropzone bind:files={record[field]} />
{/if} </div>
</div> {:else if schema[field].type === 'link'}
<hr /> <LinkedRecordSelector
secondary
bind:linkedRecords={record[field]}
schema={schema[field]} />
{/if}
{/each} {/each}
<div class="button-block"> <div class="buttons">
<button on:click={save} class:saved> <Button primary on:click={save} green={saved}>
{#if saved} {#if saved}Success{:else}{buttonText || 'Submit Form'}{/if}
<div in:fade> </Button>
<span class:saved>Success</span>
</div>
{:else}
<div>{buttonText || 'Submit Form'}</div>
{/if}
</button>
</div> </div>
</div> </div>
</form> </form>
@ -162,104 +176,14 @@
} }
.form-content { .form-content {
margin-bottom: 20px; margin-bottom: var(--spacing-xl);
display: grid;
gap: var(--spacing-xl);
width: 100%; width: 100%;
} }
.input { .buttons {
border-radius: 5px;
border: 1px solid #e6e6e6;
padding: 1rem;
font-size: 16px;
}
.form-item {
display: flex;
flex-direction: column;
margin-bottom: 16px;
}
hr {
border: 1px solid var(--grey-1);
margin: 20px 0px;
}
hr:nth-last-child(2) {
border: 1px solid #fff;
margin: 20px 0px;
}
.button-block {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
} }
button {
font-size: 16px;
padding: 0.4em;
box-sizing: border-box;
border-radius: 4px;
color: white;
background-color: #393c44;
outline: none;
width: 300px;
height: 40px;
cursor: pointer;
transition: all 0.2s ease 0s;
overflow: hidden;
outline: none;
user-select: none;
white-space: nowrap;
text-align: center;
}
button.saved {
background-color: #84c991;
border: none;
}
button:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
input[type="checkbox"] {
width: 32px;
height: 32px;
padding: 0;
margin: 0;
vertical-align: bottom;
position: relative;
top: -1px;
*overflow: hidden;
}
select::-ms-expand {
display: none;
}
select {
display: inline-block;
cursor: pointer;
align-items: baseline;
box-sizing: border-box;
padding: 1em 1em;
border: 1px solid #eaeaea;
border-radius: 5px;
font: inherit;
line-height: inherit;
-webkit-appearance: none;
-moz-appearance: none;
-ms-appearance: none;
appearance: none;
background-repeat: no-repeat;
background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
linear-gradient(135deg, currentColor 50%, transparent 50%);
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> </style>

View File

@ -0,0 +1,31 @@
<script>
export let errors = []
$: hasErrors = errors.length > 0
</script>
{#if hasErrors}
<div class="container">
{#each errors as error}
<div class="error">{error.dataPath} {error.message}</div>
{/each}
</div>
{/if}
<style>
.container {
border-radius: var(--border-radius-m);
margin: 0;
padding: var(--spacing-m);
background-color: var(--red-light);
}
.error {
font-size: var(--font-size-xs);
font-weight: 500;
color: var(--red-dark);
}
.error:first-letter {
text-transform: uppercase;
}
</style>

View File

@ -0,0 +1,65 @@
<script>
import { onMount } from "svelte"
import { Select, Label, Multiselect } from "@budibase/bbui"
import api from "./api"
import { capitalise } from "./helpers"
export let schema = {}
export let linkedRecords = []
export let showLabel = true
export let secondary
let linkedModel
$: label = capitalise(schema.name)
$: linkedModelId = schema.modelId
$: recordsPromise = fetchRecords(linkedModelId)
$: fetchModel(linkedModelId)
async function fetchModel() {
if (linkedModelId == null) {
return
}
const FETCH_MODEL_URL = `/api/models/${linkedModelId}`
const response = await api.get(FETCH_MODEL_URL)
linkedModel = await response.json()
}
async function fetchRecords(linkedModelId) {
if (linkedModelId == null) {
return
}
const FETCH_RECORDS_URL = `/api/${linkedModelId}/records`
const response = await api.get(FETCH_RECORDS_URL)
return await response.json()
}
function getPrettyName(record) {
return record[linkedModel?.primaryDisplay || "_id"]
}
</script>
{#if linkedModel != null}
{#if linkedModel.primaryDisplay == null}
{#if showLabel}
<Label extraSmall grey>{label}</Label>
{/if}
<Label small black>
Please choose a primary display column for the
<b>{linkedModel.name}</b>
table.
</Label>
{:else}
{#await recordsPromise then records}
<Multiselect
{secondary}
bind:value={linkedRecords}
label={showLabel ? label : null}
placeholder="Choose some options">
{#each records as record}
<option value={record._id}>{getPrettyName(record)}</option>
{/each}
</Multiselect>
{/await}
{/if}
{/if}

View File

@ -0,0 +1 @@
export const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)