Add attachment field to forms

This commit is contained in:
Andrew Kingston 2021-01-29 13:22:38 +00:00
parent acc8b77611
commit a4de9668ed
11 changed files with 160 additions and 71 deletions

View File

@ -14,7 +14,8 @@
"optionsfield", "optionsfield",
"booleanfield", "booleanfield",
"longformfield", "longformfield",
"datetimefield" "datetimefield",
"attachmentfield"
] ]
}, },
{ {

View File

@ -0,0 +1,5 @@
<script>
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} type="attachment" />

View File

@ -23,6 +23,7 @@
import BooleanFieldSelect from "./PropertyControls/BooleanFieldSelect.svelte" import BooleanFieldSelect from "./PropertyControls/BooleanFieldSelect.svelte"
import LongFormFieldSelect from "./PropertyControls/LongFormFieldSelect.svelte" import LongFormFieldSelect from "./PropertyControls/LongFormFieldSelect.svelte"
import DateTimeFieldSelect from "./PropertyControls/DateTimeFieldSelect.svelte" import DateTimeFieldSelect from "./PropertyControls/DateTimeFieldSelect.svelte"
import AttachmentFieldSelect from "./PropertyControls/AttachmentFieldSelect.svelte"
export let componentDefinition = {} export let componentDefinition = {}
export let componentInstance = {} export let componentInstance = {}
@ -70,6 +71,7 @@
"field/boolean": BooleanFieldSelect, "field/boolean": BooleanFieldSelect,
"field/longform": LongFormFieldSelect, "field/longform": LongFormFieldSelect,
"field/datetime": DateTimeFieldSelect, "field/datetime": DateTimeFieldSelect,
"field/attachment": AttachmentFieldSelect,
} }
const getControl = type => { const getControl = type => {

View File

@ -20,9 +20,11 @@ const makeApiCall = async ({ method, url, body, json = true }) => {
const requestBody = json ? JSON.stringify(body) : body const requestBody = json ? JSON.stringify(body) : body
let headers = { let headers = {
Accept: "application/json", Accept: "application/json",
"Content-Type": "application/json",
"x-budibase-app-id": window["##BUDIBASE_APP_ID##"], "x-budibase-app-id": window["##BUDIBASE_APP_ID##"],
} }
if (json) {
headers["Content-Type"] = "application/json"
}
if (!window["##BUDIBASE_IN_BUILDER##"]) { if (!window["##BUDIBASE_IN_BUILDER##"]) {
headers["x-budibase-type"] = "client" headers["x-budibase-type"] = "client"
} }

View File

@ -9,13 +9,18 @@
export let definition = {} export let definition = {}
let enrichedProps // Props that will be passed to the component instance
let componentProps let componentProps
// Props are hashed when inside the builder preview and used as a key, so that // Props are hashed when inside the builder preview and used as a key, so that
// components fully remount whenever any props change // components fully remount whenever any props change
let propsHash = 0 let propsHash = 0
// Latest timestamp that we started a props update.
// Due to enrichment now being async, we need to avoid overwriting newer
// props with old ones, depending on how long enrichment takes.
let latestUpdateTime
// Get contexts // Get contexts
const dataContext = getContext("data") const dataContext = getContext("data")
@ -27,8 +32,7 @@
$: constructor = getComponentConstructor(definition._component) $: constructor = getComponentConstructor(definition._component)
$: children = definition._children || [] $: children = definition._children || []
$: id = definition._id $: id = definition._id
$: enrichComponentProps(definition, $dataContext, $bindingStore) $: updateComponentProps(definition, $dataContext, $bindingStore)
$: updateProps(enrichedProps)
$: styles = definition._styles $: styles = definition._styles
// Update component context // Update component context
@ -38,29 +42,6 @@
styles: { ...styles, id }, styles: { ...styles, id },
}) })
// Updates the component props.
// Most props are deeply compared so that svelte will only trigger reactive
// statements on props that have actually changed.
const updateProps = props => {
if (!props) {
return
}
let propsChanged = false
if (!componentProps) {
componentProps = {}
propsChanged = true
}
Object.keys(props).forEach(key => {
if (!propsAreSame(props[key], componentProps[key])) {
propsChanged = true
componentProps[key] = props[key]
}
})
if (get(builderStore).inBuilder && propsChanged) {
propsHash = hashString(JSON.stringify(componentProps))
}
}
// Gets the component constructor for the specified component // Gets the component constructor for the specified component
const getComponentConstructor = component => { const getComponentConstructor = component => {
const split = component?.split("/") const split = component?.split("/")
@ -72,8 +53,42 @@
} }
// Enriches any string component props using handlebars // Enriches any string component props using handlebars
const enrichComponentProps = async (definition, context, bindingStore) => { const updateComponentProps = async (definition, context, bindingStore) => {
enrichedProps = await enrichProps(definition, context, bindingStore) // Record the timestamp so we can reference it after enrichment
latestUpdateTime = Date.now()
const enrichmentTime = latestUpdateTime
// Enrich props with context
const enrichedProps = await enrichProps(definition, context, bindingStore)
// Abandon this update if a newer update has started
if (enrichmentTime !== latestUpdateTime) {
return
}
// Update the component props.
// Most props are deeply compared so that svelte will only trigger reactive
// statements on props that have actually changed.
if (!enrichedProps) {
return
}
let propsChanged = false
if (!componentProps) {
componentProps = {}
propsChanged = true
}
Object.keys(enrichedProps).forEach(key => {
if (!propsAreSame(enrichedProps[key], componentProps[key])) {
propsChanged = true
componentProps[key] = enrichedProps[key]
}
})
// Update the hash if we're in the builder so we can fully remount this
// component
if (get(builderStore).inBuilder && propsChanged) {
propsHash = hashString(JSON.stringify(componentProps))
}
} }
</script> </script>

View File

@ -1271,5 +1271,22 @@
"defaultValue": true "defaultValue": true
} }
] ]
},
"attachmentfield": {
"name": "Attachment",
"icon": "ri-calendar-line",
"styleable": true,
"settings": [
{
"type": "field/attachment",
"label": "Field",
"key": "field"
},
{
"type": "text",
"label": "Label",
"key": "label"
}
]
} }
} }

View File

@ -0,0 +1,27 @@
<script>
import SpectrumField from "./SpectrumField.svelte"
import Dropzone from "../attachments/Dropzone.svelte"
export let field
export let label
let previousFiles = []
let files = []
$: {
// Only actually update the value when it changes, so that we don't trigger
// validation unnecessarily
if (files !== previousFiles) {
fieldApi?.setValue(files)
previousFiles = files
}
}
let fieldState
let fieldApi
</script>
<SpectrumField {label} {field} bind:fieldState bind:fieldApi>
{#if fieldState}
<Dropzone bind:files />
{/if}
</SpectrumField>

View File

@ -8,7 +8,7 @@
export let theme export let theme
export let size export let size
const { styleable, API, setBindableValue } = getContext("sdk") const { styleable, API, setBindableValue, DataProvider } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
let loaded = false let loaded = false
@ -115,12 +115,14 @@
onMount(fetchSchema) onMount(fetchSchema)
</script> </script>
<div <DataProvider row={{ ...$formState.values, tableId: datasource?.tableId }}>
lang="en" <div
dir="ltr" lang="en"
use:styleable={$component.styles} dir="ltr"
class={`spectrum ${size || 'spectrum--medium'} ${theme || 'spectrum--light'}`}> use:styleable={$component.styles}
{#if loaded} class={`spectrum ${size || 'spectrum--medium'} ${theme || 'spectrum--light'}`}>
<slot /> {#if loaded}
{/if} <slot />
</div> {/if}
</div>
</DataProvider>

View File

@ -1,17 +1,30 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
const { builderStore } = getContext("sdk")
const { styleable, builderStore } = getContext("sdk")
const component = getContext("component")
$: styles = {
id: $component.styles.id,
allowSelection: $component.styles.allowSelection,
}
</script> </script>
{#if $builderStore.inBuilder} {#if $builderStore.inBuilder}
<div use:styleable={styles}> <div>
<slot /> <slot />
</div> </div>
{/if} {/if}
<style>
div {
height: var(
--spectrum-alias-item-height-m,
var(--spectrum-global-dimension-size-400)
);
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
color: var(--spectrum-global-color-gray-600);
font-size: var(
--spectrum-alias-item-text-size-m,
var(--spectrum-global-dimension-font-size-100)
);
font-style: var(--spectrum-global-font-style-italic, italic);
font-weight: var(--spectrum-global-font-weight-regular, 400);
}
</style>

View File

@ -16,41 +16,45 @@
const component = getContext("component") const component = getContext("component")
// Register field with form // Register field with form
const { formApi } = formContext || {} $: formApi = formContext?.formApi
const labelPosition = fieldGroupContext?.labelPosition || "above" $: labelPosition = fieldGroupContext?.labelPosition || "above"
const formField = formApi?.registerField(field) $: formField = formApi?.registerField(field)
// Expose field properties to parent component // Expose field properties to parent component
fieldState = formField?.fieldState $: {
fieldApi = formField?.fieldApi fieldState = formField?.fieldState
fieldSchema = formField?.fieldSchema fieldApi = formField?.fieldApi
fieldSchema = formField?.fieldSchema
}
// Extract label position from field group context // Extract label position from field group context
$: labelPositionClass = $: labelPositionClass =
labelPosition === "above" ? "" : `spectrum-FieldLabel--${labelPosition}` labelPosition === "above" ? "" : `spectrum-FieldLabel--${labelPosition}`
</script> </script>
{#if !fieldState} <FieldGroupFallback>
<Placeholder>Add the Field setting to start using your component</Placeholder> <div class="spectrum-Form-item" use:styleable={$component.styles}>
{:else if !formContext} <label
<Placeholder>Form components need to be wrapped in a Form</Placeholder> for={$fieldState?.fieldId}
{:else} class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${labelPositionClass}`}>
<FieldGroupFallback> {label || ''}
<div class="spectrum-Form-item" use:styleable={$component.styles}> </label>
<label <div class="spectrum-Form-itemField">
for={$fieldState.fieldId} {#if !formContext}
class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${labelPositionClass}`}> <Placeholder>Form components need to be wrapped in a Form</Placeholder>
{label || ''} {:else if !fieldState}
</label> <Placeholder>
<div class="spectrum-Form-itemField"> Add the Field setting to start using your component
</Placeholder>
{:else}
<slot /> <slot />
{#if $fieldState.error} {#if $fieldState.error}
<div class="error">{$fieldState.error}</div> <div class="error">{$fieldState.error}</div>
{/if} {/if}
</div> {/if}
</div> </div>
</FieldGroupFallback> </div>
{/if} </FieldGroupFallback>
<style> <style>
.error { .error {

View File

@ -6,3 +6,4 @@ export { default as optionsfield } from "./OptionsField.svelte"
export { default as booleanfield } from "./BooleanField.svelte" export { default as booleanfield } from "./BooleanField.svelte"
export { default as longformfield } from "./LongFormField.svelte" export { default as longformfield } from "./LongFormField.svelte"
export { default as datetimefield } from "./DateTimeField.svelte" export { default as datetimefield } from "./DateTimeField.svelte"
export { default as attachmentfield } from "./AttachmentField.svelte"