commit
354817dbf3
17 changed files with 530 additions and 125 deletions
|
@ -1,5 +1,5 @@
|
|||
import filterTests from "../support/filterTests"
|
||||
const interact = require('../support/interact')
|
||||
const interact = require("../support/interact")
|
||||
|
||||
filterTests(["smoke", "all"], () => {
|
||||
context("Query Level Transformers", () => {
|
||||
|
|
|
@ -185,43 +185,42 @@ export const makeComponentUnique = component => {
|
|||
// Replace component ID
|
||||
const oldId = component._id
|
||||
const newId = Helpers.uuid()
|
||||
component._id = newId
|
||||
let definition = JSON.stringify(component)
|
||||
|
||||
if (component._children?.length) {
|
||||
let children = JSON.stringify(component._children)
|
||||
// Replace all instances of this ID in HBS bindings
|
||||
definition = definition.replace(new RegExp(oldId, "g"), newId)
|
||||
|
||||
// Replace all instances of this ID in child HBS bindings
|
||||
children = children.replace(new RegExp(oldId, "g"), newId)
|
||||
// Replace all instances of this ID in JS bindings
|
||||
const bindings = findHBSBlocks(definition)
|
||||
bindings.forEach(binding => {
|
||||
// JSON.stringify will have escaped double quotes, so we need
|
||||
// to account for that
|
||||
let sanitizedBinding = binding.replace(/\\"/g, '"')
|
||||
|
||||
// Replace all instances of this ID in child JS bindings
|
||||
const bindings = findHBSBlocks(children)
|
||||
bindings.forEach(binding => {
|
||||
// JSON.stringify will have escaped double quotes, so we need
|
||||
// to account for that
|
||||
let sanitizedBinding = binding.replace(/\\"/g, '"')
|
||||
// Check if this is a valid JS binding
|
||||
let js = decodeJSBinding(sanitizedBinding)
|
||||
if (js != null) {
|
||||
// Replace ID inside JS binding
|
||||
js = js.replace(new RegExp(oldId, "g"), newId)
|
||||
|
||||
// Check if this is a valid JS binding
|
||||
let js = decodeJSBinding(sanitizedBinding)
|
||||
if (js != null) {
|
||||
// Replace ID inside JS binding
|
||||
js = js.replace(new RegExp(oldId, "g"), newId)
|
||||
// Create new valid JS binding
|
||||
let newBinding = encodeJSBinding(js)
|
||||
|
||||
// Create new valid JS binding
|
||||
let newBinding = encodeJSBinding(js)
|
||||
// Replace escaped double quotes
|
||||
newBinding = newBinding.replace(/"/g, '\\"')
|
||||
|
||||
// Replace escaped double quotes
|
||||
newBinding = newBinding.replace(/"/g, '\\"')
|
||||
// Insert new JS back into binding.
|
||||
// A single string replace here is better than a regex as
|
||||
// the binding contains special characters, and we only need
|
||||
// to replace a single instance.
|
||||
definition = definition.replace(binding, newBinding)
|
||||
}
|
||||
})
|
||||
|
||||
// Insert new JS back into binding.
|
||||
// A single string replace here is better than a regex as
|
||||
// the binding contains special characters, and we only need
|
||||
// to replace a single instance.
|
||||
children = children.replace(binding, newBinding)
|
||||
}
|
||||
})
|
||||
|
||||
// Recurse on all children
|
||||
component._children = JSON.parse(children)
|
||||
component._children.forEach(makeComponentUnique)
|
||||
// Recurse on all children
|
||||
component = JSON.parse(definition)
|
||||
return {
|
||||
...component,
|
||||
_children: component._children?.map(makeComponentUnique),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -169,7 +169,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 []
|
||||
}
|
||||
|
@ -177,7 +182,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 => {
|
||||
|
@ -798,6 +805,17 @@ export 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/")
|
||||
|
|
|
@ -621,7 +621,7 @@ export const getFrontendStore = () => {
|
|||
|
||||
// Make new component unique if copying
|
||||
if (!cut) {
|
||||
makeComponentUnique(componentToPaste)
|
||||
componentToPaste = makeComponentUnique(componentToPaste)
|
||||
}
|
||||
newComponentId = componentToPaste._id
|
||||
|
||||
|
@ -931,7 +931,7 @@ export const getFrontendStore = () => {
|
|||
}
|
||||
|
||||
// Replace block with ejected definition
|
||||
makeComponentUnique(ejectedDefinition)
|
||||
ejectedDefinition = makeComponentUnique(ejectedDefinition)
|
||||
const index = parent._children.findIndex(x => x._id === componentId)
|
||||
parent._children[index] = ejectedDefinition
|
||||
nextSelectedComponentId = ejectedDefinition._id
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
export let key
|
||||
export let actions
|
||||
export let bindings = []
|
||||
export let nested
|
||||
|
||||
$: showAvailableActions = !actions?.length
|
||||
|
||||
|
@ -187,6 +188,7 @@
|
|||
this={selectedActionComponent}
|
||||
parameters={selectedAction.parameters}
|
||||
bindings={allBindings}
|
||||
{nested}
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
export let value = []
|
||||
export let name
|
||||
export let bindings
|
||||
export let nested
|
||||
|
||||
let drawer
|
||||
let tmpValue
|
||||
|
@ -90,6 +91,7 @@
|
|||
eventType={name}
|
||||
{bindings}
|
||||
{key}
|
||||
{nested}
|
||||
/>
|
||||
</Drawer>
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -95,6 +95,7 @@
|
|||
bindings={allBindings}
|
||||
name={key}
|
||||
text={label}
|
||||
{nested}
|
||||
{key}
|
||||
{type}
|
||||
{...props}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
export let value
|
||||
export let bindings
|
||||
export let placeholder
|
||||
|
||||
$: urlOptions = $store.screens
|
||||
.map(screen => screen.routing?.route)
|
||||
|
@ -13,6 +14,7 @@
|
|||
<DrawerBindableCombobox
|
||||
{value}
|
||||
{bindings}
|
||||
{placeholder}
|
||||
on:change
|
||||
options={urlOptions}
|
||||
appendBindingsAsOptions={false}
|
||||
|
|
|
@ -13,19 +13,29 @@
|
|||
export let componentBindings
|
||||
export let isScreen = false
|
||||
|
||||
$: sections = getSections(componentDefinition)
|
||||
$: sections = getSections(componentInstance, componentDefinition, isScreen)
|
||||
|
||||
const getSections = definition => {
|
||||
const getSections = (instance, definition, isScreen) => {
|
||||
const settings = definition?.settings ?? []
|
||||
const generalSettings = settings.filter(setting => !setting.section)
|
||||
const customSections = settings.filter(setting => setting.section)
|
||||
return [
|
||||
let sections = [
|
||||
{
|
||||
name: "General",
|
||||
settings: generalSettings,
|
||||
},
|
||||
...(customSections || []),
|
||||
]
|
||||
|
||||
// Filter out settings which shouldn't be rendered
|
||||
sections.forEach(section => {
|
||||
section.settings.forEach(setting => {
|
||||
setting.visible = canRenderControl(instance, setting, isScreen)
|
||||
})
|
||||
section.visible = section.settings.some(setting => setting.visible)
|
||||
})
|
||||
|
||||
return sections
|
||||
}
|
||||
|
||||
const updateSetting = async (key, value) => {
|
||||
|
@ -36,7 +46,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
const canRenderControl = (setting, isScreen) => {
|
||||
const canRenderControl = (instance, setting, isScreen) => {
|
||||
// Prevent rendering on click setting for screens
|
||||
if (setting?.type === "event" && isScreen) {
|
||||
return false
|
||||
|
@ -51,6 +61,7 @@
|
|||
if (setting.dependsOn) {
|
||||
let dependantSetting = setting.dependsOn
|
||||
let dependantValue = null
|
||||
let invert = !!setting.dependsOn.invert
|
||||
if (typeof setting.dependsOn === "object") {
|
||||
dependantSetting = setting.dependsOn.setting
|
||||
dependantValue = setting.dependsOn.value
|
||||
|
@ -62,7 +73,7 @@
|
|||
// If no specific value is depended upon, check if a value exists at all
|
||||
// for the dependent setting
|
||||
if (dependantValue == null) {
|
||||
const currentValue = componentInstance[dependantSetting]
|
||||
const currentValue = instance[dependantSetting]
|
||||
if (currentValue === false) {
|
||||
return false
|
||||
}
|
||||
|
@ -73,7 +84,11 @@
|
|||
}
|
||||
|
||||
// Otherwise check the value matches
|
||||
return componentInstance[dependantSetting] === dependantValue
|
||||
if (invert) {
|
||||
return instance[dependantSetting] !== dependantValue
|
||||
} else {
|
||||
return instance[dependantSetting] === dependantValue
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
|
@ -81,52 +96,54 @@
|
|||
</script>
|
||||
|
||||
{#each sections as section, idx (section.name)}
|
||||
<DetailSummary name={section.name} collapsible={false}>
|
||||
{#if idx === 0 && !componentInstance._component.endsWith("/layout") && !isScreen}
|
||||
<PropertyControl
|
||||
control={Input}
|
||||
label="Name"
|
||||
key="_instanceName"
|
||||
value={componentInstance._instanceName}
|
||||
onChange={val => updateSetting("_instanceName", val)}
|
||||
/>
|
||||
{/if}
|
||||
{#each section.settings as setting (setting.key)}
|
||||
{#if canRenderControl(setting, isScreen)}
|
||||
{#if section.visible}
|
||||
<DetailSummary name={section.name} collapsible={false}>
|
||||
{#if idx === 0 && !componentInstance._component.endsWith("/layout") && !isScreen}
|
||||
<PropertyControl
|
||||
type={setting.type}
|
||||
control={getComponentForSetting(setting)}
|
||||
label={setting.label}
|
||||
key={setting.key}
|
||||
value={componentInstance[setting.key]}
|
||||
defaultValue={setting.defaultValue}
|
||||
nested={setting.nested}
|
||||
onChange={val => updateSetting(setting.key, val)}
|
||||
highlighted={$store.highlightedSettingKey === setting.key}
|
||||
info={setting.info}
|
||||
props={{
|
||||
// Generic settings
|
||||
placeholder: setting.placeholder || null,
|
||||
|
||||
// Select settings
|
||||
options: setting.options || [],
|
||||
|
||||
// Number fields
|
||||
min: setting.min || null,
|
||||
max: setting.max || null,
|
||||
}}
|
||||
{bindings}
|
||||
{componentBindings}
|
||||
{componentInstance}
|
||||
{componentDefinition}
|
||||
control={Input}
|
||||
label="Name"
|
||||
key="_instanceName"
|
||||
value={componentInstance._instanceName}
|
||||
onChange={val => updateSetting("_instanceName", val)}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if idx === 0 && componentDefinition?.component?.endsWith("/fieldgroup")}
|
||||
<ResetFieldsButton {componentInstance} />
|
||||
{/if}
|
||||
{#if idx === 0 && componentDefinition?.block}
|
||||
<EjectBlockButton />
|
||||
{/if}
|
||||
</DetailSummary>
|
||||
{#each section.settings as setting (setting.key)}
|
||||
{#if setting.visible}
|
||||
<PropertyControl
|
||||
type={setting.type}
|
||||
control={getComponentForSetting(setting)}
|
||||
label={setting.label}
|
||||
key={setting.key}
|
||||
value={componentInstance[setting.key]}
|
||||
defaultValue={setting.defaultValue}
|
||||
nested={setting.nested}
|
||||
onChange={val => updateSetting(setting.key, val)}
|
||||
highlighted={$store.highlightedSettingKey === setting.key}
|
||||
info={setting.info}
|
||||
props={{
|
||||
// Generic settings
|
||||
placeholder: setting.placeholder || null,
|
||||
|
||||
// Select settings
|
||||
options: setting.options || [],
|
||||
|
||||
// Number fields
|
||||
min: setting.min || null,
|
||||
max: setting.max || null,
|
||||
}}
|
||||
{bindings}
|
||||
{componentBindings}
|
||||
{componentInstance}
|
||||
{componentDefinition}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if idx === 0 && componentDefinition?.component?.endsWith("/fieldgroup")}
|
||||
<ResetFieldsButton {componentInstance} />
|
||||
{/if}
|
||||
{#if idx === 0 && componentDefinition?.block}
|
||||
<EjectBlockButton />
|
||||
{/if}
|
||||
</DetailSummary>
|
||||
{/if}
|
||||
{/each}
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
"children": [
|
||||
"tableblock",
|
||||
"cardsblock",
|
||||
"repeaterblock"
|
||||
"repeaterblock",
|
||||
"formblock"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
let duplicateScreen = Helpers.cloneDeep(screen)
|
||||
delete duplicateScreen._id
|
||||
delete duplicateScreen._rev
|
||||
makeComponentUnique(duplicateScreen.props)
|
||||
duplicateScreen.props = makeComponentUnique(duplicateScreen.props)
|
||||
|
||||
// Attach the new name and URL
|
||||
duplicateScreen.routing.route = sanitizeUrl(screenUrl)
|
||||
|
|
|
@ -3845,7 +3845,7 @@
|
|||
"label": "Search Columns",
|
||||
"key": "searchColumns",
|
||||
"placeholder": "Choose search columns",
|
||||
"info": "Only the first 3 search columns will be used"
|
||||
"info": "Only the first 5 search columns will be used"
|
||||
},
|
||||
{
|
||||
"type": "filter",
|
||||
|
@ -4010,7 +4010,7 @@
|
|||
"label": "Search Columns",
|
||||
"key": "searchColumns",
|
||||
"placeholder": "Choose search columns",
|
||||
"info": "Only the first 3 search columns will be used"
|
||||
"info": "Only the first 5 search columns will be used"
|
||||
},
|
||||
{
|
||||
"type": "filter",
|
||||
|
@ -4395,5 +4395,145 @@
|
|||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"formblock": {
|
||||
"name": "Form Block",
|
||||
"icon": "Form",
|
||||
"styles": ["size"],
|
||||
"block": true,
|
||||
"info": "Form blocks are only compatible with internal or SQL tables",
|
||||
"settings": [
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Type",
|
||||
"key": "actionType",
|
||||
"options": ["Create", "Update", "View"],
|
||||
"defaultValue": "Create"
|
||||
},
|
||||
{
|
||||
"type": "table",
|
||||
"label": "Table",
|
||||
"key": "dataSource"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Row ID",
|
||||
"key": "rowId",
|
||||
"nested": true,
|
||||
"dependsOn": {
|
||||
"setting": "actionType",
|
||||
"value": "Create",
|
||||
"invert": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Title",
|
||||
"key": "title",
|
||||
"nested": true
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Size",
|
||||
"key": "size",
|
||||
"options": [
|
||||
{
|
||||
"label": "Medium",
|
||||
"value": "spectrum--medium"
|
||||
},
|
||||
{
|
||||
"label": "Large",
|
||||
"value": "spectrum--large"
|
||||
}
|
||||
],
|
||||
"defaultValue": "spectrum--medium"
|
||||
},
|
||||
{
|
||||
"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": "Above",
|
||||
"value": "above"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Disabled",
|
||||
"key": "disabled",
|
||||
"defaultValue": false,
|
||||
"dependsOn": {
|
||||
"setting": "actionType",
|
||||
"value": "View",
|
||||
"invert": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": true,
|
||||
"name": "Buttons",
|
||||
"settings": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Show save button",
|
||||
"key": "showSaveButton",
|
||||
"defaultValue": true,
|
||||
"dependsOn": {
|
||||
"setting": "actionType",
|
||||
"value": "View",
|
||||
"invert": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Show delete button",
|
||||
"key": "showDeleteButton",
|
||||
"defaultValue": false,
|
||||
"dependsOn": {
|
||||
"setting": "actionType",
|
||||
"value": "Update"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "url",
|
||||
"label": "Navigate after button press",
|
||||
"key": "actionUrl",
|
||||
"placeholder": "Choose a screen",
|
||||
"dependsOn": {
|
||||
"setting": "actionType",
|
||||
"value": "View",
|
||||
"invert": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"context": [
|
||||
{
|
||||
"type": "form",
|
||||
"suffix": "form"
|
||||
},
|
||||
{
|
||||
"type": "schema",
|
||||
"suffix": "repeater"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
239
packages/client/src/components/app/blocks/FormBlock.svelte
Normal file
239
packages/client/src/components/app/blocks/FormBlock.svelte
Normal file
|
@ -0,0 +1,239 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import BlockComponent from "../../BlockComponent.svelte"
|
||||
import Block from "../../Block.svelte"
|
||||
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
|
||||
export let actionUrl
|
||||
|
||||
const { fetchDatasourceSchema, builderStore } = getContext("sdk")
|
||||
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",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Navigate To",
|
||||
parameters: {
|
||||
url: actionUrl,
|
||||
},
|
||||
},
|
||||
]
|
||||
$: onDelete = [
|
||||
{
|
||||
"##eventHandlerType": "Delete Row",
|
||||
parameters: {
|
||||
confirm: true,
|
||||
tableId: dataSource?.tableId,
|
||||
rowId: `{{ ${safe(repeaterId)}.${safe("_id")} }}`,
|
||||
revId: `{{ ${safe(repeaterId)}.${safe("_rev")} }}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Screen Modal",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Navigate To",
|
||||
parameters: {
|
||||
url: actionUrl,
|
||||
},
|
||||
},
|
||||
]
|
||||
$: 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 !== "Create"
|
||||
? `{{ literal ${safe(providerId)} }}`
|
||||
: { rows: [{}] }
|
||||
$: renderDeleteButton = showDeleteButton && actionType === "Update"
|
||||
$: renderSaveButton = showSaveButton && actionType !== "View"
|
||||
$: renderButtons = renderDeleteButton || renderSaveButton
|
||||
$: renderHeader = renderButtons || title
|
||||
|
||||
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>
|
||||
{#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: actionType === "Create" ? "Create" : "Update",
|
||||
dataSource,
|
||||
size,
|
||||
disabled: disabled || actionType === "View",
|
||||
}}
|
||||
context="form"
|
||||
bind:id={formId}
|
||||
>
|
||||
<BlockComponent
|
||||
type="container"
|
||||
props={{
|
||||
direction: "column",
|
||||
hAlign: "stretch",
|
||||
vAlign: "top",
|
||||
gap: "M",
|
||||
}}
|
||||
>
|
||||
{#if renderHeader}
|
||||
<BlockComponent
|
||||
type="container"
|
||||
props={{
|
||||
direction: "row",
|
||||
hAlign: "stretch",
|
||||
vAlign: "center",
|
||||
gap: "M",
|
||||
wrap: true,
|
||||
}}
|
||||
order={0}
|
||||
>
|
||||
<BlockComponent
|
||||
type="heading"
|
||||
props={{ text: title || "" }}
|
||||
order={0}
|
||||
/>
|
||||
{#if renderButtons}
|
||||
<BlockComponent
|
||||
type="container"
|
||||
props={{
|
||||
direction: "row",
|
||||
hAlign: "stretch",
|
||||
vAlign: "center",
|
||||
gap: "M",
|
||||
wrap: true,
|
||||
}}
|
||||
order={1}
|
||||
>
|
||||
{#if renderDeleteButton}
|
||||
<BlockComponent
|
||||
type="button"
|
||||
props={{
|
||||
text: "Delete",
|
||||
onClick: onDelete,
|
||||
quiet: true,
|
||||
type: "secondary",
|
||||
}}
|
||||
order={0}
|
||||
/>
|
||||
{/if}
|
||||
{#if renderSaveButton}
|
||||
<BlockComponent
|
||||
type="button"
|
||||
props={{
|
||||
text: "Save",
|
||||
onClick: onSave,
|
||||
type: "cta",
|
||||
}}
|
||||
order={1}
|
||||
/>
|
||||
{/if}
|
||||
</BlockComponent>
|
||||
{/if}
|
||||
</BlockComponent>
|
||||
{/if}
|
||||
<BlockComponent
|
||||
type="fieldgroup"
|
||||
props={{ labelPosition }}
|
||||
order={1}
|
||||
>
|
||||
{#each fields as field, idx}
|
||||
{#if getComponentForField(field)}
|
||||
<BlockComponent
|
||||
type={getComponentForField(field)}
|
||||
props={{
|
||||
field,
|
||||
label: field,
|
||||
placeholder: field,
|
||||
disabled,
|
||||
}}
|
||||
order={idx}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
</BlockComponent>
|
||||
</BlockComponent>
|
||||
</BlockComponent>
|
||||
</BlockComponent>
|
||||
</BlockComponent>
|
||||
{:else}
|
||||
<Placeholder
|
||||
text="Choose your table and add some fields to your form to get started"
|
||||
/>
|
||||
{/if}
|
||||
</Block>
|
|
@ -1,3 +1,4 @@
|
|||
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"
|
||||
|
|
|
@ -48,36 +48,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
|
||||
}
|
||||
|
@ -95,7 +66,7 @@
|
|||
|
||||
$: initialValues = getInitialValues(actionType, dataSource, $context)
|
||||
$: resetKey = Helpers.hashString(
|
||||
JSON.stringify(initialValues) + JSON.stringify(schema)
|
||||
JSON.stringify(initialValues) + JSON.stringify(schema) + disabled
|
||||
)
|
||||
</script>
|
||||
|
||||
|
|
|
@ -16,7 +16,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,
|
||||
|
@ -34,7 +34,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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue