commit
354817dbf3
|
@ -1,5 +1,5 @@
|
||||||
import filterTests from "../support/filterTests"
|
import filterTests from "../support/filterTests"
|
||||||
const interact = require('../support/interact')
|
const interact = require("../support/interact")
|
||||||
|
|
||||||
filterTests(["smoke", "all"], () => {
|
filterTests(["smoke", "all"], () => {
|
||||||
context("Query Level Transformers", () => {
|
context("Query Level Transformers", () => {
|
||||||
|
|
|
@ -185,43 +185,42 @@ export const makeComponentUnique = component => {
|
||||||
// Replace component ID
|
// Replace component ID
|
||||||
const oldId = component._id
|
const oldId = component._id
|
||||||
const newId = Helpers.uuid()
|
const newId = Helpers.uuid()
|
||||||
component._id = newId
|
let definition = JSON.stringify(component)
|
||||||
|
|
||||||
if (component._children?.length) {
|
// Replace all instances of this ID in HBS bindings
|
||||||
let children = JSON.stringify(component._children)
|
definition = definition.replace(new RegExp(oldId, "g"), newId)
|
||||||
|
|
||||||
// Replace all instances of this ID in child HBS bindings
|
// Replace all instances of this ID in JS bindings
|
||||||
children = children.replace(new RegExp(oldId, "g"), newId)
|
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
|
// Check if this is a valid JS binding
|
||||||
const bindings = findHBSBlocks(children)
|
let js = decodeJSBinding(sanitizedBinding)
|
||||||
bindings.forEach(binding => {
|
if (js != null) {
|
||||||
// JSON.stringify will have escaped double quotes, so we need
|
// Replace ID inside JS binding
|
||||||
// to account for that
|
js = js.replace(new RegExp(oldId, "g"), newId)
|
||||||
let sanitizedBinding = binding.replace(/\\"/g, '"')
|
|
||||||
|
|
||||||
// Check if this is a valid JS binding
|
// Create new valid JS binding
|
||||||
let js = decodeJSBinding(sanitizedBinding)
|
let newBinding = encodeJSBinding(js)
|
||||||
if (js != null) {
|
|
||||||
// Replace ID inside JS binding
|
|
||||||
js = js.replace(new RegExp(oldId, "g"), newId)
|
|
||||||
|
|
||||||
// Create new valid JS binding
|
// Replace escaped double quotes
|
||||||
let newBinding = encodeJSBinding(js)
|
newBinding = newBinding.replace(/"/g, '\\"')
|
||||||
|
|
||||||
// Replace escaped double quotes
|
// Insert new JS back into binding.
|
||||||
newBinding = newBinding.replace(/"/g, '\\"')
|
// 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.
|
// Recurse on all children
|
||||||
// A single string replace here is better than a regex as
|
component = JSON.parse(definition)
|
||||||
// the binding contains special characters, and we only need
|
return {
|
||||||
// to replace a single instance.
|
...component,
|
||||||
children = children.replace(binding, newBinding)
|
_children: component._children?.map(makeComponentUnique),
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Recurse on all children
|
|
||||||
component._children = JSON.parse(children)
|
|
||||||
component._children.forEach(makeComponentUnique)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -169,7 +169,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 []
|
||||||
}
|
}
|
||||||
|
@ -177,7 +182,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)
|
||||||
path.pop()
|
if (!options?.includeSelf) {
|
||||||
|
path.pop()
|
||||||
|
}
|
||||||
|
|
||||||
// Filter by only data provider components
|
// Filter by only data provider components
|
||||||
return path.filter(component => {
|
return path.filter(component => {
|
||||||
|
@ -798,6 +805,17 @@ export 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/")
|
||||||
|
|
|
@ -621,7 +621,7 @@ export const getFrontendStore = () => {
|
||||||
|
|
||||||
// Make new component unique if copying
|
// Make new component unique if copying
|
||||||
if (!cut) {
|
if (!cut) {
|
||||||
makeComponentUnique(componentToPaste)
|
componentToPaste = makeComponentUnique(componentToPaste)
|
||||||
}
|
}
|
||||||
newComponentId = componentToPaste._id
|
newComponentId = componentToPaste._id
|
||||||
|
|
||||||
|
@ -931,7 +931,7 @@ export const getFrontendStore = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace block with ejected definition
|
// Replace block with ejected definition
|
||||||
makeComponentUnique(ejectedDefinition)
|
ejectedDefinition = makeComponentUnique(ejectedDefinition)
|
||||||
const index = parent._children.findIndex(x => x._id === componentId)
|
const index = parent._children.findIndex(x => x._id === componentId)
|
||||||
parent._children[index] = ejectedDefinition
|
parent._children[index] = ejectedDefinition
|
||||||
nextSelectedComponentId = ejectedDefinition._id
|
nextSelectedComponentId = ejectedDefinition._id
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
$: showAvailableActions = !actions?.length
|
$: showAvailableActions = !actions?.length
|
||||||
|
|
||||||
|
@ -187,6 +188,7 @@
|
||||||
this={selectedActionComponent}
|
this={selectedActionComponent}
|
||||||
parameters={selectedAction.parameters}
|
parameters={selectedAction.parameters}
|
||||||
bindings={allBindings}
|
bindings={allBindings}
|
||||||
|
{nested}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/key}
|
{/key}
|
||||||
|
|
|
@ -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
|
||||||
|
@ -90,6 +91,7 @@
|
||||||
eventType={name}
|
eventType={name}
|
||||||
{bindings}
|
{bindings}
|
||||||
{key}
|
{key}
|
||||||
|
{nested}
|
||||||
/>
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -95,6 +95,7 @@
|
||||||
bindings={allBindings}
|
bindings={allBindings}
|
||||||
name={key}
|
name={key}
|
||||||
text={label}
|
text={label}
|
||||||
|
{nested}
|
||||||
{key}
|
{key}
|
||||||
{type}
|
{type}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
export let bindings
|
export let bindings
|
||||||
|
export let placeholder
|
||||||
|
|
||||||
$: urlOptions = $store.screens
|
$: urlOptions = $store.screens
|
||||||
.map(screen => screen.routing?.route)
|
.map(screen => screen.routing?.route)
|
||||||
|
@ -13,6 +14,7 @@
|
||||||
<DrawerBindableCombobox
|
<DrawerBindableCombobox
|
||||||
{value}
|
{value}
|
||||||
{bindings}
|
{bindings}
|
||||||
|
{placeholder}
|
||||||
on:change
|
on:change
|
||||||
options={urlOptions}
|
options={urlOptions}
|
||||||
appendBindingsAsOptions={false}
|
appendBindingsAsOptions={false}
|
||||||
|
|
|
@ -13,19 +13,29 @@
|
||||||
export let componentBindings
|
export let componentBindings
|
||||||
export let isScreen = false
|
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 settings = definition?.settings ?? []
|
||||||
const generalSettings = settings.filter(setting => !setting.section)
|
const generalSettings = settings.filter(setting => !setting.section)
|
||||||
const customSections = settings.filter(setting => setting.section)
|
const customSections = settings.filter(setting => setting.section)
|
||||||
return [
|
let sections = [
|
||||||
{
|
{
|
||||||
name: "General",
|
name: "General",
|
||||||
settings: generalSettings,
|
settings: generalSettings,
|
||||||
},
|
},
|
||||||
...(customSections || []),
|
...(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) => {
|
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
|
// Prevent rendering on click setting for screens
|
||||||
if (setting?.type === "event" && isScreen) {
|
if (setting?.type === "event" && isScreen) {
|
||||||
return false
|
return false
|
||||||
|
@ -51,6 +61,7 @@
|
||||||
if (setting.dependsOn) {
|
if (setting.dependsOn) {
|
||||||
let dependantSetting = setting.dependsOn
|
let dependantSetting = setting.dependsOn
|
||||||
let dependantValue = null
|
let dependantValue = null
|
||||||
|
let invert = !!setting.dependsOn.invert
|
||||||
if (typeof setting.dependsOn === "object") {
|
if (typeof setting.dependsOn === "object") {
|
||||||
dependantSetting = setting.dependsOn.setting
|
dependantSetting = setting.dependsOn.setting
|
||||||
dependantValue = setting.dependsOn.value
|
dependantValue = setting.dependsOn.value
|
||||||
|
@ -62,7 +73,7 @@
|
||||||
// If no specific value is depended upon, check if a value exists at all
|
// If no specific value is depended upon, check if a value exists at all
|
||||||
// for the dependent setting
|
// for the dependent setting
|
||||||
if (dependantValue == null) {
|
if (dependantValue == null) {
|
||||||
const currentValue = componentInstance[dependantSetting]
|
const currentValue = instance[dependantSetting]
|
||||||
if (currentValue === false) {
|
if (currentValue === false) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -73,7 +84,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise check the value matches
|
// Otherwise check the value matches
|
||||||
return componentInstance[dependantSetting] === dependantValue
|
if (invert) {
|
||||||
|
return instance[dependantSetting] !== dependantValue
|
||||||
|
} else {
|
||||||
|
return instance[dependantSetting] === dependantValue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
@ -81,52 +96,54 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each sections as section, idx (section.name)}
|
{#each sections as section, idx (section.name)}
|
||||||
<DetailSummary name={section.name} collapsible={false}>
|
{#if section.visible}
|
||||||
{#if idx === 0 && !componentInstance._component.endsWith("/layout") && !isScreen}
|
<DetailSummary name={section.name} collapsible={false}>
|
||||||
<PropertyControl
|
{#if idx === 0 && !componentInstance._component.endsWith("/layout") && !isScreen}
|
||||||
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)}
|
|
||||||
<PropertyControl
|
<PropertyControl
|
||||||
type={setting.type}
|
control={Input}
|
||||||
control={getComponentForSetting(setting)}
|
label="Name"
|
||||||
label={setting.label}
|
key="_instanceName"
|
||||||
key={setting.key}
|
value={componentInstance._instanceName}
|
||||||
value={componentInstance[setting.key]}
|
onChange={val => updateSetting("_instanceName", val)}
|
||||||
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}
|
{/if}
|
||||||
{/each}
|
{#each section.settings as setting (setting.key)}
|
||||||
{#if idx === 0 && componentDefinition?.component?.endsWith("/fieldgroup")}
|
{#if setting.visible}
|
||||||
<ResetFieldsButton {componentInstance} />
|
<PropertyControl
|
||||||
{/if}
|
type={setting.type}
|
||||||
{#if idx === 0 && componentDefinition?.block}
|
control={getComponentForSetting(setting)}
|
||||||
<EjectBlockButton />
|
label={setting.label}
|
||||||
{/if}
|
key={setting.key}
|
||||||
</DetailSummary>
|
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}
|
{/each}
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
"children": [
|
"children": [
|
||||||
"tableblock",
|
"tableblock",
|
||||||
"cardsblock",
|
"cardsblock",
|
||||||
"repeaterblock"
|
"repeaterblock",
|
||||||
|
"formblock"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
let duplicateScreen = Helpers.cloneDeep(screen)
|
let duplicateScreen = Helpers.cloneDeep(screen)
|
||||||
delete duplicateScreen._id
|
delete duplicateScreen._id
|
||||||
delete duplicateScreen._rev
|
delete duplicateScreen._rev
|
||||||
makeComponentUnique(duplicateScreen.props)
|
duplicateScreen.props = makeComponentUnique(duplicateScreen.props)
|
||||||
|
|
||||||
// Attach the new name and URL
|
// Attach the new name and URL
|
||||||
duplicateScreen.routing.route = sanitizeUrl(screenUrl)
|
duplicateScreen.routing.route = sanitizeUrl(screenUrl)
|
||||||
|
|
|
@ -3845,7 +3845,7 @@
|
||||||
"label": "Search Columns",
|
"label": "Search Columns",
|
||||||
"key": "searchColumns",
|
"key": "searchColumns",
|
||||||
"placeholder": "Choose search columns",
|
"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",
|
"type": "filter",
|
||||||
|
@ -4010,7 +4010,7 @@
|
||||||
"label": "Search Columns",
|
"label": "Search Columns",
|
||||||
"key": "searchColumns",
|
"key": "searchColumns",
|
||||||
"placeholder": "Choose search columns",
|
"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",
|
"type": "filter",
|
||||||
|
@ -4395,5 +4395,145 @@
|
||||||
"required": true
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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 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"
|
||||||
|
|
|
@ -48,36 +48,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
|
||||||
}
|
}
|
||||||
|
@ -95,7 +66,7 @@
|
||||||
|
|
||||||
$: initialValues = getInitialValues(actionType, dataSource, $context)
|
$: initialValues = getInitialValues(actionType, dataSource, $context)
|
||||||
$: resetKey = Helpers.hashString(
|
$: resetKey = Helpers.hashString(
|
||||||
JSON.stringify(initialValues) + JSON.stringify(schema)
|
JSON.stringify(initialValues) + JSON.stringify(schema) + disabled
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,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,
|
||||||
|
@ -34,7 +34,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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue