Add form block and form block plus components

This commit is contained in:
Andrew Kingston 2022-04-01 12:51:23 +01:00
parent 459c285d23
commit d08d9d1248
18 changed files with 654 additions and 97 deletions

View File

@ -47,7 +47,6 @@ filterTests(["smoke", "all"], () => {
cy.readFile(
"cypress/support/queryLevelTransformerFunctionWithData.js"
).then(transformerFunction => {
//console.log(transformerFunction[1])
cy.get(".CodeMirror textarea")
// Highlight current text and overwrite with file contents
.type(Cypress.platform === "darwin" ? "{cmd}a" : "{ctrl}a", {

View File

@ -65,7 +65,12 @@ export const getComponentBindableProperties = (asset, componentId) => {
/**
* Gets all data provider components above a component.
*/
export const getContextProviderComponents = (asset, componentId, type) => {
export const getContextProviderComponents = (
asset,
componentId,
type,
options = { includeSelf: false }
) => {
if (!asset || !componentId) {
return []
}
@ -73,7 +78,9 @@ export const getContextProviderComponents = (asset, componentId, type) => {
// Get the component tree leading up to this component, ignoring the component
// itself
const path = findComponentPath(asset.props, componentId)
path.pop()
if (!options?.includeSelf) {
path.pop()
}
// Filter by only data provider components
return path.filter(component => {
@ -138,19 +145,7 @@ export const getDatasourceForProvider = (asset, component) => {
if (!datasourceSetting) {
return null
}
// There are different types of setting which can be a datasource, for
// example an actual datasource object, or a table ID string.
// Convert the datasource setting into a proper datasource object so that
// we can use it properly
if (datasourceSetting.type === "table") {
return {
tableId: component[datasourceSetting?.key],
type: "table",
}
} else {
return component[datasourceSetting?.key]
}
return component[datasourceSetting?.key]
}
/**
@ -643,6 +638,17 @@ const buildFormSchema = component => {
if (!component) {
return schema
}
// If this is a form block, simply use the fields setting
if (component._component.endsWith("formblock")) {
let schema = {}
component.fields?.forEach(field => {
schema[field] = { type: "string" }
})
return schema
}
// Otherwise find all field component children
const settings = getComponentSettings(component._component)
const fieldSetting = settings.find(
setting => setting.key === "field" && setting.type.startsWith("field/")

View File

@ -5,26 +5,20 @@
"children": [
"tableblock",
"cardsblock",
"repeaterblock"
"repeaterblock",
"formblock",
"formblockplus"
]
},
{
"name": "Layout",
"icon": "ClassicGridView",
"children": [
"container",
"section"
]
"children": ["container", "section"]
},
{
"name": "Data",
"icon": "Data",
"children": [
"dataprovider",
"repeater",
"table",
"dynamicfilter"
]
"children": ["dataprovider", "repeater", "table", "dynamicfilter"]
},
{
"name": "Form",
@ -51,22 +45,12 @@
{
"name": "Card",
"icon": "Card",
"children": [
"spectrumcard",
"cardstat"
]
"children": ["spectrumcard", "cardstat"]
},
{
"name": "Chart",
"icon": "GraphBarVertical",
"children": [
"bar",
"line",
"area",
"pie",
"donut",
"candlestick"
]
"children": ["bar", "line", "area", "pie", "donut", "candlestick"]
},
{
"name": "Elements",
@ -87,4 +71,3 @@
]
}
]

View File

@ -49,7 +49,6 @@
}
} catch (error) {
notifications.error("Error duplicating screen")
console.log(error)
}
}

View File

@ -21,6 +21,7 @@
export let key
export let actions
export let bindings = []
export let nested
let selectedAction = actions?.length ? actions[0] : null
@ -137,6 +138,7 @@
this={selectedActionComponent}
parameters={selectedAction.parameters}
bindings={allBindings}
{nested}
/>
</div>
{/key}

View File

@ -12,6 +12,7 @@
export let value = []
export let name
export let bindings
export let nested
let drawer
let tmpValue
@ -83,5 +84,6 @@
eventType={name}
{bindings}
{key}
{nested}
/>
</Drawer>

View File

@ -10,11 +10,13 @@
export let parameters
export let bindings = []
export let nested
$: formComponents = getContextProviderComponents(
$currentAsset,
$store.selectedComponentId,
"form"
"form",
{ includeSelf: nested }
)
$: schemaComponents = getContextProviderComponents(
$currentAsset,

View File

@ -79,6 +79,7 @@
bindings={allBindings}
name={key}
text={label}
{nested}
{key}
{type}
{...props}

View File

@ -1,26 +1,28 @@
<script>
import { Select } from "@budibase/bbui"
import { tables } from "stores/backend"
import { createEventDispatcher } from "svelte"
import { tables as tablesStore } from "stores/backend"
export let value
const dispatch = createEventDispatcher()
$: tables = $tablesStore.list.map(m => ({
label: m.name,
tableId: m._id,
type: "table",
}))
const onChange = e => {
const dataSource = tables?.find(x => x.tableId === e.detail)
dispatch("change", dataSource)
}
</script>
<div>
<Select extraThin secondary wide on:change {value}>
<option value="">Choose a table</option>
{#each $tables.list as table}
<option value={table._id}>{table.name}</option>
{/each}
</Select>
</div>
<style>
div {
flex: 1 1 auto;
display: flex;
flex-direction: row;
}
div :global(> *) {
flex: 1 1 auto;
}
</style>
<Select
on:change={onChange}
value={value?.tableId}
options={tables}
getOptionValue={x => x.tableId}
getOptionLabel={x => x.label}
/>

View File

@ -3637,5 +3637,255 @@
"key": "value"
}
]
},
"formblock": {
"name": "Form Block",
"icon": "Form",
"styles": ["size"],
"block": true,
"settings": [
{
"type": "select",
"label": "Type",
"key": "actionType",
"options": ["Create", "Update"],
"defaultValue": "Create"
},
{
"type": "dataSource",
"label": "Schema",
"key": "dataSource"
},
{
"type": "text",
"label": "Title",
"key": "title"
},
{
"section": true,
"name": "Fields",
"settings": [
{
"type": "multifield",
"label": "Fields",
"key": "fields"
},
{
"type": "select",
"label": "Field labels",
"key": "labelPosition",
"defaultValue": "left",
"options": [
{
"label": "Left",
"value": "left"
},
{
"label": "Right",
"value": "right"
},
{
"label": "Above",
"value": "above"
}
]
},
{
"type": "select",
"label": "Size",
"key": "size",
"options": [
{
"label": "Medium",
"value": "spectrum--medium"
},
{
"label": "Large",
"value": "spectrum--large"
}
],
"defaultValue": "spectrum--medium"
},
{
"type": "boolean",
"label": "Disabled",
"key": "disabled",
"defaultValue": false
}
]
},
{
"section": true,
"name": "Buttons",
"settings": [
{
"type": "boolean",
"label": "Show primary button",
"key": "showPrimaryButton",
"defaultValue": true
},
{
"type": "text",
"label": "Text",
"key": "primaryButtonText",
"defaultValue": "Submit",
"dependsOn": "showPrimaryButton"
},
{
"type": "event",
"label": "On click",
"key": "primaryButtonOnClick",
"nested": true,
"dependsOn": "showPrimaryButton"
},
{
"type": "boolean",
"label": "Show secondary button",
"key": "showSecondaryButton",
"defaultValue": false
},
{
"type": "text",
"label": "Text",
"key": "secondaryButtonText",
"defaultValue": "Action",
"dependsOn": "showSecondaryButton"
},
{
"type": "event",
"label": "On click",
"key": "secondaryButtonOnClick",
"nested": true,
"dependsOn": "showSecondaryButton"
}
]
}
],
"context": [
{
"type": "form",
"suffix": "form"
}
]
},
"formblockplus": {
"name": "Form Block+",
"icon": "Form",
"styles": ["size"],
"block": true,
"settings": [
{
"type": "select",
"label": "Type",
"key": "actionType",
"options": ["Create", "Update"],
"defaultValue": "Create"
},
{
"type": "table",
"label": "Schema",
"key": "dataSource"
},
{
"type": "text",
"label": "Row ID to update",
"key": "rowId",
"dependsOn": {
"setting": "actionType",
"value": "Update"
}
},
{
"type": "text",
"label": "Title",
"key": "title",
"nested": true
},
{
"section": true,
"name": "Fields",
"settings": [
{
"type": "multifield",
"label": "Fields",
"key": "fields"
},
{
"type": "select",
"label": "Field labels",
"key": "labelPosition",
"defaultValue": "left",
"options": [
{
"label": "Left",
"value": "left"
},
{
"label": "Right",
"value": "right"
},
{
"label": "Above",
"value": "above"
}
]
},
{
"type": "select",
"label": "Size",
"key": "size",
"options": [
{
"label": "Medium",
"value": "spectrum--medium"
},
{
"label": "Large",
"value": "spectrum--large"
}
],
"defaultValue": "spectrum--medium"
},
{
"type": "boolean",
"label": "Disabled",
"key": "disabled",
"defaultValue": false
}
]
},
{
"section": true,
"name": "Buttons",
"settings": [
{
"type": "boolean",
"label": "Show save button",
"key": "showSaveButton",
"defaultValue": true
},
{
"type": "boolean",
"label": "Show delete button",
"key": "showDeleteButton",
"defaultValue": false,
"dependsOn": {
"setting": "actionType",
"value": "Update"
}
}
]
}
],
"context": [
{
"type": "form",
"suffix": "form"
},
{
"type": "schema",
"suffix": "repeater"
}
]
}
}

View File

@ -0,0 +1,132 @@
<script>
import { getContext } from "svelte"
import BlockComponent from "../../BlockComponent.svelte"
import Block from "../../Block.svelte"
import { Heading, Layout } from "@budibase/bbui"
import Placeholder from "../Placeholder.svelte"
export let actionType
export let dataSource
export let size
export let disabled
export let fields
export let labelPosition
export let title
export let showPrimaryButton
export let primaryButtonOnClick
export let primaryButtonText
export let showSecondaryButton
export let secondaryButtonOnClick
export let secondaryButtonText
const { styleable, fetchDatasourceSchema } = getContext("sdk")
const component = getContext("component")
const FieldTypeToComponentMap = {
string: "stringfield",
number: "numberfield",
options: "optionsfield",
array: "multifieldselect",
boolean: "booleanfield",
longform: "longformfield",
datetime: "datetimefield",
attachment: "attachmentfield",
link: "relationshipfield",
json: "jsonfield",
}
let schema
$: fetchSchema(dataSource)
const fetchSchema = async () => {
schema = (await fetchDatasourceSchema(dataSource)) || {}
}
const getComponentForField = field => {
if (!field || !schema?.[field]) {
return null
}
const type = schema[field].type
return FieldTypeToComponentMap[type]
}
</script>
<Block>
<div use:styleable={$component.styles}>
{#if fields?.length}
<BlockComponent
type="form"
props={{ actionType, dataSource, size, disabled }}
context="form"
>
<Layout noPadding gap="M">
<div class="title" class:with-text={!!title}>
<Heading>{title || ""}</Heading>
<div class="buttons">
{#if showSecondaryButton}
<BlockComponent
type="button"
props={{
text: secondaryButtonText,
onClick: secondaryButtonOnClick,
quiet: true,
type: "secondary",
}}
/>
{/if}
{#if showPrimaryButton}
<BlockComponent
type="button"
props={{
text: primaryButtonText,
onClick: primaryButtonOnClick,
type: "cta",
}}
/>
{/if}
</div>
</div>
<BlockComponent type="fieldgroup" props={{ labelPosition }}>
{#each fields as field}
{#if getComponentForField(field)}
<BlockComponent
type={getComponentForField(field)}
props={{
field,
label: field,
placeholder: field,
disabled,
}}
/>
{/if}
{/each}
</BlockComponent>
</Layout>
</BlockComponent>
{:else}
<Placeholder
text="Choose your schema and add some fields to your form to get started"
/>
{/if}
</div>
</Block>
<style>
.title {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: var(--spacing-m);
order: 2;
}
.title.with-text {
order: 0;
}
.buttons {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
gap: var(--spacing-m);
}
</style>

View File

@ -0,0 +1,199 @@
<script>
import { getContext } from "svelte"
import BlockComponent from "../../BlockComponent.svelte"
import Block from "../../Block.svelte"
import { Layout } from "@budibase/bbui"
import Placeholder from "../Placeholder.svelte"
import { makePropSafe as safe } from "@budibase/string-templates"
export let actionType
export let dataSource
export let size
export let disabled
export let fields
export let labelPosition
export let title
export let showSaveButton
export let showDeleteButton
export let rowId
const { styleable, fetchDatasourceSchema, builderStore } = getContext("sdk")
const component = getContext("component")
const FieldTypeToComponentMap = {
string: "stringfield",
number: "numberfield",
options: "optionsfield",
array: "multifieldselect",
boolean: "booleanfield",
longform: "longformfield",
datetime: "datetimefield",
attachment: "attachmentfield",
link: "relationshipfield",
json: "jsonfield",
}
let schema
let formId
let providerId
let repeaterId
$: fetchSchema(dataSource)
$: onSave = [
{
"##eventHandlerType": "Save Row",
parameters: {
providerId: formId,
tableId: dataSource?.tableId,
},
},
{
"##eventHandlerType": "Close Screen Modal",
},
]
$: onDelete = [
{
"##eventHandlerType": "Delete Row",
parameters: {
confirm: true,
tableId: dataSource?.tableId,
rowId: `{{ ${safe(repeaterId)}.${safe("_id")} }}`,
revId: `{{ ${safe(repeaterId)}.${safe("_rev")} }}`,
},
},
{
"##eventHandlerType": "Close Screen Modal",
},
]
$: filter = [
{
field: "_id",
operator: "equal",
type: "string",
value: rowId,
valueType: "binding",
},
]
// If we're using an "update" form, use the real data provider. If we're
// using a create form, we just want a fake array so that our repeater
// will actually render the form, but data doesn't matter.
$: dataProvider =
actionType === "Update"
? `{{ literal ${safe(providerId)} }}`
: { rows: [{}] }
const fetchSchema = async () => {
schema = (await fetchDatasourceSchema(dataSource)) || {}
}
const getComponentForField = field => {
if (!field || !schema?.[field]) {
return null
}
const type = schema[field].type
return FieldTypeToComponentMap[type]
}
</script>
<Block>
<div use:styleable={$component.styles}>
{#if fields?.length}
<BlockComponent
type="dataprovider"
context="provider"
bind:id={providerId}
props={{
dataSource,
filter,
limit: rowId ? 1 : $builderStore.inBuilder ? 1 : 0,
paginate: false,
}}
>
<BlockComponent
type="repeater"
context="repeater"
bind:id={repeaterId}
props={{
dataProvider,
noRowsMessage: "We couldn't find a row to display",
}}
>
<BlockComponent
type="form"
props={{ actionType, dataSource, size, disabled }}
context="form"
bind:id={formId}
>
<Layout noPadding gap="M">
<div class="title" class:with-text={!!title}>
<BlockComponent type="heading" props={{ text: title || "" }} />
<div class="buttons">
{#if showDeleteButton}
<BlockComponent
type="button"
props={{
text: "Delete",
onClick: onDelete,
quiet: true,
type: "secondary",
}}
/>
{/if}
{#if showSaveButton}
<BlockComponent
type="button"
props={{
text: "Save",
onClick: onSave,
type: "cta",
}}
/>
{/if}
</div>
</div>
<BlockComponent type="fieldgroup" props={{ labelPosition }}>
{#each fields as field}
{#if getComponentForField(field)}
<BlockComponent
type={getComponentForField(field)}
props={{
field,
label: field,
placeholder: field,
disabled,
}}
/>
{/if}
{/each}
</BlockComponent>
</Layout>
</BlockComponent>
</BlockComponent>
</BlockComponent>
{:else}
<Placeholder
text="Choose your schema and add some fields to your form to get started"
/>
{/if}
</div>
</Block>
<style>
.title {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: var(--spacing-m);
order: 2;
}
.title.with-text {
order: 0;
}
.buttons {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
gap: var(--spacing-m);
}
</style>

View File

@ -1,3 +1,5 @@
export { default as tableblock } from "./TableBlock.svelte"
export { default as cardsblock } from "./CardsBlock.svelte"
export { default as repeaterblock } from "./RepeaterBlock.svelte"
export { default as formblock } from "./FormBlock.svelte"
export { default as formblockplus } from "./FormBlockPlus.svelte"

View File

@ -53,7 +53,6 @@
palette,
horizontal
) => {
console.log("new chart")
const allCols = [labelColumn, ...(valueColumns || [null])]
if (
!dataProvider ||

View File

@ -40,36 +40,7 @@
// Fetches the form schema from this form's dataSource
const fetchSchema = async dataSource => {
if (!dataSource) {
schema = {}
}
// If the datasource is a query, then we instead use a schema of the query
// parameters rather than the output schema
else if (
dataSource.type === "query" &&
dataSource._id &&
actionType === "Create"
) {
try {
const query = await API.fetchQueryDefinition(dataSource._id)
let paramSchema = {}
const params = query.parameters || []
params.forEach(param => {
paramSchema[param.name] = { ...param, type: "string" }
})
schema = paramSchema
} catch (error) {
schema = {}
}
}
// For all other cases, just grab the normal schema
else {
const dataSourceSchema = await fetchDatasourceSchema(dataSource)
schema = dataSourceSchema || {}
}
schema = (await fetchDatasourceSchema(dataSource)) || {}
if (!loaded) {
loaded = true
}

View File

@ -66,7 +66,6 @@ const createScreenStore = () => {
}
let children = []
findChildrenByType(component, type, children)
console.log(children)
return children
},
}

View File

@ -17,7 +17,7 @@ import JSONArrayFetch from "@budibase/frontend-core/src/fetch/JSONArrayFetch.js"
*/
export const fetchDatasourceSchema = async (
datasource,
options = { enrichRelationships: false }
options = { enrichRelationships: false, formSchema: false }
) => {
const handler = {
table: TableFetch,
@ -35,7 +35,17 @@ export const fetchDatasourceSchema = async (
// Get the datasource definition and then schema
const definition = await instance.getDefinition(datasource)
let schema = instance.getSchema(datasource, definition)
// Get the normal schema as long as we aren't wanting a form schema
let schema
if (datasource?.type !== "query" || !options?.formSchema) {
schema = instance.getSchema(datasource, definition)
} else if (definition.parameters?.length) {
schema = {}
definition.parameters.forEach(param => {
schema[param.name] = { ...param, type: "string" }
})
}
if (!schema) {
return null
}

View File

@ -48,7 +48,6 @@ export const buildAttachmentEndpoints = API => {
* @param data the file to upload
*/
externalUpload: async ({ datasourceId, bucket, key, data }) => {
console.log(API)
const { signedUrl, publicUrl } = await getSignedDatasourceURL({
datasourceId,
bucket,