Browse Source

Add attachment field to forms

pull/1105/head
Andrew Kingston 6 years ago
parent
commit
deccd11def
  1. 3
      packages/builder/src/components/design/AppPreview/componentStructure.json
  2. 5
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/AttachmentFieldSelect.svelte
  3. 2
      packages/builder/src/components/design/PropertiesPanel/SettingsView.svelte
  4. 4
      packages/client/src/api/api.js
  5. 67
      packages/client/src/components/Component.svelte
  6. 17
      packages/standard-components/manifest.json
  7. 27
      packages/standard-components/src/forms/AttachmentField.svelte
  8. 22
      packages/standard-components/src/forms/Form.svelte
  9. 31
      packages/standard-components/src/forms/Placeholder.svelte
  10. 48
      packages/standard-components/src/forms/SpectrumField.svelte
  11. 1
      packages/standard-components/src/forms/index.js

3
packages/builder/src/components/design/AppPreview/componentStructure.json

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

5
packages/builder/src/components/design/PropertiesPanel/PropertyControls/AttachmentFieldSelect.svelte

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

2
packages/builder/src/components/design/PropertiesPanel/SettingsView.svelte

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

4
packages/client/src/api/api.js

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

67
packages/client/src/components/Component.svelte

@ -9,13 +9,18 @@
export let definition = {}
let enrichedProps
// Props that will be passed to the component instance
let componentProps
// Props are hashed when inside the builder preview and used as a key, so that
// components fully remount whenever any props change
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
const dataContext = getContext("data")
@ -27,8 +32,7 @@
$: constructor = getComponentConstructor(definition._component)
$: children = definition._children || []
$: id = definition._id
$: enrichComponentProps(definition, $dataContext, $bindingStore)
$: updateProps(enrichedProps)
$: updateComponentProps(definition, $dataContext, $bindingStore)
$: styles = definition._styles
// Update component context
@ -38,11 +42,34 @@
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) {
// Gets the component constructor for the specified component
const getComponentConstructor = component => {
const split = component?.split("/")
const name = split?.[split.length - 1]
if (name === "screenslot" && $builderStore.previewType !== "layout") {
return Router
}
return ComponentLibrary[name]
}
// Enriches any string component props using handlebars
const updateComponentProps = async (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
@ -50,31 +77,19 @@
componentProps = {}
propsChanged = true
}
Object.keys(props).forEach(key => {
if (!propsAreSame(props[key], componentProps[key])) {
Object.keys(enrichedProps).forEach(key => {
if (!propsAreSame(enrichedProps[key], componentProps[key])) {
propsChanged = true
componentProps[key] = props[key]
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))
}
}
// Gets the component constructor for the specified component
const getComponentConstructor = component => {
const split = component?.split("/")
const name = split?.[split.length - 1]
if (name === "screenslot" && $builderStore.previewType !== "layout") {
return Router
}
return ComponentLibrary[name]
}
// Enriches any string component props using handlebars
const enrichComponentProps = async (definition, context, bindingStore) => {
enrichedProps = await enrichProps(definition, context, bindingStore)
}
</script>
{#if constructor && componentProps}

17
packages/standard-components/manifest.json

@ -1271,5 +1271,22 @@
"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"
}
]
}
}

27
packages/standard-components/src/forms/AttachmentField.svelte

@ -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>

22
packages/standard-components/src/forms/Form.svelte

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

31
packages/standard-components/src/forms/Placeholder.svelte

@ -1,17 +1,30 @@
<script>
import { getContext } from "svelte"
const { styleable, builderStore } = getContext("sdk")
const component = getContext("component")
$: styles = {
id: $component.styles.id,
allowSelection: $component.styles.allowSelection,
}
const { builderStore } = getContext("sdk")
</script>
{#if $builderStore.inBuilder}
<div use:styleable={styles}>
<div>
<slot />
</div>
{/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>

48
packages/standard-components/src/forms/SpectrumField.svelte

@ -16,41 +16,45 @@
const component = getContext("component")
// Register field with form
const { formApi } = formContext || {}
const labelPosition = fieldGroupContext?.labelPosition || "above"
const formField = formApi?.registerField(field)
$: formApi = formContext?.formApi
$: labelPosition = fieldGroupContext?.labelPosition || "above"
$: formField = formApi?.registerField(field)
// Expose field properties to parent component
fieldState = formField?.fieldState
fieldApi = formField?.fieldApi
fieldSchema = formField?.fieldSchema
$: {
fieldState = formField?.fieldState
fieldApi = formField?.fieldApi
fieldSchema = formField?.fieldSchema
}
// Extract label position from field group context
$: labelPositionClass =
labelPosition === "above" ? "" : `spectrum-FieldLabel--${labelPosition}`
</script>
{#if !fieldState}
<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}>
<label
for={$fieldState.fieldId}
class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${labelPositionClass}`}>
{label || ''}
</label>
<div class="spectrum-Form-itemField">
<FieldGroupFallback>
<div class="spectrum-Form-item" use:styleable={$component.styles}>
<label
for={$fieldState?.fieldId}
class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${labelPositionClass}`}>
{label || ''}
</label>
<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 />
{#if $fieldState.error}
<div class="error">{$fieldState.error}</div>
{/if}
</div>
{/if}
</div>
</FieldGroupFallback>
{/if}
</div>
</FieldGroupFallback>
<style>
.error {

1
packages/standard-components/src/forms/index.js

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

Loading…
Cancel
Save