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()
.mainType("div")
.component("@budibase/standard-components/container")
.instanceName("New Screen")
.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 rowDetailScreen from "./rowDetailScreen"
import rowListScreen from "./rowListScreen"
import emptyNewRowScreen from "./emptyNewRowScreen"
import createFromScratchScreen from "./createFromScratchScreen"
import emptyRowDetailScreen from "./emptyRowDetailScreen"
const allTemplates = tables => [
createFromScratchScreen,
...newRowScreen(tables),
...rowDetailScreen(tables),
...rowListScreen(tables),
emptyNewRowScreen,
emptyRowDetailScreen,
]
// Allows us to apply common behaviour to all create() functions
@ -22,8 +17,18 @@ const createTemplateOverride = (frontendState, create) => () => {
return screen
}
export default (frontendState, tables) =>
allTemplates(tables).map(template => ({
export default (frontendState, tables) => {
const enrichTemplate = template => ({
...template,
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 { Component } from "./utils/Component"
import { Screen } from "./utils/Screen"
import { Component } from "./utils/Component"
import {
makeBreadcrumbContainer,
makeMainContainer,
makeMainForm,
makeTitleContainer,
makeSaveButton,
makeSchemaFormComponents,
} from "./utils/commonComponents"
export default function(tables) {
@ -21,29 +22,45 @@ export default function(tables) {
export const newRowUrl = table => sanitizeUrl(`/${table.name}/new/row`)
export const NEW_ROW_TEMPLATE = "NEW_ROW_TEMPLATE"
function generateTitleContainer(table, providerId) {
return makeTitleContainer("New Row").addChild(
makeSaveButton(table, providerId)
)
function generateTitleContainer(table, formId) {
return makeTitleContainer("New Row").addChild(makeSaveButton(table, formId))
}
const createScreen = table => {
const screen = new Screen()
.component("@budibase/standard-components/newrow")
.table(table._id)
.route(newRowUrl(table))
.component("@budibase/standard-components/container")
.instanceName(`${table.name} - New`)
.name("")
.route(newRowUrl(table))
const dataform = new Component(
"@budibase/standard-components/dataformwide"
).instanceName("Form")
const form = makeMainForm()
.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 container = makeMainContainer()
const fieldGroup = new Component("@budibase/standard-components/fieldgroup")
.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(generateTitleContainer(table, providerId))
.addChild(dataform)
.addChild(generateTitleContainer(table, formId))
.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 { Component } from "./utils/Component"
import {
makeMainContainer,
makeBreadcrumbContainer,
makeTitleContainer,
makeSaveButton,
makeSchemaFormComponents,
makeMainForm,
} from "./utils/commonComponents"
export default function(tables) {
return tables.map(table => {
const heading = table.primaryDisplay
? `{{ data.${table.primaryDisplay} }}`
: null
return {
name: `${table.name} - Detail`,
create: () => createScreen(table, heading),
create: () => createScreen(table),
id: ROW_DETAIL_TEMPLATE,
}
})
@ -25,9 +23,9 @@ export default function(tables) {
export const ROW_DETAIL_TEMPLATE = "ROW_DETAIL_TEMPLATE"
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
const saveButton = makeSaveButton(table, providerId).normalStyle({
const saveButton = makeSaveButton(table, formId).normalStyle({
background: "#000000",
"border-width": "0",
"border-style": "None",
@ -60,8 +58,8 @@ function generateTitleContainer(table, title, providerId) {
onClick: [
{
parameters: {
rowId: `{{ ${providerId}._id }}`,
revId: `{{ ${providerId}._rev }}`,
rowId: `{{ ${formId}._id }}`,
revId: `{{ ${formId}._rev }}`,
tableId: table._id,
},
"##eventHandlerType": "Delete Row",
@ -81,23 +79,46 @@ function generateTitleContainer(table, title, providerId) {
.addChild(saveButton)
}
const createScreen = (table, heading) => {
const createScreen = table => {
const screen = new Screen()
.component("@budibase/standard-components/rowdetail")
.table(table._id)
.instanceName(`${table.name} - Detail`)
.route(rowDetailUrl(table))
.name("")
const dataform = new Component(
"@budibase/standard-components/dataformwide"
).instanceName("Form")
const form = makeMainForm()
.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 container = makeMainContainer()
const fieldGroup = new Component("@budibase/standard-components/fieldgroup")
.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(generateTitleContainer(table, heading || "Edit Row", providerId))
.addChild(dataform)
.addChild(generateTitleContainer(table, heading || "Edit Row", formId))
.addChild(fieldGroup)
return screen.addChild(container).json()
return screen.addChild(form).json()
}

View File

@ -14,17 +14,11 @@ export class Component extends BaseStructure {
active: {},
selected: {},
},
type: "",
_instanceName: "",
_children: [],
}
}
type(type) {
this._json.type = type
return this
}
normalStyle(styling) {
this._json._styles.normal = styling
return this
@ -35,14 +29,20 @@ export class Component extends BaseStructure {
return this
}
text(text) {
this._json.text = text
return this
}
// TODO: do we need this
instanceName(name) {
this._json._instanceName = name
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 { 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) {
return new Component("@budibase/standard-components/link")
@ -22,13 +32,12 @@ export function makeLinkComponent(tableName) {
})
}
export function makeMainContainer() {
return new Component("@budibase/standard-components/container")
export function makeMainForm() {
return new Component("@budibase/standard-components/form")
.type("div")
.normalStyle({
width: "700px",
padding: "0px",
background: "white",
"border-radius": "0.5rem",
"box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
margin: "auto",
@ -39,7 +48,26 @@ export function makeMainContainer() {
"padding-left": "48px",
"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) {
@ -78,7 +106,7 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
.addChild(identifierText)
}
export function makeSaveButton(table, providerId) {
export function makeSaveButton(table, formId) {
return new Component("@budibase/standard-components/button")
.normalStyle({
background: "#000000",
@ -99,8 +127,14 @@ export function makeSaveButton(table, providerId) {
disabled: false,
onClick: [
{
"##eventHandlerType": "Validate Form",
parameters: {
providerId,
componentId: formId,
},
},
{
parameters: {
providerId: formId,
},
"##eventHandlerType": "Save Row",
},
@ -142,3 +176,43 @@ export function makeTitleContainer(title) {
.instanceName("Title Container")
.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",
"navigation",
"login",
"rowdetail",
"newrow"
"rowdetail"
]
}
]

View File

@ -9,7 +9,10 @@ export const createContextStore = existingContext => {
store.update(state => {
if (componentId) {
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
}
return state

View File

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

View File

@ -35,8 +35,10 @@ export const enrichProps = async (props, context, user) => {
const totalContext = {
...context,
user,
// This is only required for legacy bindings that used "data" rather than a
// component ID.
data: context[context.closestComponentId],
data_draft: context[`${context.closestComponentId}_draft`],
}
// 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 cardhorizontal } from "./CardHorizontal.svelte"
export { default as cardstat } from "./CardStat.svelte"
export { default as newrow } from "./NewRow.svelte"
export { default as icon } from "./Icon.svelte"
export * from "./charts"
export * from "./forms"