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( cy.readFile(
"cypress/support/queryLevelTransformerFunctionWithData.js" "cypress/support/queryLevelTransformerFunctionWithData.js"
).then(transformerFunction => { ).then(transformerFunction => {
//console.log(transformerFunction[1])
cy.get(".CodeMirror textarea") cy.get(".CodeMirror textarea")
// Highlight current text and overwrite with file contents // Highlight current text and overwrite with file contents
.type(Cypress.platform === "darwin" ? "{cmd}a" : "{ctrl}a", { .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. * 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) { if (!asset || !componentId) {
return [] return []
} }
@ -73,7 +78,9 @@ export const getContextProviderComponents = (asset, componentId, type) => {
// Get the component tree leading up to this component, ignoring the component // Get the component tree leading up to this component, ignoring the component
// itself // itself
const path = findComponentPath(asset.props, componentId) const path = findComponentPath(asset.props, componentId)
if (!options?.includeSelf) {
path.pop() path.pop()
}
// Filter by only data provider components // Filter by only data provider components
return path.filter(component => { return path.filter(component => {
@ -138,19 +145,7 @@ export const getDatasourceForProvider = (asset, component) => {
if (!datasourceSetting) { if (!datasourceSetting) {
return null 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) { if (!component) {
return schema 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 settings = getComponentSettings(component._component)
const fieldSetting = settings.find( const fieldSetting = settings.find(
setting => setting.key === "field" && setting.type.startsWith("field/") setting => setting.key === "field" && setting.type.startsWith("field/")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3637,5 +3637,255 @@
"key": "value" "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 tableblock } from "./TableBlock.svelte"
export { default as cardsblock } from "./CardsBlock.svelte" export { default as cardsblock } from "./CardsBlock.svelte"
export { default as repeaterblock } from "./RepeaterBlock.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, palette,
horizontal horizontal
) => { ) => {
console.log("new chart")
const allCols = [labelColumn, ...(valueColumns || [null])] const allCols = [labelColumn, ...(valueColumns || [null])]
if ( if (
!dataProvider || !dataProvider ||

View File

@ -40,36 +40,7 @@
// Fetches the form schema from this form's dataSource // Fetches the form schema from this form's dataSource
const fetchSchema = async dataSource => { const fetchSchema = async dataSource => {
if (!dataSource) { schema = (await fetchDatasourceSchema(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 || {}
}
if (!loaded) { if (!loaded) {
loaded = true loaded = true
} }

View File

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

View File

@ -17,7 +17,7 @@ import JSONArrayFetch from "@budibase/frontend-core/src/fetch/JSONArrayFetch.js"
*/ */
export const fetchDatasourceSchema = async ( export const fetchDatasourceSchema = async (
datasource, datasource,
options = { enrichRelationships: false } options = { enrichRelationships: false, formSchema: false }
) => { ) => {
const handler = { const handler = {
table: TableFetch, table: TableFetch,
@ -35,7 +35,17 @@ export const fetchDatasourceSchema = async (
// Get the datasource definition and then schema // Get the datasource definition and then schema
const definition = await instance.getDefinition(datasource) 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) { if (!schema) {
return null return null
} }

View File

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