1
0
Fork 0
mirror of synced 2024-07-06 15:00:49 +12:00

List refinement, Form Block UX updates for action type. Bug fixes for FormBlock bindings. TableBlock UX updates and Component Setting updates

This commit is contained in:
Dean 2023-08-24 14:39:53 +01:00
parent 046ef853e3
commit 1ec2faf74d
21 changed files with 666 additions and 485 deletions

View file

@ -23,6 +23,9 @@
export let animate = true
export let customZindex
export let showPopover = true
export let clickOutsideOverride = false
$: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum"
export const show = () => {
@ -36,6 +39,9 @@
}
const handleOutsideClick = e => {
if (clickOutsideOverride) {
return
}
if (open) {
// Stop propagation if the source is the anchor
let node = e.target
@ -54,6 +60,9 @@
}
function handleEscape(e) {
if (!clickOutsideOverride) {
return
}
if (open && e.key === "Escape") {
hide()
}
@ -79,6 +88,7 @@
on:keydown={handleEscape}
class="spectrum-Popover is-open"
class:customZindex
class:hide-popover={open && !showPopover}
role="presentation"
style="height: {customHeight}; --customZindex: {customZindex};"
transition:fly|local={{ y: -20, duration: animate ? 200 : 0 }}
@ -89,6 +99,10 @@
{/if}
<style>
.hide-popover {
display: contents;
}
.spectrum-Popover {
min-width: var(--spectrum-global-dimension-size-2000);
border-color: var(--spectrum-global-color-gray-300);

View file

@ -22,6 +22,10 @@ import { TableNames } from "../constants"
import { JSONUtils } from "@budibase/frontend-core"
import ActionDefinitions from "components/design/settings/controls/ButtonActionEditor/manifest.json"
import { environment, licensing } from "stores/portal"
import {
convertOldFieldFormat,
getComponentForField,
} from "components/design/settings/controls/FieldConfiguration/utils"
// Regex to match all instances of template strings
const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
@ -328,7 +332,7 @@ const getProviderContextBindings = (asset, dataProviders) => {
if (context.type === "form") {
// Forms do not need table schemas
// Their schemas are built from their component field names
schema = buildFormSchema(component)
schema = buildFormSchema(component, asset)
readablePrefix = "Fields"
} else if (context.type === "static") {
// Static contexts are fully defined by the components
@ -370,6 +374,11 @@ const getProviderContextBindings = (asset, dataProviders) => {
if (runtimeSuffix) {
providerId += `-${runtimeSuffix}`
}
if (!filterCategoryByContext(component, context)) {
return
}
const safeComponentId = makePropSafe(providerId)
// Create bindable properties for each schema field
@ -387,6 +396,12 @@ const getProviderContextBindings = (asset, dataProviders) => {
}
readableBinding += `.${fieldSchema.name || key}`
const bindingCategory = getComponentBindingCategory(
component,
context,
def
)
// Create the binding object
bindings.push({
type: "context",
@ -399,8 +414,8 @@ const getProviderContextBindings = (asset, dataProviders) => {
// Table ID is used by JSON fields to know what table the field is in
tableId: table?._id,
component: component._component,
category: component._instanceName,
icon: def.icon,
category: bindingCategory.category,
icon: bindingCategory.icon,
display: {
name: fieldSchema.name || key,
type: fieldSchema.type,
@ -413,6 +428,40 @@ const getProviderContextBindings = (asset, dataProviders) => {
return bindings
}
// Exclude a data context based on the component settings
const filterCategoryByContext = (component, context) => {
const { _component } = component
if (_component.endsWith("formblock")) {
if (
(component.actionType == "Create" && context.type === "schema") ||
(component.actionType == "View" && context.type === "form")
) {
return false
}
}
return true
}
const getComponentBindingCategory = (component, context, def) => {
let icon = def.icon
let category = component._instanceName
if (component._component.endsWith("formblock")) {
let contextCategorySuffix = {
form: "Fields",
schema: "Row",
}
category = `${component._instanceName} - ${
contextCategorySuffix[context.type]
}`
icon = context.type === "form" ? "Form" : "Data"
}
return {
icon,
category,
}
}
/**
* Gets all bindable properties from the logged in user.
*/
@ -507,6 +556,7 @@ const getSelectedRowsBindings = asset => {
)}.${makePropSafe("selectedRows")}`,
readableBinding: `${block._instanceName}.Selected rows`,
category: "Selected rows",
icon: "ViewRow",
display: { name: block._instanceName },
}))
)
@ -835,18 +885,36 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
* Builds a form schema given a form component.
* A form schema is a schema of all the fields nested anywhere within a form.
*/
export const buildFormSchema = component => {
export const buildFormSchema = (component, asset) => {
let schema = {}
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" }
})
const datasource = getDatasourceForProvider(asset, component)
const info = getSchemaForDatasource(component, datasource)
if (!component.fields) {
Object.values(info?.schema)
.filter(
({ autocolumn, name }) =>
!autocolumn && !["_rev", "_id"].includes(name)
)
.forEach(({ name }) => {
schema[name] = { type: info?.schema[name].type }
})
} else {
// Field conversion
const patched = convertOldFieldFormat(component.fields || [])
patched?.forEach(({ field, active }) => {
if (!active) return
schema[field] = { type: info?.schema[field].type }
})
}
return schema
}
@ -862,7 +930,7 @@ export const buildFormSchema = component => {
}
}
component._children?.forEach(child => {
const childSchema = buildFormSchema(child)
const childSchema = buildFormSchema(child, asset)
schema = { ...schema, ...childSchema }
})
return schema

View file

@ -93,40 +93,6 @@ const INITIAL_FRONTEND_STATE = {
tourNodes: null,
}
export const updateComponentSetting = (name, value) => {
return component => {
if (!name || !component) {
return false
}
// Skip update if the value is the same
if (component[name] === value) {
return false
}
const settings = getComponentSettings(component._component)
const updatedSetting = settings.find(setting => setting.key === name)
if (
updatedSetting?.type === "dataSource" ||
updatedSetting?.type === "table"
) {
const { schema } = getSchemaForDatasource(null, value)
const columnNames = Object.keys(schema || {})
const multifieldKeysToSelectAll = settings
.filter(setting => {
return setting.type === "multifield" && setting.selectAllFields
})
.map(setting => setting.key)
multifieldKeysToSelectAll.forEach(key => {
component[key] = columnNames
})
}
component[name] = value
}
}
export const getFrontendStore = () => {
const store = writable({ ...INITIAL_FRONTEND_STATE })
let websocket
@ -145,18 +111,14 @@ export const getFrontendStore = () => {
}
let clone = cloneDeep(screen)
const result = patchFn(clone)
console.log("sequentialScreenPatch ", result)
if (result === false) {
return
}
return
//return await store.actions.screens.save(clone)
return await store.actions.screens.save(clone)
})
store.actions = {
tester: (name, value) => {
return updateComponentSetting(name, value)
},
reset: () => {
store.set({ ...INITIAL_FRONTEND_STATE })
websocket?.disconnect()
@ -864,7 +826,6 @@ export const getFrontendStore = () => {
},
patch: async (patchFn, componentId, screenId) => {
// Use selected component by default
console.log("front end patch")
if (!componentId || !screenId) {
const state = get(store)
componentId = componentId || state.selectedComponentId
@ -883,18 +844,6 @@ export const getFrontendStore = () => {
}
await store.actions.screens.patch(patchScreen, screenId)
},
// Temporary
customPatch: async (patchFn, componentId, screenId) => {
console.log("patchUpdate :")
if (!componentId || !screenId) {
const state = get(store)
componentId = componentId || state.selectedComponentId
screenId = screenId || state.selectedScreenId
}
if (!componentId || !screenId || !patchFn) {
return
}
},
delete: async component => {
if (!component) {
return
@ -1261,9 +1210,41 @@ export const getFrontendStore = () => {
},
updateSetting: async (name, value) => {
await store.actions.components.patch(
updateComponentSetting(name, value)
store.actions.components.updateComponentSetting(name, value)
)
},
updateComponentSetting: (name, value) => {
return component => {
if (!name || !component) {
return false
}
// Skip update if the value is the same
if (component[name] === value) {
return false
}
const settings = getComponentSettings(component._component)
const updatedSetting = settings.find(setting => setting.key === name)
if (
updatedSetting?.type === "dataSource" ||
updatedSetting?.type === "table"
) {
const { schema } = getSchemaForDatasource(null, value)
const columnNames = Object.keys(schema || {})
const multifieldKeysToSelectAll = settings
.filter(setting => {
return setting.type === "multifield" && setting.selectAllFields
})
.map(setting => setting.key)
multifieldKeysToSelectAll.forEach(key => {
component[key] = columnNames
})
}
component[name] = value
}
},
requestEjectBlock: componentId => {
store.actions.preview.sendEvent("eject-block", componentId)
},

View file

@ -74,6 +74,8 @@
{/if}
</div>
<Drawer
on:drawerHide
on:drawerShow
{fillWidth}
bind:this={bindingDrawer}
{title}

View file

@ -74,7 +74,7 @@
<ActionButton on:click={openDrawer}>{actionText}</ActionButton>
</div>
<Drawer bind:this={drawer} title={"Actions"}>
<Drawer bind:this={drawer} title={"Actions"} on:drawerHide on:drawerShow>
<svelte:fragment slot="description">
Define what actions to run.
</svelte:fragment>

View file

@ -1,82 +1,88 @@
<script>
import { Toggle, Icon } from "@budibase/bbui"
import { Icon } from "@budibase/bbui"
import { dndzone } from "svelte-dnd-action"
import { flip } from "svelte/animate"
import { createEventDispatcher } from "svelte"
import { generate } from "shortid"
export let value = []
export let items = []
export let showHandle = true
export let rightButton
export let rightProps = {}
export let highlighted
export let listType
export let listTypeProps = {}
export let listItemKey
export let draggable = true
const dispatch = createEventDispatcher()
const flipDurationMs = 150
let dragDisabled = false
let listOptions = [...value]
let anchors = {}
const updateColumnOrder = e => {
listOptions = e.detail.items
let anchors = {}
let draggableItems = []
const buildDragable = items => {
return items.map(item => {
return {
id: listItemKey ? item[listItemKey] : generate(),
item,
}
})
}
$: if (items) {
draggableItems = buildDragable(items)
}
const updateRowOrder = e => {
draggableItems = e.detail.items
}
const serialiseUpdate = () => {
return draggableItems.reduce((acc, ele) => {
acc.push(ele.item)
return acc
}, [])
}
const handleFinalize = e => {
updateColumnOrder(e)
dispatch("change", listOptions)
dragDisabled = false
updateRowOrder(e)
dispatch("change", serialiseUpdate())
}
// This is optional and should be moved.
const onToggle = item => {
return e => {
console.log(`${item.name} toggled: ${e.detail}`)
item.active = e.detail
dispatch("change", listOptions)
}
const onItemChanged = e => {
dispatch("itemChange", e.detail)
}
</script>
<ul
class="list-wrap"
use:dndzone={{
items: listOptions,
items: draggableItems,
flipDurationMs,
dropTargetStyle: { outline: "none" },
dragDisabled,
dragDisabled: !draggable,
}}
on:finalize={handleFinalize}
on:consider={updateColumnOrder}
on:consider={updateRowOrder}
>
{#each listOptions as item (item.id)}
{#each draggableItems as draggable (draggable.id)}
<li
animate:flip={{ duration: flipDurationMs }}
bind:this={anchors[item.id]}
bind:this={anchors[draggable.id]}
class:highlighted={draggable.id === highlighted}
>
<div class="left-content">
{#if showHandle}
<div
class="handle"
aria-label="drag-handle"
style={dragDisabled ? "cursor: grab" : "cursor: grabbing"}
>
<div class="handle" aria-label="drag-handle">
<Icon name="DragHandle" size="XL" />
</div>
{/if}
<!-- slot - left action -->
<Toggle on:change={onToggle(item)} text="" value={item.active} thin />
{item.name}
</div>
<!-- slot - right action -->
<div class="right-content">
{#if rightButton}
<svelte:component
this={rightButton}
anchor={anchors[item.id]}
field={item}
componentBindings={rightProps.componentBindings}
bindings={rightProps.bindings}
parent={rightProps.parent}
/>
{/if}
<svelte:component
this={listType}
anchor={anchors[draggable.item._id]}
item={draggable.item}
{...listTypeProps}
on:change={onItemChanged}
/>
</div>
</li>
{/each}
@ -104,7 +110,6 @@
transition: background-color ease-in-out 130ms;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
}
@ -122,12 +127,15 @@
border-top-left-radius: var(--spectrum-table-regular-border-radius);
border-top-right-radius: var(--spectrum-table-regular-border-radius);
}
.left-content {
display: flex;
align-items: center;
.right-content {
flex: 1;
min-width: 0;
}
.list-wrap li {
padding-left: var(--spacing-s);
padding-right: var(--spacing-s);
}
li.highlighted {
background-color: pink;
}
</style>

View file

@ -1,86 +1,58 @@
<!-- FormBlockFieldSettingsPopover -->
<script>
import { Icon, Popover, Layout } from "@budibase/bbui"
import { store } from "builderStore"
import { cloneDeep } from "lodash/fp"
import { createEventDispatcher } from "svelte"
import ComponentSettingsSection from "../../../../../pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentSettingsSection.svelte"
export let anchor
export let field
export let componentBindings
export let bindings
export let parent
const dispatch = createEventDispatcher()
let popover
let drawers = []
let sudoComponentInstance
$: sudoComponentInstance = buildSudoInstance(field)
$: componentDef = store.actions.components.getDefinition(field._component)
$: if (field) {
sudoComponentInstance = field
}
$: componentDef = store.actions.components.getDefinition(
sudoComponentInstance._component
)
$: parsedComponentDef = processComponentDefinitionSettings(componentDef)
const buildSudoInstance = instance => {
let clone = cloneDeep(instance)
// only do this IF necessary
const instanceCheck = store.actions.components.createInstance(
clone._component,
{
_instanceName: instance.displayName,
field: instance.name, //Must be fixed
label: instance.displayName,
placeholder: instance.displayName,
},
{} //?
)
// mutating on load would achieve this.
// Would need to replace the entire config at this point
// console.log(instanceCheck)
return instanceCheck
}
// Ensures parent bindings are pushed down
// Confirm this
const processComponentDefinitionSettings = componentDef => {
if (!componentDef) {
return {}
}
const clone = cloneDeep(componentDef)
clone.settings.forEach(setting => {
if (setting.type === "text") {
setting.nested = true
}
})
const updatedSettings = clone.settings
.filter(setting => setting.key !== "field")
.map(setting => {
return { ...setting, nested: true }
})
clone.settings = updatedSettings
return clone
}
// Current core update setting fn
const updateSetting = async (setting, value) => {
console.log("Custom Save Setting", setting, value)
console.log("The parent", parent)
const nestedComponentInstance = cloneDeep(sudoComponentInstance)
//updateBlockFieldSetting in frontend?
const nestedFieldInstance = cloneDeep(sudoComponentInstance)
// Parse the current fields on load and check for unbuilt instances
// This is icky
let parentFieldsSettings = parent.fields.find(
pSetting => pSetting.name === nestedFieldInstance.field
const patchFn = store.actions.components.updateComponentSetting(
setting.key,
value
)
patchFn(nestedComponentInstance)
//In this scenario it may be best to extract
store.actions.tester(setting.key, value)(nestedFieldInstance) //mods the internal val
const update = {
...nestedComponentInstance,
active: sudoComponentInstance.active,
}
parentFieldsSettings = cloneDeep(nestedFieldInstance)
console.log("UPDATED nestedFieldInstance", nestedFieldInstance)
//Overwrite all the fields
await store.actions.components.updateSetting("fields", parent.fields)
/*
ignore/disabled _instanceName > this will be handled in the new header field.
ignore/disabled field > this should be populated and hidden.
*/
dispatch("change", update)
}
</script>
@ -93,69 +65,60 @@
}}
/>
<Popover bind:this={popover} {anchor} align="left-outside">
<Layout noPadding>
<!--
property-group-container - has a border, is there a scenario where it doesnt render?
FormBlock Default behaviour.
validation: field.validation,
field: field.name,
label: field.displayName,
placeholder: field.displayName,
Block differences
_instanceName:
Filtered as it has been moved to own area.
field:
Fixed - not visible.
componentBindings
These appear to be removed/invalid
Bindings
{bindings} - working
<Popover
bind:this={popover}
on:open={() => {
drawers = []
}}
{anchor}
align="left-outside"
showPopover={drawers.length == 0}
clickOutsideOverride={drawers.length > 0}
>
<span class="popover-wrap">
<Layout noPadding noGap>
<div class="type-icon">
<Icon name={parsedComponentDef.icon} />
<span>{parsedComponentDef.name}</span>
</div>
<ComponentSettingsSection
componentInstance={sudoComponentInstance}
componentDefinition={parsedComponentDef}
isScreen={false}
onUpdateSetting={updateSetting}
showSectionTitle={false}
showInstanceName={false}
{bindings}
{componentBindings}
componentdefinition.settings[x].nested needs to be true
Are these appropriate for the form block
FormBlock will have to pull the settings from fields:[]
Frontend Store > updateSetting: async (name, value)
Performs a patch for the component settings change
PropertyControl
Would this behaviour require a flag?
highlighted={$store.highlightedSettingKey === setting.key}
propertyFocus={$store.propertyFocus === setting.key}
Mode filtering of fields
Create
Update
View > do we filter fields here or disable them?
Default value?? Makes no sense
Drawer actions
CRUD - how to persist to the correct location?
Its just not a thing now
- Validation
- Bindings
- Custom options.
** Options source - should this be shaped too?
Schema,
Datasource
Custom
-->
<ComponentSettingsSection
componentInstance={sudoComponentInstance}
componentDefinition={parsedComponentDef}
isScreen={false}
{bindings}
{componentBindings}
onUpdateSetting={updateSetting}
/>
</Layout>
on:drawerShow={e => {
drawers = [...drawers, e.detail]
}}
on:drawerHide={e => {
drawers = drawers.slice(0, -1)
}}
/>
</Layout>
</span>
</Popover>
<style>
.popover-wrap {
background-color: var(--spectrum-alias-background-color-secondary);
}
.type-icon {
display: flex;
gap: var(--spacing-m);
margin: var(--spacing-xl);
margin-bottom: 0px;
height: var(--spectrum-alias-item-height-m);
padding: 0px var(--spectrum-alias-item-padding-m);
border-width: var(--spectrum-actionbutton-border-size);
border-radius: var(--spectrum-alias-border-radius-regular);
border: 1px solid
var(
--spectrum-actionbutton-m-border-color,
var(--spectrum-alias-border-color)
);
align-items: center;
}
</style>

View file

@ -1,127 +1,76 @@
<script>
import { cloneDeep } from "lodash/fp"
import { generate } from "shortid"
import { cloneDeep, isEqual } from "lodash/fp"
import {
getDatasourceForProvider,
getSchemaForDatasource,
} from "builderStore/dataBinding"
import { currentAsset } from "builderStore"
import SettingsList from "../SettingsList.svelte"
import { createEventDispatcher } from "svelte"
import { store, selectedScreen } from "builderStore"
import {
getBindableProperties,
getComponentBindableProperties,
} from "builderStore/dataBinding"
import EditFieldPopover from "./EditFieldPopover.svelte"
import { currentAsset } from "builderStore"
import DraggableList from "../DraggableList.svelte"
import { createEventDispatcher } from "svelte"
import { store, selectedScreen } from "builderStore"
import FieldSetting from "./FieldSetting.svelte"
import { convertOldFieldFormat, getComponentForField } from "./utils"
export let componentInstance
export let value
const dispatch = createEventDispatcher()
let sanitisedFields
let fieldList
let schema
// let assetIdCache
let cachedValue
// $: value, console.log("VALUE UPDATED")
// $: $currentAsset, console.log("currentAsset updated ", $currentAsset)
// Dean - From the inner form block - make a util
const FieldTypeToComponentMap = {
string: "stringfield",
number: "numberfield",
bigint: "bigintfield",
options: "optionsfield",
array: "multifieldselect",
boolean: "booleanfield",
longform: "longformfield",
datetime: "datetimefield",
attachment: "attachmentfield",
link: "relationshipfield",
json: "jsonfield",
barcodeqr: "codescanner",
$: bindings = getBindableProperties($selectedScreen, componentInstance._id)
$: actionType = componentInstance.actionType
let componentBindings = []
$: if (actionType) {
componentBindings = getComponentBindableProperties(
$selectedScreen,
componentInstance._id
)
}
// Dean - From the inner form block - make a util
const getComponentForField = field => {
if (!field || !schema?.[field]) {
return null
}
const type = schema[field].type
return FieldTypeToComponentMap[type]
}
let fieldConfigList
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
$: schema = getSchema($currentAsset, datasource)
$: if (!isEqual(value, cachedValue)) {
cachedValue = value
schema = getSchema($currentAsset, datasource)
}
$: options = Object.keys(schema || {})
$: sanitisedValue = getValidColumns(convertOldFieldFormat(value), options)
$: updateBoundValue(sanitisedValue)
$: updateSanitsedFields(sanitisedValue)
$: fieldConfigList.forEach(column => {
if (!column.id) {
column.id = generate()
}
})
$: bindings = getBindableProperties(
$selectedScreen,
$store.selectedComponentId
)
$: console.log("bindings ", bindings)
$: componentBindings = getComponentBindableProperties(
$selectedScreen,
$store.selectedComponentId
)
$: console.log("componentBindings ", componentBindings)
$: unconfigured = buildUnconfiguredOptions(schema, sanitisedFields)
// Builds unused ones only
const buildListOptions = (schema, selected) => {
const buildUnconfiguredOptions = (schema, selected) => {
if (!schema) {
return []
}
let schemaClone = cloneDeep(schema)
selected.forEach(val => {
delete schemaClone[val.name]
delete schemaClone[val.field]
})
return Object.keys(schemaClone)
.filter(key => !schemaClone[key].autocolumn)
.map(key => {
const col = schemaClone[key]
let toggleOn = !value
return {
name: key,
displayName: key,
id: generate(),
active: typeof col.active != "boolean" ? !value : col.active,
field: key,
active: typeof col.active != "boolean" ? toggleOn : col.active,
}
})
}
/*
SUPPORT
- ["FIELD1", "FIELD2"...]
"fields": [ "First Name", "Last Name" ]
- [{name: "FIELD1", displayName: "FIELD1"}, ... only the currentlyadded fields]
* [{name: "FIELD1", displayName: "FIELD1", active: true|false}, all currently available fields]
*/
$: unconfigured = buildListOptions(schema, fieldConfigList)
const convertOldFieldFormat = fields => {
let formFields
if (typeof fields?.[0] === "string") {
formFields = fields.map(field => ({
name: field,
displayName: field,
active: true,
}))
} else {
formFields = fields
}
return (formFields || []).map(field => {
return {
...field,
active: typeof field?.active != "boolean" ? true : field?.active,
}
})
}
const getSchema = (asset, datasource) => {
const schema = getSchemaForDatasource(asset, datasource).schema
@ -134,36 +83,61 @@
return schema
}
const updateBoundValue = value => {
fieldConfigList = cloneDeep(value)
const updateSanitsedFields = value => {
sanitisedFields = cloneDeep(value)
}
const getValidColumns = (columns, options) => {
if (!Array.isArray(columns) || !columns.length) {
return []
}
// We need to account for legacy configs which would just be an array
// of strings
if (typeof columns[0] === "string") {
columns = columns.map(col => ({
name: col,
displayName: col,
}))
}
return columns.filter(column => {
return options.includes(column.name)
return options.includes(column.field)
})
}
let listOptions
$: if (fieldConfigList) {
listOptions = [...fieldConfigList, ...unconfigured].map(column => {
const type = getComponentForField(column.name)
const _component = `@budibase/standard-components/${type}`
const buildSudoInstance = instance => {
if (instance._component) {
return instance
}
return { ...column, _component } //only necessary if it doesnt exist
const type = getComponentForField(instance.field, schema)
instance._component = `@budibase/standard-components/${type}`
const sudoComponentInstance = store.actions.components.createInstance(
instance._component,
{
_instanceName: instance.field,
field: instance.field,
label: instance.field,
placeholder: instance.field,
},
{}
)
return { ...instance, ...sudoComponentInstance }
}
$: if (sanitisedFields) {
fieldList = [...sanitisedFields, ...unconfigured].map(buildSudoInstance)
}
const processItemUpdate = e => {
const updatedField = e.detail
const parentFieldsUpdated = fieldList ? cloneDeep(fieldList) : []
let parentFieldIdx = parentFieldsUpdated.findIndex(pSetting => {
return pSetting.field === updatedField?.field
})
console.log(listOptions)
if (parentFieldIdx == -1) {
parentFieldsUpdated.push(updatedField)
} else {
parentFieldsUpdated[parentFieldIdx] = updatedField
}
// fieldList = parentFieldsUpdated
dispatch("change", getValidColumns(parentFieldsUpdated, options))
}
const listUpdated = e => {
@ -173,12 +147,19 @@
</script>
<div class="field-configuration">
<SettingsList
value={listOptions}
on:change={listUpdated}
rightButton={EditFieldPopover}
rightProps={{ componentBindings, bindings, parent: componentInstance }}
/>
{#if fieldList?.length}
<DraggableList
on:change={listUpdated}
on:itemChange={processItemUpdate}
items={fieldList}
listItemKey={"_id"}
listType={FieldSetting}
listTypeProps={{
componentBindings,
bindings,
}}
/>
{/if}
</div>
<style>

View file

@ -0,0 +1,56 @@
<script>
import EditFieldPopover from "./EditFieldPopover.svelte"
import { Toggle } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { cloneDeep } from "lodash/fp"
export let item
export let componentBindings
export let bindings
export let anchor
const dispatch = createEventDispatcher()
const onToggle = item => {
return e => {
item.active = e.detail
dispatch("change", { ...cloneDeep(item), active: e.detail })
}
}
</script>
<div class="list-item-body">
<div class="list-item-left">
<EditFieldPopover
{anchor}
field={item}
{componentBindings}
{bindings}
on:change
/>
<div class="field-label">{item.label || item.field}</div>
</div>
<div class="list-item-right">
<Toggle on:change={onToggle(item)} text="" value={item.active} thin />
</div>
</div>
<style>
.field-label {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.list-item-body,
.list-item-left {
display: flex;
align-items: center;
gap: var(--spacing-m);
min-width: 0;
}
.list-item-right :global(div.spectrum-Switch) {
margin: 0px;
}
.list-item-body {
justify-content: space-between;
}
</style>

View file

@ -0,0 +1,46 @@
export const convertOldFieldFormat = fields => {
if (!fields) {
return []
}
const converted = fields.map(field => {
if (typeof field === "string") {
// existed but was a string
return {
field,
active: true,
}
} else if (typeof field?.active != "boolean") {
// existed but had no state
return {
field: field.name,
active: true,
}
} else {
return field
}
})
return converted
}
export const getComponentForField = (field, schema) => {
if (!field || !schema?.[field]) {
return null
}
const type = schema[field].type
return FieldTypeToComponentMap[type]
}
export const FieldTypeToComponentMap = {
string: "stringfield",
number: "numberfield",
bigint: "bigintfield",
options: "optionsfield",
array: "multifieldselect",
boolean: "booleanfield",
longform: "longformfield",
datetime: "datetimefield",
attachment: "attachmentfield",
link: "relationshipfield",
json: "jsonfield",
barcodeqr: "codescanner",
}

View file

@ -24,11 +24,22 @@
}
</script>
<ActionButton on:click={drawer.show}>Define Options</ActionButton>
<Drawer bind:this={drawer} title="Options">
<div class="options-wrap">
<div />
<div><ActionButton on:click={drawer.show}>Define Options</ActionButton></div>
</div>
<Drawer bind:this={drawer} title="Options" on:drawerHide on:drawerShow>
<svelte:fragment slot="description">
Define the options for this picker.
</svelte:fragment>
<Button cta slot="buttons" on:click={saveOptions}>Save</Button>
<OptionsDrawer bind:options={tempValue} slot="body" />
</Drawer>
<style>
.options-wrap {
gap: 8px;
display: grid;
grid-template-columns: 90px 1fr;
}
</style>

View file

@ -100,6 +100,8 @@
{key}
{type}
{...props}
on:drawerHide
on:drawerShow
/>
</div>
{#if info}

View file

@ -5,9 +5,8 @@
export let value = []
export let bindings = []
export let componentDefinition
export let componentInstance
export let type
const dispatch = createEventDispatcher()
let drawer
@ -31,7 +30,7 @@
<ActionButton on:click={drawer.show}>{text}</ActionButton>
</div>
<Drawer bind:this={drawer} title="Validation Rules">
<Drawer bind:this={drawer} title="Validation Rules" on:drawerHide on:drawerShow>
<svelte:fragment slot="description">
Configure validation rules for this field.
</svelte:fragment>
@ -41,7 +40,7 @@
bind:rules={value}
{type}
{bindings}
{componentDefinition}
fieldName={componentInstance?.field}
/>
</Drawer>

View file

@ -1,36 +1,12 @@
<script>
import { DetailSummary, Icon } from "@budibase/bbui"
import { DetailSummary } from "@budibase/bbui"
import InfoDisplay from "./InfoDisplay.svelte"
export let componentDefinition
</script>
<DetailSummary collapsible={false}>
<div class="info">
<div class="title">
<Icon name="HelpOutline" />
{componentDefinition.name}
</div>
{componentDefinition.info}
</div>
<DetailSummary collapsible={false} noPadding={true}>
<InfoDisplay
title={componentDefinition.name}
body={componentDefinition.info}
/>
</DetailSummary>
<style>
.title {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
margin-bottom: var(--spacing-m);
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-s);
color: var(--spectrum-global-color-gray-600);
}
.info {
padding: var(--spacing-m) var(--spacing-l) var(--spacing-l) var(--spacing-l);
background-color: var(--background-alt);
border-radius: var(--border-radius-s);
font-size: 13px;
}
</style>

View file

@ -5,7 +5,7 @@
import DesignSection from "./DesignSection.svelte"
import CustomStylesSection from "./CustomStylesSection.svelte"
import ConditionalUISection from "./ConditionalUISection.svelte"
import ComponentInfoSection from "./ComponentInfoSection.svelte"
import {
getBindableProperties,
getComponentBindableProperties,
@ -55,9 +55,6 @@
</div>
</span>
{#if section == "settings"}
{#if componentDefinition?.info}
<ComponentInfoSection {componentDefinition} />
{/if}
<ComponentSettingsSection
{componentInstance}
{componentDefinition}

View file

@ -6,6 +6,7 @@
import ResetFieldsButton from "components/design/settings/controls/ResetFieldsButton.svelte"
import EjectBlockButton from "components/design/settings/controls/EjectBlockButton.svelte"
import { getComponentForSetting } from "components/design/settings/componentSettings"
import InfoDisplay from "./InfoDisplay.svelte"
import analytics, { Events } from "analytics"
export let componentDefinition
@ -14,15 +15,13 @@
export let componentBindings
export let isScreen = false
export let onUpdateSetting
export let showSectionTitle = true
export let showInstanceName = true
$: sections = getSections(componentInstance, componentDefinition, isScreen)
const getSections = (instance, definition, isScreen) => {
const settings = definition?.settings ?? []
console.log(
"ComponentSettingsSection::definition?.settings",
definition?.settings
)
const generalSettings = settings.filter(setting => !setting.section)
const customSections = settings.filter(setting => setting.section)
let sections = [
@ -51,23 +50,22 @@
}
const updateSetting = async (setting, value) => {
if (typeof onUpdateSetting === "function") {
onUpdateSetting(setting, value)
} else {
try {
try {
if (typeof onUpdateSetting === "function") {
await onUpdateSetting(setting, value)
} else {
await store.actions.components.updateSetting(setting.key, value)
// Send event if required
if (setting.sendEvents) {
analytics.captureEvent(Events.COMPONENT_UPDATED, {
name: componentInstance._component,
setting: setting.key,
value,
})
}
} catch (error) {
notifications.error("Error updating component prop")
}
// Send event if required
if (setting.sendEvents) {
analytics.captureEvent(Events.COMPONENT_UPDATED, {
name: componentInstance._component,
setting: setting.key,
value,
})
}
} catch (error) {
notifications.error("Error updating component prop")
}
}
@ -106,7 +104,7 @@
}
}
return true
return typeof setting.visible == "boolean" ? setting.visible : true
}
const canRenderControl = (instance, setting, isScreen) => {
@ -125,9 +123,22 @@
{#each sections as section, idx (section.name)}
{#if section.visible}
<DetailSummary name={section.name} collapsible={false}>
<DetailSummary
name={showSectionTitle ? section.name : ""}
collapsible={false}
>
{#if section.info}
<div class="section-info">
<InfoDisplay body={section.info} />
</div>
{:else if idx === 0 && section.name === "General" && componentDefinition.info}
<InfoDisplay
title={componentDefinition.name}
body={componentDefinition.info}
/>
{/if}
<div class="settings">
{#if idx === 0 && !componentInstance._component.endsWith("/layout") && !isScreen}
{#if idx === 0 && !componentInstance._component.endsWith("/layout") && !isScreen && showInstanceName}
<PropertyControl
control={Input}
label="Name"
@ -138,13 +149,10 @@
{/if}
{#each section.settings as setting (setting.key)}
{#if setting.visible}
<!-- DEAN - Remove fieldConfiguration label config -->
<PropertyControl
type={setting.type}
control={getComponentForSetting(setting)}
label={setting.type != "fieldConfiguration"
? setting.label
: undefined}
label={setting.label}
labelHidden={setting.labelHidden}
key={setting.key}
value={componentInstance[setting.key]}
@ -169,6 +177,8 @@
{componentBindings}
{componentInstance}
{componentDefinition}
on:drawerShow
on:drawerHide
/>
{/if}
{/each}

View file

@ -0,0 +1,61 @@
<script>
import { Icon } from "@budibase/bbui"
export let title
export let body
export let icon = "HelpOutline"
</script>
<div class="info" class:noTitle={!title}>
{#if title}
<div class="title">
<Icon name={icon} />
{title || ""}
</div>
{@html body}
{:else}
<span class="icon">
<Icon name={icon} />
</span>
{@html body}
{/if}
</div>
<style>
.title {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
margin-bottom: var(--spacing-m);
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-s);
}
.title,
.icon {
color: var(--spectrum-global-color-gray-600);
}
.info {
padding: var(--spacing-m) var(--spacing-l) var(--spacing-l) var(--spacing-l);
background-color: var(--background-alt);
border-radius: var(--border-radius-s);
font-size: 13px;
}
.noTitle {
display: flex;
align-items: center;
gap: var(--spacing-m);
}
.info :global(a) {
color: inherit;
transition: color 130ms ease-out;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.info :global(a:hover) {
color: var(--spectrum-global-color-gray-900);
}
</style>

View file

@ -5282,6 +5282,11 @@
},
{
"section": true,
"dependsOn": {
"setting": "actionType",
"value": "Create",
"invert": true
},
"name": "Row details",
"info": "<a href='https://docs.budibase.com/docs/form-block' target='_blank'>How to pass a row ID using bindings</a>",
"settings": [
@ -5289,23 +5294,13 @@
"type": "text",
"label": "Row ID",
"key": "rowId",
"nested": true,
"dependsOn": {
"setting": "actionType",
"value": "Create",
"invert": true
}
"nested": true
},
{
"type": "text",
"label": "Empty text",
"key": "noRowsMessage",
"defaultValue": "We couldn't find a row to display",
"dependsOn": {
"setting": "actionType",
"value": "Create",
"invert": true
}
"defaultValue": "We couldn't find a row to display"
}
]
},
@ -5399,6 +5394,7 @@
{
"type": "fieldConfiguration",
"key": "fields",
"nested": true,
"selectAllFields": true
},
{

View file

@ -45,6 +45,9 @@
let enrichedSearchColumns
let schemaLoaded = false
// Accommodate old config to ensure delete button does not reappear
$: deleteLabel = sidePanelShowDelete === false ? "" : sidePanelDeleteLabel
$: fetchSchema(dataSource)
$: enrichSearchColumns(searchColumns, schema).then(
val => (enrichedSearchColumns = val)
@ -245,10 +248,8 @@
bind:id={detailsFormBlockId}
props={{
dataSource,
showSaveButton: true,
showDeleteButton: sidePanelShowDelete,
saveButtonLabel: sidePanelSaveLabel,
deleteButtonLabel: sidePanelDeleteLabel,
saveButtonLabel: sidePanelSaveLabel || "Save", //always show
deleteButtonLabel: deleteLabel, //respect config
actionType: "Update",
rowId: `{{ ${safe("state")}.${safe(stateKey)} }}`,
fields: sidePanelFields || normalFields,

View file

@ -12,55 +12,59 @@
export let fields
export let labelPosition
export let title
export let showDeleteButton
export let showSaveButton
export let saveButtonLabel
export let deleteButtonLabel
export let showSaveButton
export let showDeleteButton
export let rowId
export let actionUrl
export let noRowsMessage
export let notificationOverride
// Accommodate old config to ensure delete button does not reappear
$: deleteLabel = showDeleteButton === false ? "" : deleteButtonLabel?.trim()
$: saveLabel = showSaveButton === false ? "" : saveButtonLabel?.trim()
const { fetchDatasourceSchema } = getContext("sdk")
const convertOldFieldFormat = fields => {
return typeof fields?.[0] === "string"
? fields.map(field => ({
if (!fields) {
return []
}
return fields.map(field => {
if (typeof field === "string") {
// existed but was a string
return {
name: field,
displayName: field,
active: true,
}))
: fields
}
//All settings need to derive from the block config now
// Parse the fields here too. Not present means false.
const getDefaultFields = (fields, schema) => {
let formFields
if (schema && (!fields || fields.length === 0)) {
const defaultFields = []
Object.values(schema).forEach(field => {
if (field.autocolumn) return
defaultFields.push({
name: field.name,
displayName: field.name,
active: true,
})
})
formFields = [...defaultFields]
} else {
formFields = (fields || []).map(field => {
}
} else {
// existed but had no state
return {
...field,
active: typeof field?.active != "boolean" ? true : field?.active,
}
})
}
}
})
}
return formFields.filter(field => field.active)
const getDefaultFields = (fields, schema) => {
if (!schema) {
return []
}
let defaultFields = []
if (!fields || fields.length === 0) {
Object.values(schema)
.filter(field => !field.autocolumn)
.forEach(field => {
defaultFields.push({
name: field.name,
active: true,
})
})
}
return [...fields, ...defaultFields].filter(field => field.active)
}
let schema
@ -94,15 +98,12 @@
fields: fieldsOrDefault,
labelPosition,
title,
saveButtonLabel,
deleteButtonLabel,
showSaveButton,
showDeleteButton,
saveButtonLabel: saveLabel,
deleteButtonLabel: deleteLabel,
schema,
repeaterId,
notificationOverride,
}
const fetchSchema = async () => {
schema = (await fetchDatasourceSchema(dataSource)) || {}
}

View file

@ -13,8 +13,6 @@
export let title
export let saveButtonLabel
export let deleteButtonLabel
export let showSaveButton
export let showDeleteButton
export let schema
export let repeaterId
export let notificationOverride
@ -100,18 +98,33 @@
},
]
$: renderDeleteButton = showDeleteButton && actionType === "Update"
$: renderSaveButton = showSaveButton && actionType !== "View"
$: renderDeleteButton = deleteButtonLabel && actionType === "Update"
$: renderSaveButton = saveButtonLabel && actionType !== "View"
$: renderButtons = renderDeleteButton || renderSaveButton
$: renderHeader = renderButtons || title
const getComponentForField = field => {
if (!field || !schema?.[field]) {
const fieldSchemaName = field.field || field.name
if (!fieldSchemaName || !schema?.[fieldSchemaName]) {
return null
}
const type = schema[field].type
const type = schema[fieldSchemaName].type
return FieldTypeToComponentMap[type]
}
const getPropsForField = field => {
let fieldProps = field._component
? {
...field,
}
: {
field: field.name,
label: field.name,
placeholder: field.name,
_instanceName: field.name,
}
return fieldProps
}
</script>
{#if fields?.length}
@ -175,7 +188,7 @@
<BlockComponent
type="button"
props={{
text: deleteButtonLabel || "Delete",
text: deleteButtonLabel,
onClick: onDelete,
quiet: true,
type: "secondary",
@ -187,7 +200,7 @@
<BlockComponent
type="button"
props={{
text: saveButtonLabel || "Save",
text: saveButtonLabel,
onClick: onSave,
type: "cta",
}}
@ -201,15 +214,10 @@
{#key fields}
<BlockComponent type="fieldgroup" props={{ labelPosition }} order={1}>
{#each fields as field, idx}
{#if getComponentForField(field.name)}
{#if getComponentForField(field) && field.active}
<BlockComponent
type={getComponentForField(field.name)}
props={{
validation: field.validation,
field: field.name,
label: field.displayName,
placeholder: field.displayName,
}}
type={getComponentForField(field)}
props={getPropsForField(field)}
order={idx}
/>
{/if}