Add attachment field to forms
This commit is contained in:
parent
acc8b77611
commit
a4de9668ed
|
@ -14,7 +14,8 @@
|
||||||
"optionsfield",
|
"optionsfield",
|
||||||
"booleanfield",
|
"booleanfield",
|
||||||
"longformfield",
|
"longformfield",
|
||||||
"datetimefield"
|
"datetimefield",
|
||||||
|
"attachmentfield"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormFieldSelect {...$$props} type="attachment" />
|
|
@ -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 => {
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
@ -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,7 +115,8 @@
|
||||||
onMount(fetchSchema)
|
onMount(fetchSchema)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<DataProvider row={{ ...$formState.values, tableId: datasource?.tableId }}>
|
||||||
|
<div
|
||||||
lang="en"
|
lang="en"
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
use:styleable={$component.styles}
|
use:styleable={$component.styles}
|
||||||
|
@ -123,4 +124,5 @@
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
<slot />
|
<slot />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
</DataProvider>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
fieldState = formField?.fieldState
|
||||||
fieldApi = formField?.fieldApi
|
fieldApi = formField?.fieldApi
|
||||||
fieldSchema = formField?.fieldSchema
|
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>
|
|
||||||
{:else if !formContext}
|
|
||||||
<Placeholder>Form components need to be wrapped in a Form</Placeholder>
|
|
||||||
{:else}
|
|
||||||
<FieldGroupFallback>
|
|
||||||
<div class="spectrum-Form-item" use:styleable={$component.styles}>
|
<div class="spectrum-Form-item" use:styleable={$component.styles}>
|
||||||
<label
|
<label
|
||||||
for={$fieldState.fieldId}
|
for={$fieldState?.fieldId}
|
||||||
class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${labelPositionClass}`}>
|
class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${labelPositionClass}`}>
|
||||||
{label || ''}
|
{label || ''}
|
||||||
</label>
|
</label>
|
||||||
<div class="spectrum-Form-itemField">
|
<div class="spectrum-Form-itemField">
|
||||||
|
{#if !formContext}
|
||||||
|
<Placeholder>Form components need to be wrapped in a Form</Placeholder>
|
||||||
|
{:else if !fieldState}
|
||||||
|
<Placeholder>
|
||||||
|
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}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FieldGroupFallback>
|
</FieldGroupFallback>
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.error {
|
.error {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue