Merge pull request #11597 from Budibase/feature/form-block-ux-updates

Form/Table block UX Updates
This commit is contained in:
Martin McKeaveney 2023-08-29 17:45:12 +01:00 committed by GitHub
commit 77655be571
26 changed files with 1003 additions and 326 deletions

View File

@ -17,6 +17,7 @@ export default function positionDropdown(element, opts) {
maxWidth, maxWidth,
useAnchorWidth, useAnchorWidth,
offset = 5, offset = 5,
customUpdate,
} = opts } = opts
if (!anchor) { if (!anchor) {
return return
@ -32,33 +33,42 @@ export default function positionDropdown(element, opts) {
left: null, left: null,
top: null, top: null,
} }
// Determine vertical styles
if (align === "right-outside") {
styles.top = anchorBounds.top
} else if (window.innerHeight - anchorBounds.bottom < (maxHeight || 100)) {
styles.top = anchorBounds.top - elementBounds.height - offset
styles.maxHeight = maxHeight || 240
} else {
styles.top = anchorBounds.bottom + offset
styles.maxHeight =
maxHeight || window.innerHeight - anchorBounds.bottom - 20
}
// Determine horizontal styles if (typeof customUpdate === "function") {
if (!maxWidth && useAnchorWidth) { styles = customUpdate(anchorBounds, elementBounds, styles)
styles.maxWidth = anchorBounds.width
}
if (useAnchorWidth) {
styles.minWidth = anchorBounds.width
}
if (align === "right") {
styles.left = anchorBounds.left + anchorBounds.width - elementBounds.width
} else if (align === "right-outside") {
styles.left = anchorBounds.right + offset
} else if (align === "left-outside") {
styles.left = anchorBounds.left - elementBounds.width - offset
} else { } else {
styles.left = anchorBounds.left // Determine vertical styles
if (align === "right-outside") {
styles.top = anchorBounds.top
} else if (
window.innerHeight - anchorBounds.bottom <
(maxHeight || 100)
) {
styles.top = anchorBounds.top - elementBounds.height - offset
styles.maxHeight = maxHeight || 240
} else {
styles.top = anchorBounds.bottom + offset
styles.maxHeight =
maxHeight || window.innerHeight - anchorBounds.bottom - 20
}
// Determine horizontal styles
if (!maxWidth && useAnchorWidth) {
styles.maxWidth = anchorBounds.width
}
if (useAnchorWidth) {
styles.minWidth = anchorBounds.width
}
if (align === "right") {
styles.left =
anchorBounds.left + anchorBounds.width - elementBounds.width
} else if (align === "right-outside") {
styles.left = anchorBounds.right + offset
} else if (align === "left-outside") {
styles.left = anchorBounds.left - elementBounds.width - offset
} else {
styles.left = anchorBounds.left
}
} }
// Apply styles // Apply styles

View File

@ -44,7 +44,9 @@
align-items: stretch; align-items: stretch;
border-bottom: var(--border-light); border-bottom: var(--border-light);
} }
.property-group-container:last-child {
border-bottom: 0px;
}
.property-group-name { .property-group-name {
cursor: pointer; cursor: pointer;
display: flex; display: flex;

View File

@ -4,6 +4,8 @@
import Body from "../Typography/Body.svelte" import Body from "../Typography/Body.svelte"
import Heading from "../Typography/Heading.svelte" import Heading from "../Typography/Heading.svelte"
import { setContext } from "svelte" import { setContext } from "svelte"
import { createEventDispatcher } from "svelte"
import { generate } from "shortid"
export let title export let title
export let fillWidth export let fillWidth
@ -11,13 +13,17 @@
export let width = "calc(100% - 626px)" export let width = "calc(100% - 626px)"
export let headless = false export let headless = false
const dispatch = createEventDispatcher()
let visible = false let visible = false
let drawerId = generate()
export function show() { export function show() {
if (visible) { if (visible) {
return return
} }
visible = true visible = true
dispatch("drawerShow", drawerId)
} }
export function hide() { export function hide() {
@ -25,6 +31,7 @@
return return
} }
visible = false visible = false
dispatch("drawerHide", drawerId)
} }
setContext("drawer-actions", { setContext("drawer-actions", {

View File

@ -2,8 +2,8 @@
import "@spectrum-css/inputgroup/dist/index-vars.css" import "@spectrum-css/inputgroup/dist/index-vars.css"
import "@spectrum-css/popover/dist/index-vars.css" import "@spectrum-css/popover/dist/index-vars.css"
import "@spectrum-css/menu/dist/index-vars.css" import "@spectrum-css/menu/dist/index-vars.css"
import { fly } from "svelte/transition"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import clickOutside from "../../Actions/click_outside"
export let value = null export let value = null
export let id = null export let id = null
@ -80,10 +80,11 @@
</svg> </svg>
</button> </button>
{#if open} {#if open}
<div class="overlay" on:mousedown|self={() => (open = false)} />
<div <div
transition:fly|local={{ y: -20, duration: 200 }}
class="spectrum-Popover spectrum-Popover--bottom is-open" class="spectrum-Popover spectrum-Popover--bottom is-open"
use:clickOutside={() => {
open = false
}}
> >
<ul class="spectrum-Menu" role="listbox"> <ul class="spectrum-Menu" role="listbox">
{#if options && Array.isArray(options)} {#if options && Array.isArray(options)}
@ -125,14 +126,6 @@
.spectrum-Textfield-input { .spectrum-Textfield-input {
width: 0; width: 0;
} }
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 999;
}
.spectrum-Popover { .spectrum-Popover {
max-height: 240px; max-height: 240px;
width: 100%; width: 100%;

View File

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

View File

@ -6,7 +6,7 @@ import {
findComponentPath, findComponentPath,
getComponentSettings, getComponentSettings,
} from "./componentUtils" } from "./componentUtils"
import { store } from "builderStore" import { store, currentAsset } from "builderStore"
import { import {
queries as queriesStores, queries as queriesStores,
tables as tablesStore, tables as tablesStore,
@ -22,6 +22,7 @@ import { TableNames } from "../constants"
import { JSONUtils } from "@budibase/frontend-core" import { JSONUtils } from "@budibase/frontend-core"
import ActionDefinitions from "components/design/settings/controls/ButtonActionEditor/manifest.json" import ActionDefinitions from "components/design/settings/controls/ButtonActionEditor/manifest.json"
import { environment, licensing } from "stores/portal" import { environment, licensing } from "stores/portal"
import { convertOldFieldFormat } from "components/design/settings/controls/FieldConfiguration/utils"
// Regex to match all instances of template strings // Regex to match all instances of template strings
const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
@ -328,7 +329,7 @@ const getProviderContextBindings = (asset, dataProviders) => {
if (context.type === "form") { if (context.type === "form") {
// Forms do not need table schemas // Forms do not need table schemas
// Their schemas are built from their component field names // Their schemas are built from their component field names
schema = buildFormSchema(component) schema = buildFormSchema(component, asset)
readablePrefix = "Fields" readablePrefix = "Fields"
} else if (context.type === "static") { } else if (context.type === "static") {
// Static contexts are fully defined by the components // Static contexts are fully defined by the components
@ -370,6 +371,11 @@ const getProviderContextBindings = (asset, dataProviders) => {
if (runtimeSuffix) { if (runtimeSuffix) {
providerId += `-${runtimeSuffix}` providerId += `-${runtimeSuffix}`
} }
if (!filterCategoryByContext(component, context)) {
return
}
const safeComponentId = makePropSafe(providerId) const safeComponentId = makePropSafe(providerId)
// Create bindable properties for each schema field // Create bindable properties for each schema field
@ -387,6 +393,12 @@ const getProviderContextBindings = (asset, dataProviders) => {
} }
readableBinding += `.${fieldSchema.name || key}` readableBinding += `.${fieldSchema.name || key}`
const bindingCategory = getComponentBindingCategory(
component,
context,
def
)
// Create the binding object // Create the binding object
bindings.push({ bindings.push({
type: "context", type: "context",
@ -399,8 +411,8 @@ const getProviderContextBindings = (asset, dataProviders) => {
// Table ID is used by JSON fields to know what table the field is in // Table ID is used by JSON fields to know what table the field is in
tableId: table?._id, tableId: table?._id,
component: component._component, component: component._component,
category: component._instanceName, category: bindingCategory.category,
icon: def.icon, icon: bindingCategory.icon,
display: { display: {
name: fieldSchema.name || key, name: fieldSchema.name || key,
type: fieldSchema.type, type: fieldSchema.type,
@ -413,6 +425,40 @@ const getProviderContextBindings = (asset, dataProviders) => {
return bindings 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. * Gets all bindable properties from the logged in user.
*/ */
@ -507,6 +553,7 @@ const getSelectedRowsBindings = asset => {
)}.${makePropSafe("selectedRows")}`, )}.${makePropSafe("selectedRows")}`,
readableBinding: `${block._instanceName}.Selected rows`, readableBinding: `${block._instanceName}.Selected rows`,
category: "Selected rows", category: "Selected rows",
icon: "ViewRow",
display: { name: block._instanceName }, display: { name: block._instanceName },
})) }))
) )
@ -582,24 +629,36 @@ const getRoleBindings = () => {
} }
/** /**
* Gets all bindable properties exposed in an event action flow up until * Gets all bindable event context properties provided in the component
* the specified action ID, as well as context provided for the action * setting
* setting as a whole by the component.
*/ */
export const getEventContextBindings = ( export const getEventContextBindings = ({
asset,
componentId,
settingKey, settingKey,
actions, componentInstance,
actionId componentId,
) => { componentDefinition,
asset,
}) => {
let bindings = [] let bindings = []
const selectedAsset = asset ?? get(currentAsset)
// Check if any context bindings are provided by the component for this // Check if any context bindings are provided by the component for this
// setting // setting
const component = findComponent(asset.props, componentId) const component =
const def = store.actions.components.getDefinition(component?._component) componentInstance ?? findComponent(selectedAsset.props, componentId)
if (!component) {
return bindings
}
const definition =
componentDefinition ??
store.actions.components.getDefinition(component?._component)
const settings = getComponentSettings(component?._component) const settings = getComponentSettings(component?._component)
const eventSetting = settings.find(setting => setting.key === settingKey) const eventSetting = settings.find(setting => setting.key === settingKey)
if (eventSetting?.context?.length) { if (eventSetting?.context?.length) {
eventSetting.context.forEach(contextEntry => { eventSetting.context.forEach(contextEntry => {
bindings.push({ bindings.push({
@ -608,14 +667,23 @@ export const getEventContextBindings = (
contextEntry.key contextEntry.key
)}`, )}`,
category: component._instanceName, category: component._instanceName,
icon: def.icon, icon: definition.icon,
display: { display: {
name: contextEntry.label, name: contextEntry.label,
}, },
}) })
}) })
} }
return bindings
}
/**
* Gets all bindable properties exposed in an event action flow up until
* the specified action ID, as well as context provided for the action
* setting as a whole by the component.
*/
export const getActionBindings = (actions, actionId) => {
let bindings = []
// Get the steps leading up to this value // Get the steps leading up to this value
const index = actions?.findIndex(action => action.id === actionId) const index = actions?.findIndex(action => action.id === actionId)
if (index == null || index === -1) { if (index == null || index === -1) {
@ -642,7 +710,6 @@ export const getEventContextBindings = (
}) })
} }
}) })
return bindings return bindings
} }
@ -835,18 +902,36 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
* Builds a form schema given a form component. * Builds a form schema given a form component.
* A form schema is a schema of all the fields nested anywhere within a form. * 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 = {} let schema = {}
if (!component) { if (!component) {
return schema return schema
} }
// If this is a form block, simply use the fields setting
if (component._component.endsWith("formblock")) { if (component._component.endsWith("formblock")) {
let schema = {} 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 return schema
} }
@ -862,7 +947,7 @@ export const buildFormSchema = component => {
} }
} }
component._children?.forEach(child => { component._children?.forEach(child => {
const childSchema = buildFormSchema(child) const childSchema = buildFormSchema(child, asset)
schema = { ...schema, ...childSchema } schema = { ...schema, ...childSchema }
}) })
return schema return schema

View File

@ -111,6 +111,7 @@ export const getFrontendStore = () => {
} }
let clone = cloneDeep(screen) let clone = cloneDeep(screen)
const result = patchFn(clone) const result = patchFn(clone)
if (result === false) { if (result === false) {
return return
} }
@ -837,6 +838,7 @@ export const getFrontendStore = () => {
return return
} }
const patchScreen = screen => { const patchScreen = screen => {
// findComponent looks in the tree not comp.settings[0]
let component = findComponent(screen.props, componentId) let component = findComponent(screen.props, componentId)
if (!component) { if (!component) {
return false return false
@ -1226,7 +1228,12 @@ export const getFrontendStore = () => {
}) })
}, },
updateSetting: async (name, value) => { updateSetting: async (name, value) => {
await store.actions.components.patch(component => { await store.actions.components.patch(
store.actions.components.updateComponentSetting(name, value)
)
},
updateComponentSetting: (name, value) => {
return component => {
if (!name || !component) { if (!name || !component) {
return false return false
} }
@ -1254,9 +1261,8 @@ export const getFrontendStore = () => {
component[key] = columnNames component[key] = columnNames
}) })
} }
component[name] = value component[name] = value
}) }
}, },
requestEjectBlock: componentId => { requestEjectBlock: componentId => {
store.actions.preview.sendEvent("eject-block", componentId) store.actions.preview.sendEvent("eject-block", componentId)

View File

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

View File

@ -13,9 +13,9 @@
import { generate } from "shortid" import { generate } from "shortid"
import { import {
getEventContextBindings, getEventContextBindings,
getActionBindings,
makeStateBinding, makeStateBinding,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import { currentAsset, store } from "builderStore"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
const flipDurationMs = 150 const flipDurationMs = 150
@ -26,6 +26,7 @@
export let actions export let actions
export let bindings = [] export let bindings = []
export let nested export let nested
export let componentInstance
let actionQuery let actionQuery
let selectedAction = actions?.length ? actions[0] : null let selectedAction = actions?.length ? actions[0] : null
@ -68,15 +69,19 @@
acc[action.type].push(action) acc[action.type].push(action)
return acc return acc
}, {}) }, {})
// These are ephemeral bindings which only exist while executing actions // These are ephemeral bindings which only exist while executing actions
$: eventContexBindings = getEventContextBindings( $: eventContextBindings = getEventContextBindings({
$currentAsset, componentInstance,
$store.selectedComponentId, settingKey: key,
key, })
actions, $: actionContextBindings = getActionBindings(actions, selectedAction?.id)
selectedAction?.id
$: allBindings = getAllBindings(
bindings,
[...eventContextBindings, ...actionContextBindings],
actions
) )
$: allBindings = getAllBindings(bindings, eventContexBindings, actions)
$: { $: {
// Ensure each action has a unique ID // Ensure each action has a unique ID
if (actions) { if (actions) {

View File

@ -13,6 +13,7 @@
export let name export let name
export let bindings export let bindings
export let nested export let nested
export let componentInstance
let drawer let drawer
let tmpValue let tmpValue
@ -74,7 +75,7 @@
<ActionButton on:click={openDrawer}>{actionText}</ActionButton> <ActionButton on:click={openDrawer}>{actionText}</ActionButton>
</div> </div>
<Drawer bind:this={drawer} title={"Actions"}> <Drawer bind:this={drawer} title={"Actions"} on:drawerHide on:drawerShow>
<svelte:fragment slot="description"> <svelte:fragment slot="description">
Define what actions to run. Define what actions to run.
</svelte:fragment> </svelte:fragment>
@ -86,6 +87,7 @@
{bindings} {bindings}
{key} {key}
{nested} {nested}
{componentInstance}
/> />
</Drawer> </Drawer>

View File

@ -0,0 +1,154 @@
<script>
import { Icon } from "@budibase/bbui"
import { dndzone } from "svelte-dnd-action"
import { createEventDispatcher } from "svelte"
import { generate } from "shortid"
import { setContext } from "svelte"
import { writable } from "svelte/store"
export let items = []
export let showHandle = true
export let listType
export let listTypeProps = {}
export let listItemKey
export let draggable = true
let store = writable({
selected: null,
actions: {
select: id => {
store.update(state => ({
...state,
selected: id,
}))
},
},
})
setContext("draggable", store)
const dispatch = createEventDispatcher()
const flipDurationMs = 150
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 => {
updateRowOrder(e)
dispatch("change", serialiseUpdate())
}
const onItemChanged = e => {
dispatch("itemChange", e.detail)
}
</script>
<ul
class="list-wrap"
use:dndzone={{
items: draggableItems,
flipDurationMs,
dropTargetStyle: { outline: "none" },
dragDisabled: !draggable,
}}
on:finalize={handleFinalize}
on:consider={updateRowOrder}
>
{#each draggableItems as draggable (draggable.id)}
<li
bind:this={anchors[draggable.id]}
class:highlighted={draggable.id === $store.selected}
>
<div class="left-content">
{#if showHandle}
<div class="handle" aria-label="drag-handle">
<Icon name="DragHandle" size="XL" />
</div>
{/if}
</div>
<div class="right-content">
<svelte:component
this={listType}
anchor={anchors[draggable.item._id]}
item={draggable.item}
{...listTypeProps}
on:change={onItemChanged}
/>
</div>
</li>
{/each}
</ul>
<style>
.list-wrap {
list-style-type: none;
margin: 0;
padding: 0;
width: 100%;
border-radius: 4px;
background-color: var(
--spectrum-table-background-color,
var(--spectrum-global-color-gray-50)
);
border: 1px solid
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
}
.list-wrap > li {
background-color: var(
--spectrum-table-background-color,
var(--spectrum-global-color-gray-50)
);
transition: background-color ease-in-out 130ms;
display: flex;
align-items: center;
border-bottom: 1px solid
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
}
.list-wrap > li:hover,
li.highlighted {
background-color: var(
--spectrum-table-row-background-color-hover,
var(--spectrum-alias-highlight-hover)
);
}
.list-wrap > li:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.list-wrap > li:last-child {
border-top-left-radius: var(--spectrum-table-regular-border-radius);
border-top-right-radius: var(--spectrum-table-regular-border-radius);
}
.right-content {
flex: 1;
min-width: 0;
}
.list-wrap li {
padding-left: var(--spacing-s);
padding-right: var(--spacing-s);
}
</style>

View File

@ -0,0 +1,160 @@
<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]/[componentId]/_components/Component/ComponentSettingsSection.svelte"
import { getContext } from "svelte"
export let anchor
export let field
export let componentBindings
export let bindings
const draggable = getContext("draggable")
const dispatch = createEventDispatcher()
let popover
let drawers = []
let pseudoComponentInstance
let open = false
$: if (open && $draggable.selected && $draggable.selected != field._id) {
popover.hide()
}
$: if (field) {
pseudoComponentInstance = field
}
$: componentDef = store.actions.components.getDefinition(
pseudoComponentInstance._component
)
$: parsedComponentDef = processComponentDefinitionSettings(componentDef)
const processComponentDefinitionSettings = componentDef => {
if (!componentDef) {
return {}
}
const clone = cloneDeep(componentDef)
const updatedSettings = clone.settings
.filter(setting => setting.key !== "field")
.map(setting => {
return { ...setting, nested: true }
})
clone.settings = updatedSettings
return clone
}
const updateSetting = async (setting, value) => {
const nestedComponentInstance = cloneDeep(pseudoComponentInstance)
const patchFn = store.actions.components.updateComponentSetting(
setting.key,
value
)
patchFn(nestedComponentInstance)
const update = {
...nestedComponentInstance,
active: pseudoComponentInstance.active,
}
dispatch("change", update)
}
</script>
<Icon
name="Settings"
hoverable
size="S"
on:click={() => {
if (!open) {
popover.show()
open = true
}
}}
/>
<Popover
bind:this={popover}
on:open={() => {
drawers = []
$draggable.actions.select(field._id)
}}
on:close={() => {
open = false
if ($draggable.selected == field._id) {
$draggable.actions.select()
}
}}
{anchor}
align="left-outside"
showPopover={drawers.length == 0}
clickOutsideOverride={drawers.length > 0}
maxHeight={600}
handlePostionUpdate={(anchorBounds, eleBounds, cfg) => {
let { left, top } = cfg
let percentageOffset = 30
// left-outside
left = anchorBounds.left - eleBounds.width - 18
// shift up from the anchor, if space allows
let offsetPos = Math.floor(eleBounds.height / 100) * percentageOffset
let defaultTop = anchorBounds.top - offsetPos
if (window.innerHeight - defaultTop < eleBounds.height) {
top = window.innerHeight - eleBounds.height - 5
} else {
top = anchorBounds.top - offsetPos
}
return { ...cfg, left, top }
}}
>
<span class="popover-wrap">
<Layout noPadding noGap>
<div class="type-icon">
<Icon name={parsedComponentDef.icon} />
<span>{field.field}</span>
</div>
<ComponentSettingsSection
componentInstance={pseudoComponentInstance}
componentDefinition={parsedComponentDef}
isScreen={false}
onUpdateSetting={updateSetting}
showSectionTitle={false}
showInstanceName={false}
{bindings}
{componentBindings}
on:drawerShow={e => {
drawers = [...drawers, e.detail]
}}
on:drawerHide={() => {
drawers = drawers.slice(0, -1)
}}
/>
</Layout>
</span>
</Popover>
<style>
.popover-wrap {
background-color: var(--spectrum-alias-background-color-primary);
}
.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,45 +1,70 @@
<script> <script>
import { Button, ActionButton, Drawer } from "@budibase/bbui" import { cloneDeep, isEqual } from "lodash/fp"
import { createEventDispatcher } from "svelte"
import ColumnDrawer from "./ColumnDrawer.svelte"
import { cloneDeep } from "lodash/fp"
import { import {
getDatasourceForProvider, getDatasourceForProvider,
getSchemaForDatasource, getSchemaForDatasource,
getBindableProperties,
getComponentBindableProperties,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import { currentAsset } from "builderStore" 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 componentInstance
export let value = [] export let value
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let sanitisedFields
let fieldList
let schema
let cachedValue
let drawer $: bindings = getBindableProperties($selectedScreen, componentInstance._id)
let boundValue $: actionType = componentInstance.actionType
let componentBindings = []
$: text = getText(value) $: if (actionType) {
$: convertOldColumnFormat(value) componentBindings = getComponentBindableProperties(
$: datasource = getDatasourceForProvider($currentAsset, componentInstance) $selectedScreen,
$: schema = getSchema($currentAsset, datasource) componentInstance._id
$: options = Object.keys(schema || {}) )
$: sanitisedValue = getValidColumns(value, options)
$: updateBoundValue(sanitisedValue)
const getText = value => {
if (!value?.length) {
return "All fields"
}
let text = `${value.length} field`
if (value.length !== 1) {
text += "s"
}
return text
} }
const convertOldColumnFormat = oldColumns => { $: datasource = getDatasourceForProvider($currentAsset, componentInstance)
if (typeof oldColumns?.[0] === "string") {
value = oldColumns.map(field => ({ name: field, displayName: field })) $: if (!isEqual(value, cachedValue)) {
cachedValue = value
schema = getSchema($currentAsset, datasource)
}
$: options = Object.keys(schema || {})
$: sanitisedValue = getValidColumns(convertOldFieldFormat(value), options)
$: updateSanitsedFields(sanitisedValue)
$: unconfigured = buildUnconfiguredOptions(schema, sanitisedFields)
// Builds unused ones only
const buildUnconfiguredOptions = (schema, selected) => {
if (!schema) {
return []
} }
let schemaClone = cloneDeep(schema)
selected.forEach(val => {
delete schemaClone[val.field]
})
return Object.keys(schemaClone)
.filter(key => !schemaClone[key].autocolumn)
.map(key => {
const col = schemaClone[key]
let toggleOn = !value
return {
field: key,
active: typeof col.active != "boolean" ? toggleOn : col.active,
}
})
} }
const getSchema = (asset, datasource) => { const getSchema = (asset, datasource) => {
@ -54,50 +79,85 @@
return schema return schema
} }
const updateBoundValue = value => { const updateSanitsedFields = value => {
boundValue = cloneDeep(value) sanitisedFields = cloneDeep(value)
} }
const getValidColumns = (columns, options) => { const getValidColumns = (columns, options) => {
if (!Array.isArray(columns) || !columns.length) { if (!Array.isArray(columns) || !columns.length) {
return [] 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 columns.filter(column => {
return options.includes(column.name) return options.includes(column.field)
}) })
} }
const open = () => { const buildSudoInstance = instance => {
updateBoundValue(sanitisedValue) if (instance._component) {
drawer.show() return instance
}
const type = getComponentForField(instance.field, schema)
instance._component = `@budibase/standard-components/${type}`
const pseudoComponentInstance = store.actions.components.createInstance(
instance._component,
{
_instanceName: instance.field,
field: instance.field,
label: instance.field,
placeholder: instance.field,
},
{}
)
return { ...instance, ...pseudoComponentInstance }
} }
const save = () => { $: if (sanitisedFields) {
dispatch("change", getValidColumns(boundValue, options)) fieldList = [...sanitisedFields, ...unconfigured].map(buildSudoInstance)
drawer.hide() }
const processItemUpdate = e => {
const updatedField = e.detail
const parentFieldsUpdated = fieldList ? cloneDeep(fieldList) : []
let parentFieldIdx = parentFieldsUpdated.findIndex(pSetting => {
return pSetting.field === updatedField?.field
})
if (parentFieldIdx == -1) {
parentFieldsUpdated.push(updatedField)
} else {
parentFieldsUpdated[parentFieldIdx] = updatedField
}
dispatch("change", getValidColumns(parentFieldsUpdated, options))
}
const listUpdated = e => {
const parsedColumns = getValidColumns(e.detail, options)
dispatch("change", parsedColumns)
} }
</script> </script>
<div class="field-configuration"> <div class="field-configuration">
<ActionButton on:click={open}>{text}</ActionButton> {#if fieldList?.length}
<DraggableList
on:change={listUpdated}
on:itemChange={processItemUpdate}
items={fieldList}
listItemKey={"_id"}
listType={FieldSetting}
listTypeProps={{
componentBindings,
bindings,
}}
/>
{/if}
</div> </div>
<Drawer bind:this={drawer} title="Form Fields">
<svelte:fragment slot="description">
Configure the fields in your form.
</svelte:fragment>
<Button cta slot="buttons" on:click={save}>Save</Button>
<ColumnDrawer slot="body" bind:columns={boundValue} {options} {schema} />
</Drawer>
<style> <style>
.field-configuration :global(.spectrum-ActionButton) { .field-configuration :global(.spectrum-ActionButton) {
width: 100%; width: 100%;

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

View File

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

View File

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

View File

@ -1,36 +1,12 @@
<script> <script>
import { DetailSummary, Icon } from "@budibase/bbui" import { DetailSummary } from "@budibase/bbui"
import InfoDisplay from "./InfoDisplay.svelte"
export let componentDefinition export let componentDefinition
</script> </script>
<DetailSummary collapsible={false}> <DetailSummary collapsible={false} noPadding={true}>
<div class="info"> <InfoDisplay
<div class="title"> title={componentDefinition.name}
<Icon name="HelpOutline" /> body={componentDefinition.info}
{componentDefinition.name} />
</div>
{componentDefinition.info}
</div>
</DetailSummary> </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 DesignSection from "./DesignSection.svelte"
import CustomStylesSection from "./CustomStylesSection.svelte" import CustomStylesSection from "./CustomStylesSection.svelte"
import ConditionalUISection from "./ConditionalUISection.svelte" import ConditionalUISection from "./ConditionalUISection.svelte"
import ComponentInfoSection from "./ComponentInfoSection.svelte"
import { import {
getBindableProperties, getBindableProperties,
getComponentBindableProperties, getComponentBindableProperties,
@ -55,9 +55,6 @@
</div> </div>
</span> </span>
{#if section == "settings"} {#if section == "settings"}
{#if componentDefinition?.info}
<ComponentInfoSection {componentDefinition} />
{/if}
<ComponentSettingsSection <ComponentSettingsSection
{componentInstance} {componentInstance}
{componentDefinition} {componentDefinition}

View File

@ -6,6 +6,7 @@
import ResetFieldsButton from "components/design/settings/controls/ResetFieldsButton.svelte" import ResetFieldsButton from "components/design/settings/controls/ResetFieldsButton.svelte"
import EjectBlockButton from "components/design/settings/controls/EjectBlockButton.svelte" import EjectBlockButton from "components/design/settings/controls/EjectBlockButton.svelte"
import { getComponentForSetting } from "components/design/settings/componentSettings" import { getComponentForSetting } from "components/design/settings/componentSettings"
import InfoDisplay from "./InfoDisplay.svelte"
import analytics, { Events } from "analytics" import analytics, { Events } from "analytics"
export let componentDefinition export let componentDefinition
@ -13,6 +14,9 @@
export let bindings export let bindings
export let componentBindings export let componentBindings
export let isScreen = false export let isScreen = false
export let onUpdateSetting
export let showSectionTitle = true
export let showInstanceName = true
$: sections = getSections(componentInstance, componentDefinition, isScreen) $: sections = getSections(componentInstance, componentDefinition, isScreen)
@ -47,8 +51,11 @@
const updateSetting = async (setting, value) => { const updateSetting = async (setting, value) => {
try { try {
await store.actions.components.updateSetting(setting.key, value) if (typeof onUpdateSetting === "function") {
await onUpdateSetting(setting, value)
} else {
await store.actions.components.updateSetting(setting.key, value)
}
// Send event if required // Send event if required
if (setting.sendEvents) { if (setting.sendEvents) {
analytics.captureEvent(Events.COMPONENT_UPDATED, { analytics.captureEvent(Events.COMPONENT_UPDATED, {
@ -97,7 +104,7 @@
} }
} }
return true return typeof setting.visible == "boolean" ? setting.visible : true
} }
const canRenderControl = (instance, setting, isScreen) => { const canRenderControl = (instance, setting, isScreen) => {
@ -116,9 +123,22 @@
{#each sections as section, idx (section.name)} {#each sections as section, idx (section.name)}
{#if section.visible} {#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"> <div class="settings">
{#if idx === 0 && !componentInstance._component.endsWith("/layout") && !isScreen} {#if idx === 0 && !componentInstance._component.endsWith("/layout") && !isScreen && showInstanceName}
<PropertyControl <PropertyControl
control={Input} control={Input}
label="Name" label="Name"
@ -157,6 +177,8 @@
{componentBindings} {componentBindings}
{componentInstance} {componentInstance}
{componentDefinition} {componentDefinition}
on:drawerShow
on:drawerHide
/> />
{/if} {/if}
{/each} {/each}

View File

@ -0,0 +1,66 @@
<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>
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html body}
{:else}
<span class="icon">
<Icon name={icon} />
</span>
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@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);
}
.info :global(a) {
text-decoration: underline;
}
</style>

View File

@ -4737,7 +4737,7 @@
] ]
}, },
{ {
"label": "Fields", "label": "",
"type": "fieldConfiguration", "type": "fieldConfiguration",
"key": "sidePanelFields", "key": "sidePanelFields",
"nested": true, "nested": true,
@ -4747,17 +4747,7 @@
} }
}, },
{ {
"label": "Show delete", "label": "Save button",
"type": "boolean",
"key": "sidePanelShowDelete",
"nested": true,
"dependsOn": {
"setting": "clickBehaviour",
"value": "details"
}
},
{
"label": "Save label",
"type": "text", "type": "text",
"key": "sidePanelSaveLabel", "key": "sidePanelSaveLabel",
"defaultValue": "Save", "defaultValue": "Save",
@ -4768,7 +4758,7 @@
} }
}, },
{ {
"label": "Delete label", "label": "Delete button",
"type": "text", "type": "text",
"key": "sidePanelDeleteLabel", "key": "sidePanelDeleteLabel",
"defaultValue": "Delete", "defaultValue": "Delete",
@ -5284,17 +5274,6 @@
"label": "Table", "label": "Table",
"key": "dataSource" "key": "dataSource"
}, },
{
"type": "text",
"label": "Row ID",
"key": "rowId",
"nested": true,
"dependsOn": {
"setting": "actionType",
"value": "Create",
"invert": true
}
},
{ {
"type": "text", "type": "text",
"label": "Title", "label": "Title",
@ -5302,116 +5281,55 @@
"nested": true "nested": true
}, },
{ {
"type": "select", "section": true,
"label": "Size",
"key": "size",
"options": [
{
"label": "Medium",
"value": "spectrum--medium"
},
{
"label": "Large",
"value": "spectrum--large"
}
],
"defaultValue": "spectrum--medium"
},
{
"type": "text",
"label": "Empty text",
"key": "noRowsMessage",
"defaultValue": "We couldn't find a row to display",
"dependsOn": { "dependsOn": {
"setting": "actionType", "setting": "actionType",
"value": "Create", "value": "Create",
"invert": true "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>",
"section": true,
"name": "Fields",
"settings": [ "settings": [
{ {
"type": "fieldConfiguration", "type": "text",
"label": "Fields", "label": "Row ID",
"key": "fields", "key": "rowId",
"selectAllFields": true "nested": true
}, },
{ {
"type": "select", "type": "text",
"label": "Field labels", "label": "Empty text",
"key": "labelPosition", "key": "noRowsMessage",
"defaultValue": "left", "defaultValue": "We couldn't find a row to display",
"options": [ "nested": true
{
"label": "Left",
"value": "left"
},
{
"label": "Above",
"value": "above"
}
]
},
{
"type": "boolean",
"label": "Disabled",
"key": "disabled",
"defaultValue": false,
"dependsOn": {
"setting": "actionType",
"value": "View",
"invert": true
}
} }
] ]
}, },
{ {
"section": true, "section": true,
"name": "Buttons", "name": "Buttons",
"dependsOn": {
"setting": "actionType",
"value": "View",
"invert": true
},
"settings": [ "settings": [
{
"type": "boolean",
"label": "Show save button",
"key": "showSaveButton",
"defaultValue": true,
"dependsOn": {
"setting": "actionType",
"value": "View",
"invert": true
}
},
{ {
"type": "text", "type": "text",
"key": "saveButtonLabel", "key": "saveButtonLabel",
"label": "Save button label", "label": "Save button",
"nested": true, "nested": true,
"defaultValue": "Save", "defaultValue": "Save"
"dependsOn": {
"setting": "showSaveButton",
"value": true
}
},
{
"type": "boolean",
"label": "Allow delete",
"key": "showDeleteButton",
"defaultValue": false,
"dependsOn": {
"setting": "actionType",
"value": "Update"
}
}, },
{ {
"type": "text", "type": "text",
"key": "deleteButtonLabel", "key": "deleteButtonLabel",
"label": "Delete button label", "label": "Delete button",
"nested": true, "nested": true,
"defaultValue": "Delete", "defaultValue": "Delete",
"dependsOn": { "dependsOn": {
"setting": "showDeleteButton", "setting": "actionType",
"value": true "value": "Update"
} }
}, },
{ {
@ -5429,7 +5347,67 @@
"type": "boolean", "type": "boolean",
"label": "Hide notifications", "label": "Hide notifications",
"key": "notificationOverride", "key": "notificationOverride",
"defaultValue": false "defaultValue": false,
"dependsOn": {
"setting": "actionType",
"value": "View",
"invert": true
}
}
]
},
{
"section": true,
"name": "Fields",
"settings": [
{
"type": "select",
"label": "Align labels",
"key": "labelPosition",
"defaultValue": "left",
"options": [
{
"label": "Left",
"value": "left"
},
{
"label": "Above",
"value": "above"
}
]
},
{
"type": "select",
"label": "Size",
"key": "size",
"options": [
{
"label": "Medium",
"value": "spectrum--medium"
},
{
"label": "Large",
"value": "spectrum--large"
}
],
"defaultValue": "spectrum--medium"
},
{
"type": "fieldConfiguration",
"key": "fields",
"nested": true,
"selectAllFields": true
},
{
"type": "boolean",
"label": "Disabled",
"key": "disabled",
"defaultValue": false,
"dependsOn": {
"setting": "actionType",
"value": "View",
"invert": true
}
} }
] ]
} }

View File

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

View File

@ -12,42 +12,59 @@
export let fields export let fields
export let labelPosition export let labelPosition
export let title export let title
export let showDeleteButton
export let showSaveButton
export let saveButtonLabel export let saveButtonLabel
export let deleteButtonLabel export let deleteButtonLabel
export let showSaveButton
export let showDeleteButton
export let rowId export let rowId
export let actionUrl export let actionUrl
export let noRowsMessage export let noRowsMessage
export let notificationOverride 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 { fetchDatasourceSchema } = getContext("sdk")
const convertOldFieldFormat = fields => { const convertOldFieldFormat = fields => {
if (typeof fields?.[0] === "string") { if (!fields) {
return fields.map(field => ({ name: field, displayName: field })) return []
} }
return fields.map(field => {
return fields if (typeof field === "string") {
// existed but was a string
return {
name: field,
active: true,
}
} else {
// existed but had no state
return {
...field,
active: typeof field?.active != "boolean" ? true : field?.active,
}
}
})
} }
const getDefaultFields = (fields, schema) => { const getDefaultFields = (fields, schema) => {
if (schema && (!fields || fields.length === 0)) { if (!schema) {
const defaultFields = [] return []
Object.values(schema).forEach(field => {
if (field.autocolumn) return
defaultFields.push({
name: field.name,
displayName: field.name,
})
})
return defaultFields
} }
let defaultFields = []
return fields 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 let schema
@ -56,7 +73,6 @@
$: formattedFields = convertOldFieldFormat(fields) $: formattedFields = convertOldFieldFormat(fields)
$: fieldsOrDefault = getDefaultFields(formattedFields, schema) $: fieldsOrDefault = getDefaultFields(formattedFields, schema)
$: fetchSchema(dataSource) $: fetchSchema(dataSource)
$: dataProvider = `{{ literal ${safe(providerId)} }}` $: dataProvider = `{{ literal ${safe(providerId)} }}`
$: filter = [ $: filter = [
@ -82,15 +98,12 @@
fields: fieldsOrDefault, fields: fieldsOrDefault,
labelPosition, labelPosition,
title, title,
saveButtonLabel, saveButtonLabel: saveLabel,
deleteButtonLabel, deleteButtonLabel: deleteLabel,
showSaveButton,
showDeleteButton,
schema, schema,
repeaterId, repeaterId,
notificationOverride, notificationOverride,
} }
const fetchSchema = async () => { const fetchSchema = async () => {
schema = (await fetchDatasourceSchema(dataSource)) || {} schema = (await fetchDatasourceSchema(dataSource)) || {}
} }

View File

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