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:
parent
046ef853e3
commit
1ec2faf74d
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
|
|
|
@ -74,6 +74,8 @@
|
|||
{/if}
|
||||
</div>
|
||||
<Drawer
|
||||
on:drawerHide
|
||||
on:drawerShow
|
||||
{fillWidth}
|
||||
bind:this={bindingDrawer}
|
||||
{title}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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",
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -100,6 +100,8 @@
|
|||
{key}
|
||||
{type}
|
||||
{...props}
|
||||
on:drawerHide
|
||||
on:drawerShow
|
||||
/>
|
||||
</div>
|
||||
{#if info}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)) || {}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Reference in New Issue