mirror of https://github.com/Budibase/budibase.git
3 changed files with 201 additions and 425 deletions
@ -1,189 +1,10 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
import { fade } from "svelte/transition" |
|||
import { |
|||
Label, |
|||
DatePicker, |
|||
Input, |
|||
Select, |
|||
Button, |
|||
Toggle, |
|||
} from "@budibase/bbui" |
|||
import Dropzone from "./attachments/Dropzone.svelte" |
|||
import LinkedRecordSelector from "./LinkedRecordSelector.svelte" |
|||
import debounce from "lodash.debounce" |
|||
import ErrorsBox from "./ErrorsBox.svelte" |
|||
import { capitalise } from "./helpers" |
|||
import Form from "./Form.svelte" |
|||
|
|||
export let _bb |
|||
export let model |
|||
export let title |
|||
export let buttonText |
|||
|
|||
const TYPE_MAP = { |
|||
string: "text", |
|||
boolean: "checkbox", |
|||
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 recordId |
|||
let isNew = true |
|||
let errors = {} |
|||
|
|||
$: fields = schema ? Object.keys(schema) : [] |
|||
$: if (model && model.length !== 0) { |
|||
fetchModel() |
|||
} |
|||
|
|||
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 = { |
|||
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] |
|||
} |
|||
} |
|||
|
|||
const SAVE_RECORD_URL = `/api/${model}/records` |
|||
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 |
|||
}) |
|||
|
|||
errors = {} |
|||
|
|||
// wipe form, if new record, otherwise update |
|||
// model to get new _rev |
|||
record = isNew ? { modelId: model } : 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 |
|||
}, 3000) |
|||
} |
|||
|
|||
if (response.status === 400) { |
|||
errors = Object.keys(json.errors) |
|||
.map(k => ({ dataPath: k, message: json.errors[k] })) |
|||
.flat() |
|||
} |
|||
}) |
|||
|
|||
onMount(async () => { |
|||
const routeParams = _bb.routeParams() |
|||
recordId = |
|||
Object.keys(routeParams).length > 0 && (routeParams.id || routeParams[0]) |
|||
isNew = !recordId || recordId === "new" |
|||
|
|||
if (isNew) { |
|||
record = { modelId: model } |
|||
return |
|||
} |
|||
|
|||
const GET_RECORD_URL = `/api/${model}/records/${recordId}` |
|||
const response = await _bb.api.get(GET_RECORD_URL) |
|||
record = await response.json() |
|||
}) |
|||
</script> |
|||
|
|||
<form class="form" on:submit|preventDefault> |
|||
{#if title} |
|||
<h1>{title}</h1> |
|||
{/if} |
|||
<div class="form-content"> |
|||
<ErrorsBox {errors} /> |
|||
{#each fields as field} |
|||
{#if schema[field].type === 'options'} |
|||
<Select |
|||
secondary |
|||
label={capitalise(schema[field].name)} |
|||
bind:value={record[field]}> |
|||
<option value="">Choose an option</option> |
|||
{#each schema[field].constraints.inclusion as opt} |
|||
<option>{opt}</option> |
|||
{/each} |
|||
</Select> |
|||
{:else if schema[field].type === 'datetime'} |
|||
<DatePicker |
|||
label={capitalise(schema[field].name)} |
|||
bind:value={record[field]} /> |
|||
{:else if schema[field].type === 'boolean'} |
|||
<Toggle |
|||
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]} /> |
|||
</div> |
|||
{:else if schema[field].type === 'link'} |
|||
<LinkedRecordSelector |
|||
secondary |
|||
bind:linkedRecords={record[field]} |
|||
schema={schema[field]} /> |
|||
{/if} |
|||
{/each} |
|||
<div class="buttons"> |
|||
<Button primary on:click={save} green={saved}> |
|||
{#if saved}Success{:else}{buttonText || 'Submit Form'}{/if} |
|||
</Button> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
|
|||
<style> |
|||
.form { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
} |
|||
|
|||
.form-content { |
|||
margin-bottom: var(--spacing-xl); |
|||
display: grid; |
|||
gap: var(--spacing-xl); |
|||
width: 100%; |
|||
} |
|||
|
|||
.buttons { |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
} |
|||
</style> |
|||
<Form {_bb} {model} {title} {buttonText} wide={false} /> |
|||
|
|||
@ -1,252 +1,10 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
import { fade } from "svelte/transition" |
|||
import { Label, DatePicker } from "@budibase/bbui" |
|||
import debounce from "lodash.debounce" |
|||
import Form from "./Form.svelte" |
|||
|
|||
export let _bb |
|||
export let model |
|||
export let title |
|||
export let buttonText |
|||
|
|||
const TYPE_MAP = { |
|||
string: "text", |
|||
boolean: "checkbox", |
|||
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 recordId |
|||
let isNew = true |
|||
let errors = {} |
|||
|
|||
$: if (model && model.length !== 0) { |
|||
fetchModel() |
|||
} |
|||
|
|||
$: fields = schema ? Object.keys(schema) : [] |
|||
|
|||
$: 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 = { |
|||
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] |
|||
} |
|||
|
|||
const SAVE_RECORD_URL = `/api/${model}/records` |
|||
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 |
|||
}) |
|||
|
|||
errors = {} |
|||
|
|||
// wipe form, if new record, otherwise update |
|||
// model to get new _rev |
|||
record = isNew ? { modelId: model } : 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) |
|||
} |
|||
|
|||
if (response.status === 400) { |
|||
errors = json.errors |
|||
} |
|||
}) |
|||
|
|||
onMount(async () => { |
|||
const routeParams = _bb.routeParams() |
|||
recordId = |
|||
Object.keys(routeParams).length > 0 && (routeParams.id || routeParams[0]) |
|||
isNew = !recordId || recordId === "new" |
|||
|
|||
if (isNew) { |
|||
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> |
|||
|
|||
<form class="form" on:submit|preventDefault> |
|||
{#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 bind:value={record[field]}> |
|||
{#each schema[field].constraints.inclusion as opt} |
|||
<option>{opt}</option> |
|||
{/each} |
|||
</select> |
|||
{: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 /> |
|||
{/each} |
|||
<div class="button-block"> |
|||
<button on:click={save} class:saved> |
|||
{#if saved} |
|||
<div in:fade> |
|||
<span class:saved>Success</span> |
|||
</div> |
|||
{:else} |
|||
<div>{buttonText || 'Submit Form'}</div> |
|||
{/if} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
|
|||
<style> |
|||
.form { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
margin: auto; |
|||
padding: 40px; |
|||
} |
|||
.form-content { |
|||
margin-bottom: 20px; |
|||
} |
|||
.input { |
|||
border-radius: 5px; |
|||
border: 1px solid #e6e6e6; |
|||
padding: 1em; |
|||
font-size: 16px; |
|||
} |
|||
|
|||
.form-item { |
|||
display: grid; |
|||
grid-template-columns: 30% 1fr; |
|||
margin-bottom: 16px; |
|||
align-items: center; |
|||
gap: 1em; |
|||
} |
|||
|
|||
hr { |
|||
border: 1px solid #f5f5f5; |
|||
margin: 40px 0px; |
|||
} |
|||
hr:nth-last-child(2) { |
|||
border: 1px solid #f5f5f5; |
|||
margin: 40px 0px; |
|||
} |
|||
.button-block { |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
} |
|||
button { |
|||
font-size: 16px; |
|||
padding: 8px 16px; |
|||
box-sizing: border-box; |
|||
border-radius: 4px; |
|||
color: white; |
|||
background-color: black; |
|||
outline: none; |
|||
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"] { |
|||
transform: scale(2); |
|||
cursor: pointer; |
|||
} |
|||
|
|||
select::-ms-expand { |
|||
display: none; |
|||
} |
|||
select { |
|||
cursor: pointer; |
|||
display: inline-block; |
|||
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> |
|||
<Form {_bb} {model} {title} {buttonText} wide={true} /> |
|||
|
|||
@ -0,0 +1,197 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
import { fade } from "svelte/transition" |
|||
import { |
|||
Label, |
|||
DatePicker, |
|||
Input, |
|||
Select, |
|||
Button, |
|||
Toggle, |
|||
} from "@budibase/bbui" |
|||
import Dropzone from "./attachments/Dropzone.svelte" |
|||
import LinkedRecordSelector from "./LinkedRecordSelector.svelte" |
|||
import debounce from "lodash.debounce" |
|||
import ErrorsBox from "./ErrorsBox.svelte" |
|||
import { capitalise } from "./helpers" |
|||
|
|||
export let _bb |
|||
export let model |
|||
export let title |
|||
export let buttonText |
|||
export let wide = false |
|||
|
|||
const TYPE_MAP = { |
|||
string: "text", |
|||
boolean: "checkbox", |
|||
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 recordId |
|||
let isNew = true |
|||
let errors = {} |
|||
|
|||
$: fields = schema ? Object.keys(schema) : [] |
|||
$: if (model && model.length !== 0) { |
|||
fetchModel() |
|||
} |
|||
|
|||
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 = { |
|||
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] |
|||
} |
|||
} |
|||
|
|||
const SAVE_RECORD_URL = `/api/${model}/records` |
|||
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 |
|||
}) |
|||
|
|||
errors = {} |
|||
|
|||
// wipe form, if new record, otherwise update |
|||
// model to get new _rev |
|||
record = isNew ? { modelId: model } : 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 |
|||
}, 3000) |
|||
} |
|||
|
|||
if (response.status === 400) { |
|||
errors = Object.keys(json.errors) |
|||
.map(k => ({ dataPath: k, message: json.errors[k] })) |
|||
.flat() |
|||
} |
|||
}) |
|||
|
|||
onMount(async () => { |
|||
const routeParams = _bb.routeParams() |
|||
recordId = |
|||
Object.keys(routeParams).length > 0 && (routeParams.id || routeParams[0]) |
|||
isNew = !recordId || recordId === "new" |
|||
|
|||
if (isNew) { |
|||
record = { modelId: model } |
|||
return |
|||
} |
|||
|
|||
const GET_RECORD_URL = `/api/${model}/records/${recordId}` |
|||
const response = await _bb.api.get(GET_RECORD_URL) |
|||
record = await response.json() |
|||
}) |
|||
</script> |
|||
|
|||
<form class="form" on:submit|preventDefault> |
|||
{#if title} |
|||
<h1>{title}</h1> |
|||
{/if} |
|||
<div class="form-content"> |
|||
<ErrorsBox {errors} /> |
|||
{#each fields as field} |
|||
<div class="form-field" class:wide> |
|||
{#if !(schema[field].type === 'boolean' && !wide)} |
|||
<Label extraSmall={!wide} grey={!wide}> |
|||
{capitalise(schema[field].name)} |
|||
</Label> |
|||
{/if} |
|||
{#if schema[field].type === 'options'} |
|||
<Select secondary bind:value={record[field]}> |
|||
<option value="">Choose an option</option> |
|||
{#each schema[field].constraints.inclusion as opt} |
|||
<option>{opt}</option> |
|||
{/each} |
|||
</Select> |
|||
{:else if schema[field].type === 'datetime'} |
|||
<DatePicker bind:value={record[field]} /> |
|||
{:else if schema[field].type === 'boolean'} |
|||
<Toggle |
|||
text={wide ? null : capitalise(schema[field].name)} |
|||
bind:checked={record[field]} /> |
|||
{:else if schema[field].type === 'number'} |
|||
<Input type="number" bind:value={record[field]} /> |
|||
{:else if schema[field].type === 'string'} |
|||
<Input bind:value={record[field]} /> |
|||
{:else if schema[field].type === 'attachment'} |
|||
<Dropzone bind:files={record[field]} /> |
|||
{:else if schema[field].type === 'link'} |
|||
<LinkedRecordSelector |
|||
secondary |
|||
showLabel={false} |
|||
bind:linkedRecords={record[field]} |
|||
schema={schema[field]} /> |
|||
{/if} |
|||
</div> |
|||
{/each} |
|||
<div class="buttons"> |
|||
<Button primary on:click={save} green={saved}> |
|||
{#if saved}Success{:else}{buttonText || 'Submit Form'}{/if} |
|||
</Button> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
|
|||
<style> |
|||
.form { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
} |
|||
|
|||
.form-content { |
|||
margin-bottom: var(--spacing-xl); |
|||
display: grid; |
|||
gap: var(--spacing-xl); |
|||
width: 100%; |
|||
} |
|||
|
|||
.form-field { |
|||
display: grid; |
|||
} |
|||
.form-field.wide { |
|||
align-items: center; |
|||
grid-template-columns: 30% 1fr; |
|||
gap: var(--spacing-xl); |
|||
} |
|||
.form-field.wide :global(label) { |
|||
margin-bottom: 0; |
|||
} |
|||
|
|||
.buttons { |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue