Update screen templates to support full form generation. Fix issues with screen templates

This commit is contained in:
Andrew Kingston 2021-02-02 14:32:58 +00:00
parent 3dd6769777
commit e3b0de6805
15 changed files with 190 additions and 212 deletions

View File

@ -9,5 +9,6 @@ const createScreen = () => {
return new Screen() return new Screen()
.mainType("div") .mainType("div")
.component("@budibase/standard-components/container") .component("@budibase/standard-components/container")
.instanceName("New Screen")
.json() .json()
} }

View File

@ -1,13 +0,0 @@
import { Screen } from "./utils/Screen"
export default {
name: `New Row (Empty)`,
create: () => createScreen(),
}
const createScreen = () => {
return new Screen()
.component("@budibase/standard-components/newrow")
.table("")
.json()
}

View File

@ -1,13 +0,0 @@
import { Screen } from "./utils/Screen"
export default {
name: `Row Detail (Empty)`,
create: () => createScreen(),
}
const createScreen = () => {
return new Screen()
.component("@budibase/standard-components/rowdetail")
.table("")
.json()
}

View File

@ -1,17 +1,12 @@
import newRowScreen from "./newRowScreen" import newRowScreen from "./newRowScreen"
import rowDetailScreen from "./rowDetailScreen" import rowDetailScreen from "./rowDetailScreen"
import rowListScreen from "./rowListScreen" import rowListScreen from "./rowListScreen"
import emptyNewRowScreen from "./emptyNewRowScreen"
import createFromScratchScreen from "./createFromScratchScreen" import createFromScratchScreen from "./createFromScratchScreen"
import emptyRowDetailScreen from "./emptyRowDetailScreen"
const allTemplates = tables => [ const allTemplates = tables => [
createFromScratchScreen,
...newRowScreen(tables), ...newRowScreen(tables),
...rowDetailScreen(tables), ...rowDetailScreen(tables),
...rowListScreen(tables), ...rowListScreen(tables),
emptyNewRowScreen,
emptyRowDetailScreen,
] ]
// Allows us to apply common behaviour to all create() functions // Allows us to apply common behaviour to all create() functions
@ -22,8 +17,18 @@ const createTemplateOverride = (frontendState, create) => () => {
return screen return screen
} }
export default (frontendState, tables) => export default (frontendState, tables) => {
allTemplates(tables).map(template => ({ const enrichTemplate = template => ({
...template, ...template,
create: createTemplateOverride(frontendState, template.create), create: createTemplateOverride(frontendState, template.create),
})) })
const fromScratch = enrichTemplate(createFromScratchScreen)
const tableTemplates = allTemplates(tables).map(enrichTemplate)
return [
fromScratch,
...tableTemplates.sort((templateA, templateB) => {
return templateA.name > templateB.name ? 1 : -1
}),
]
}

View File

@ -1,11 +1,12 @@
import sanitizeUrl from "./utils/sanitizeUrl" import sanitizeUrl from "./utils/sanitizeUrl"
import { Component } from "./utils/Component"
import { Screen } from "./utils/Screen" import { Screen } from "./utils/Screen"
import { Component } from "./utils/Component"
import { import {
makeBreadcrumbContainer, makeBreadcrumbContainer,
makeMainContainer, makeMainForm,
makeTitleContainer, makeTitleContainer,
makeSaveButton, makeSaveButton,
makeSchemaFormComponents,
} from "./utils/commonComponents" } from "./utils/commonComponents"
export default function(tables) { export default function(tables) {
@ -21,29 +22,45 @@ export default function(tables) {
export const newRowUrl = table => sanitizeUrl(`/${table.name}/new/row`) export const newRowUrl = table => sanitizeUrl(`/${table.name}/new/row`)
export const NEW_ROW_TEMPLATE = "NEW_ROW_TEMPLATE" export const NEW_ROW_TEMPLATE = "NEW_ROW_TEMPLATE"
function generateTitleContainer(table, providerId) { function generateTitleContainer(table, formId) {
return makeTitleContainer("New Row").addChild( return makeTitleContainer("New Row").addChild(makeSaveButton(table, formId))
makeSaveButton(table, providerId)
)
} }
const createScreen = table => { const createScreen = table => {
const screen = new Screen() const screen = new Screen()
.component("@budibase/standard-components/newrow") .component("@budibase/standard-components/container")
.table(table._id)
.route(newRowUrl(table))
.instanceName(`${table.name} - New`) .instanceName(`${table.name} - New`)
.name("") .route(newRowUrl(table))
const dataform = new Component( const form = makeMainForm()
"@budibase/standard-components/dataformwide" .instanceName("Form")
).instanceName("Form") .customProps({
theme: "spectrum--light",
size: "spectrum--medium",
datasource: {
label: table.name,
tableId: table._id,
type: "table",
},
})
const providerId = screen._json.props._id const fieldGroup = new Component("@budibase/standard-components/fieldgroup")
const container = makeMainContainer() .instanceName("Field Group")
.customProps({
labelPosition: "left",
})
// Add all form fields from this schema to the field group
makeSchemaFormComponents(table._id).forEach(component => {
fieldGroup.addChild(component)
})
// Add all children to the form
const formId = form._json._id
form
.addChild(makeBreadcrumbContainer(table.name, "New")) .addChild(makeBreadcrumbContainer(table.name, "New"))
.addChild(generateTitleContainer(table, providerId)) .addChild(generateTitleContainer(table, formId))
.addChild(dataform) .addChild(fieldGroup)
return screen.addChild(container).json() return screen.addChild(form).json()
} }

View File

@ -3,20 +3,18 @@ import { rowListUrl } from "./rowListScreen"
import { Screen } from "./utils/Screen" import { Screen } from "./utils/Screen"
import { Component } from "./utils/Component" import { Component } from "./utils/Component"
import { import {
makeMainContainer,
makeBreadcrumbContainer, makeBreadcrumbContainer,
makeTitleContainer, makeTitleContainer,
makeSaveButton, makeSaveButton,
makeSchemaFormComponents,
makeMainForm,
} from "./utils/commonComponents" } from "./utils/commonComponents"
export default function(tables) { export default function(tables) {
return tables.map(table => { return tables.map(table => {
const heading = table.primaryDisplay
? `{{ data.${table.primaryDisplay} }}`
: null
return { return {
name: `${table.name} - Detail`, name: `${table.name} - Detail`,
create: () => createScreen(table, heading), create: () => createScreen(table),
id: ROW_DETAIL_TEMPLATE, id: ROW_DETAIL_TEMPLATE,
} }
}) })
@ -25,9 +23,9 @@ export default function(tables) {
export const ROW_DETAIL_TEMPLATE = "ROW_DETAIL_TEMPLATE" export const ROW_DETAIL_TEMPLATE = "ROW_DETAIL_TEMPLATE"
export const rowDetailUrl = table => sanitizeUrl(`/${table.name}/:id`) export const rowDetailUrl = table => sanitizeUrl(`/${table.name}/:id`)
function generateTitleContainer(table, title, providerId) { function generateTitleContainer(table, title, formId) {
// have to override style for this, its missing margin // have to override style for this, its missing margin
const saveButton = makeSaveButton(table, providerId).normalStyle({ const saveButton = makeSaveButton(table, formId).normalStyle({
background: "#000000", background: "#000000",
"border-width": "0", "border-width": "0",
"border-style": "None", "border-style": "None",
@ -60,8 +58,8 @@ function generateTitleContainer(table, title, providerId) {
onClick: [ onClick: [
{ {
parameters: { parameters: {
rowId: `{{ ${providerId}._id }}`, rowId: `{{ ${formId}._id }}`,
revId: `{{ ${providerId}._rev }}`, revId: `{{ ${formId}._rev }}`,
tableId: table._id, tableId: table._id,
}, },
"##eventHandlerType": "Delete Row", "##eventHandlerType": "Delete Row",
@ -81,23 +79,46 @@ function generateTitleContainer(table, title, providerId) {
.addChild(saveButton) .addChild(saveButton)
} }
const createScreen = (table, heading) => { const createScreen = table => {
const screen = new Screen() const screen = new Screen()
.component("@budibase/standard-components/rowdetail") .component("@budibase/standard-components/rowdetail")
.table(table._id) .table(table._id)
.instanceName(`${table.name} - Detail`) .instanceName(`${table.name} - Detail`)
.route(rowDetailUrl(table)) .route(rowDetailUrl(table))
.name("")
const dataform = new Component( const form = makeMainForm()
"@budibase/standard-components/dataformwide" .instanceName("Form")
).instanceName("Form") .customProps({
theme: "spectrum--light",
size: "spectrum--medium",
datasource: {
label: table.name,
tableId: table._id,
type: "table",
},
})
const providerId = screen._json.props._id const fieldGroup = new Component("@budibase/standard-components/fieldgroup")
const container = makeMainContainer() .instanceName("Field Group")
.customProps({
labelPosition: "left",
})
// Add all form fields from this schema to the field group
makeSchemaFormComponents(table._id).forEach(component => {
fieldGroup.addChild(component)
})
// Add all children to the form
const formId = form._json._id
const rowDetailId = screen._json.props._id
const heading = table.primaryDisplay
? `{{ ${rowDetailId}.${table.primaryDisplay} }}`
: null
form
.addChild(makeBreadcrumbContainer(table.name, heading || "Edit")) .addChild(makeBreadcrumbContainer(table.name, heading || "Edit"))
.addChild(generateTitleContainer(table, heading || "Edit Row", providerId)) .addChild(generateTitleContainer(table, heading || "Edit Row", formId))
.addChild(dataform) .addChild(fieldGroup)
return screen.addChild(container).json() return screen.addChild(form).json()
} }

View File

@ -14,17 +14,11 @@ export class Component extends BaseStructure {
active: {}, active: {},
selected: {}, selected: {},
}, },
type: "",
_instanceName: "", _instanceName: "",
_children: [], _children: [],
} }
} }
type(type) {
this._json.type = type
return this
}
normalStyle(styling) { normalStyle(styling) {
this._json._styles.normal = styling this._json._styles.normal = styling
return this return this
@ -35,14 +29,20 @@ export class Component extends BaseStructure {
return this return this
} }
text(text) {
this._json.text = text
return this
}
// TODO: do we need this
instanceName(name) { instanceName(name) {
this._json._instanceName = name this._json._instanceName = name
return this return this
} }
// Shorthand for custom props "type"
type(type) {
this._json.type = type
return this
}
// Shorthand for custom props "text"
text(text) {
this._json.text = text
return this
}
} }

View File

@ -1,5 +1,15 @@
import { get } from "svelte/store"
import { Component } from "./Component" import { Component } from "./Component"
import { rowListUrl } from "../rowListScreen" import { rowListUrl } from "../rowListScreen"
import { backendUiStore } from "builderStore"
import StringFieldSelect from "../../../../components/design/PropertiesPanel/PropertyControls/StringFieldSelect.svelte"
import NumberFieldSelect from "../../../../components/design/PropertiesPanel/PropertyControls/NumberFieldSelect.svelte"
import OptionsFieldSelect from "../../../../components/design/PropertiesPanel/PropertyControls/OptionsFieldSelect.svelte"
import BooleanFieldSelect from "../../../../components/design/PropertiesPanel/PropertyControls/BooleanFieldSelect.svelte"
import LongFormFieldSelect from "../../../../components/design/PropertiesPanel/PropertyControls/LongFormFieldSelect.svelte"
import DateTimeFieldSelect from "../../../../components/design/PropertiesPanel/PropertyControls/DateTimeFieldSelect.svelte"
import AttachmentFieldSelect from "../../../../components/design/PropertiesPanel/PropertyControls/AttachmentFieldSelect.svelte"
import RelationshipFieldSelect from "../../../../components/design/PropertiesPanel/PropertyControls/RelationshipFieldSelect.svelte"
export function makeLinkComponent(tableName) { export function makeLinkComponent(tableName) {
return new Component("@budibase/standard-components/link") return new Component("@budibase/standard-components/link")
@ -22,13 +32,12 @@ export function makeLinkComponent(tableName) {
}) })
} }
export function makeMainContainer() { export function makeMainForm() {
return new Component("@budibase/standard-components/container") return new Component("@budibase/standard-components/form")
.type("div") .type("div")
.normalStyle({ .normalStyle({
width: "700px", width: "700px",
padding: "0px", padding: "0px",
background: "white",
"border-radius": "0.5rem", "border-radius": "0.5rem",
"box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)", "box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
margin: "auto", margin: "auto",
@ -39,7 +48,26 @@ export function makeMainContainer() {
"padding-left": "48px", "padding-left": "48px",
"margin-bottom": "20px", "margin-bottom": "20px",
}) })
.instanceName("Container") .instanceName("Form")
}
export function makeMainContainer() {
return new Component("@budibase/standard-components/container")
.type("div")
.normalStyle({
width: "700px",
padding: "0px",
"border-radius": "0.5rem",
"box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
margin: "auto",
"margin-top": "20px",
"padding-top": "48px",
"padding-bottom": "48px",
"padding-right": "48px",
"padding-left": "48px",
"margin-bottom": "20px",
})
.instanceName("Form")
} }
export function makeBreadcrumbContainer(tableName, text, capitalise = false) { export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
@ -78,7 +106,7 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
.addChild(identifierText) .addChild(identifierText)
} }
export function makeSaveButton(table, providerId) { export function makeSaveButton(table, formId) {
return new Component("@budibase/standard-components/button") return new Component("@budibase/standard-components/button")
.normalStyle({ .normalStyle({
background: "#000000", background: "#000000",
@ -99,8 +127,14 @@ export function makeSaveButton(table, providerId) {
disabled: false, disabled: false,
onClick: [ onClick: [
{ {
"##eventHandlerType": "Validate Form",
parameters: { parameters: {
providerId, componentId: formId,
},
},
{
parameters: {
providerId: formId,
}, },
"##eventHandlerType": "Save Row", "##eventHandlerType": "Save Row",
}, },
@ -142,3 +176,43 @@ export function makeTitleContainer(title) {
.instanceName("Title Container") .instanceName("Title Container")
.addChild(heading) .addChild(heading)
} }
const fieldTypeToComponentMap = {
string: "stringfield",
number: "numberfield",
options: "optionsfield",
boolean: "booleanfield",
longform: "longformfield",
datetime: "datetimefield",
attachment: "attachmentfield",
link: "relationshipfield",
}
export function makeSchemaFormComponents(tableId) {
const tables = get(backendUiStore).tables
const schema = tables.find(table => table._id === tableId)?.schema ?? {}
let components = []
let fields = Object.keys(schema)
fields.forEach(field => {
const fieldSchema = schema[field]
const componentType = fieldTypeToComponentMap[fieldSchema.type]
const fullComponentType = `@budibase/standard-components/${componentType}`
if (componentType) {
const component = new Component(fullComponentType)
.instanceName(field)
.customProps({
field,
label: field,
placeholder: field,
})
if (fieldSchema.type === "options") {
component.customProps({ placeholder: "Choose an option " })
}
if (fieldSchema.type === "boolean") {
component.customProps({ text: field, label: "" })
}
components.push(component)
}
})
return components
}

View File

@ -60,8 +60,7 @@
"screenslot", "screenslot",
"navigation", "navigation",
"login", "login",
"rowdetail", "rowdetail"
"newrow"
] ]
} }
] ]

View File

@ -9,7 +9,10 @@ export const createContextStore = existingContext => {
store.update(state => { store.update(state => {
if (componentId) { if (componentId) {
state[componentId] = data state[componentId] = data
state[`${componentId}_draft`] = cloneDeep(data)
// Keep track of the closest component ID so we can later hydrate a "data" prop.
// This is only required for legacy bindings that used "data" rather than a
// component ID.
state.closestComponentId = componentId state.closestComponentId = componentId
} }
return state return state

View File

@ -7,7 +7,7 @@ import { ActionTypes } from "../constants"
const saveRowHandler = async (action, context) => { const saveRowHandler = async (action, context) => {
const { fields, providerId } = action.parameters const { fields, providerId } = action.parameters
if (providerId) { if (providerId) {
let draft = context[`${providerId}_draft`] let draft = context[providerId]
if (fields) { if (fields) {
for (let [key, entry] of Object.entries(fields)) { for (let [key, entry] of Object.entries(fields)) {
draft[key] = await enrichDataBinding(entry.value, context) draft[key] = await enrichDataBinding(entry.value, context)

View File

@ -35,8 +35,10 @@ export const enrichProps = async (props, context, user) => {
const totalContext = { const totalContext = {
...context, ...context,
user, user,
// This is only required for legacy bindings that used "data" rather than a
// component ID.
data: context[context.closestComponentId], data: context[context.closestComponentId],
data_draft: context[`${context.closestComponentId}_draft`],
} }
// Enrich all data bindings in top level props // Enrich all data bindings in top level props

View File

@ -1,103 +0,0 @@
<script>
import { getContext } from "svelte"
import {
Label,
DatePicker,
Input,
Select,
Toggle,
RichText,
} from "@budibase/bbui"
import Dropzone from "./attachments/Dropzone.svelte"
import LinkedRowSelector from "./LinkedRowSelector.svelte"
import { capitalise } from "./helpers"
const { styleable, API } = getContext("sdk")
const component = getContext("component")
const context = getContext("context")
export let wide = false
let row
let schema
let fields = []
// Fetch info about the closest data context
$: getFormData($context[$context.closestComponentId])
const getFormData = async context => {
if (context) {
const tableDefinition = await API.fetchTableDefinition(context.tableId)
schema = tableDefinition?.schema
fields = Object.keys(schema ?? {})
// Use the draft version for editing
row = $context[`${$context.closestComponentId}_draft`]
}
}
</script>
<div class="form-content" use:styleable={$component.styles}>
<!-- <ErrorsBox errors={$store.saveRowErrors || {}} />-->
{#each fields as field}
<div class="form-field" class:wide>
{#if !(schema[field].type === 'boolean' && !wide)}
<Label extraSmall={!wide} grey>{capitalise(schema[field].name)}</Label>
{/if}
{#if schema[field].type === 'options'}
<Select secondary bind:value={row[field]}>
<option value="">Choose an option</option>
{#each schema[field].constraints.inclusion as opt}
<option>{opt}</option>
{/each}
</Select>
{:else if schema[field].type === 'datetime'}
<DatePicker bind:value={row[field]} />
{:else if schema[field].type === 'boolean'}
<Toggle
text={wide ? null : capitalise(schema[field].name)}
bind:checked={row[field]} />
{:else if schema[field].type === 'number'}
<Input type="number" bind:value={row[field]} />
{:else if schema[field].type === 'string'}
<Input bind:value={row[field]} />
{:else if schema[field].type === 'longform'}
<RichText bind:value={row[field]} />
{:else if schema[field].type === 'attachment'}
<Dropzone bind:files={row[field]} />
{:else if schema[field].type === 'link'}
<LinkedRowSelector
secondary
showLabel={false}
bind:linkedRows={row[field]}
schema={schema[field]} />
{/if}
</div>
{/each}
</div>
<style>
.form {
display: flex;
flex-direction: column;
align-items: center;
}
.form-content {
display: grid;
gap: var(--spacing-xl);
width: 100%;
}
.form-field {
display: grid;
}
.form-field.wide {
align-items: center;
grid-template-columns: 20% 1fr;
gap: var(--spacing-xl);
}
.form-field.wide :global(label) {
margin-bottom: 0;
}
</style>

View File

@ -1,14 +0,0 @@
<script>
import { getContext } from "svelte"
const { Provider, styleable } = getContext("sdk")
const component = getContext("component")
export let table
</script>
<div use:styleable={$component.styles}>
<Provider data={{ tableId: table }}>
<slot />
</Provider>
</div>

View File

@ -28,7 +28,6 @@ export { default as image } from "./Image.svelte"
export { default as embed } from "./Embed.svelte" export { default as embed } from "./Embed.svelte"
export { default as cardhorizontal } from "./CardHorizontal.svelte" export { default as cardhorizontal } from "./CardHorizontal.svelte"
export { default as cardstat } from "./CardStat.svelte" export { default as cardstat } from "./CardStat.svelte"
export { default as newrow } from "./NewRow.svelte"
export { default as icon } from "./Icon.svelte" export { default as icon } from "./Icon.svelte"
export * from "./charts" export * from "./charts"
export * from "./forms" export * from "./forms"