Merge pull request #11597 from Budibase/feature/form-block-ux-updates
Form/Table block UX Updates
This commit is contained in:
commit
77655be571
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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", {
|
||||||
|
|
|
@ -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%;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -74,6 +74,8 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Drawer
|
<Drawer
|
||||||
|
on:drawerHide
|
||||||
|
on:drawerShow
|
||||||
{fillWidth}
|
{fillWidth}
|
||||||
bind:this={bindingDrawer}
|
bind:this={bindingDrawer}
|
||||||
{title}
|
{title}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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%;
|
||||||
|
|
|
@ -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>
|
</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>
|
||||||
|
|
|
@ -100,6 +100,8 @@
|
||||||
{key}
|
{key}
|
||||||
{type}
|
{type}
|
||||||
{...props}
|
{...props}
|
||||||
|
on:drawerHide
|
||||||
|
on:drawerShow
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if info}
|
{#if info}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)) || {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
Loading…
Reference in New Issue