diff --git a/packages/builder/src/builderStore/index.js b/packages/builder/src/builderStore/index.js
index ece17cb46f..dd54dcf13e 100644
--- a/packages/builder/src/builderStore/index.js
+++ b/packages/builder/src/builderStore/index.js
@@ -8,6 +8,7 @@ import { derived, get } from "svelte/store"
import { findComponent, findComponentPath } from "./componentUtils"
import { RoleUtils } from "@budibase/frontend-core"
import { createHistoryStore } from "builderStore/store/history"
+import { cloneDeep } from "lodash/fp"
export const store = getFrontendStore()
export const automationStore = getAutomationStore()
@@ -69,7 +70,14 @@ export const selectedComponent = derived(
if (!$selectedScreen || !$store.selectedComponentId) {
return null
}
- return findComponent($selectedScreen?.props, $store.selectedComponentId)
+ const selected = findComponent(
+ $selectedScreen?.props,
+ $store.selectedComponentId
+ )
+
+ const clone = selected ? cloneDeep(selected) : selected
+ store.actions.components.migrateSettings(clone)
+ return clone
}
)
diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js
index a4729b4a8a..aaa0eb0184 100644
--- a/packages/builder/src/builderStore/store/frontend.js
+++ b/packages/builder/src/builderStore/store/frontend.js
@@ -85,6 +85,7 @@ const INITIAL_FRONTEND_STATE = {
selectedScreenId: null,
selectedComponentId: null,
selectedLayoutId: null,
+ hoverComponentId: null,
// Client state
selectedComponentInstance: null,
@@ -112,7 +113,7 @@ export const getFrontendStore = () => {
}
let clone = cloneDeep(screen)
const result = patchFn(clone)
-
+ // An explicit false result means skip this change
if (result === false) {
return
}
@@ -601,6 +602,36 @@ export const getFrontendStore = () => {
// Finally try an external table
return validTables.find(table => table.sourceType === DB_TYPE_EXTERNAL)
},
+ migrateSettings: enrichedComponent => {
+ const componentPrefix = "@budibase/standard-components"
+ let migrated = false
+
+ if (enrichedComponent?._component == `${componentPrefix}/formblock`) {
+ // Use default config if the 'buttons' prop has never been initialised
+ if (!("buttons" in enrichedComponent)) {
+ enrichedComponent["buttons"] =
+ Utils.buildDynamicButtonConfig(enrichedComponent)
+ migrated = true
+ } else if (enrichedComponent["buttons"] == null) {
+ // Ignore legacy config if 'buttons' has been reset by 'resetOn'
+ const { _id, actionType, dataSource } = enrichedComponent
+ enrichedComponent["buttons"] = Utils.buildDynamicButtonConfig({
+ _id,
+ actionType,
+ dataSource,
+ })
+ migrated = true
+ }
+
+ // Ensure existing Formblocks position their buttons at the top.
+ if (!("buttonPosition" in enrichedComponent)) {
+ enrichedComponent["buttonPosition"] = "top"
+ migrated = true
+ }
+ }
+
+ return migrated
+ },
enrichEmptySettings: (component, opts) => {
if (!component?._component) {
return
@@ -672,7 +703,6 @@ export const getFrontendStore = () => {
component[setting.key] = setting.defaultValue
}
}
-
// Validate non-empty settings
else {
if (setting.type === "dataProvider") {
@@ -722,6 +752,9 @@ export const getFrontendStore = () => {
useDefaultValues: true,
})
+ // Migrate nested component settings
+ store.actions.components.migrateSettings(instance)
+
// Add any extra properties the component needs
let extras = {}
if (definition.hasChildren) {
@@ -845,7 +878,16 @@ export const getFrontendStore = () => {
if (!component) {
return false
}
- return patchFn(component, screen)
+
+ // Mutates the fetched component with updates
+ const patchResult = patchFn(component, screen)
+
+ // Mutates the component with any required settings updates
+ const migrated = store.actions.components.migrateSettings(component)
+
+ // Returning an explicit false signifies that we should skip this
+ // update. If we migrated something, ensure we never skip.
+ return migrated ? null : patchResult
}
await store.actions.screens.patch(patchScreen, screenId)
},
@@ -1247,9 +1289,13 @@ export const getFrontendStore = () => {
const settings = getComponentSettings(component._component)
const updatedSetting = settings.find(setting => setting.key === name)
- const resetFields = settings.filter(
- setting => name === setting.resetOn
- )
+ // Can be a single string or array of strings
+ const resetFields = settings.filter(setting => {
+ return (
+ name === setting.resetOn ||
+ (Array.isArray(setting.resetOn) && setting.resetOn.includes(name))
+ )
+ })
resetFields?.forEach(setting => {
component[setting.key] = null
})
@@ -1271,6 +1317,7 @@ export const getFrontendStore = () => {
})
}
component[name] = value
+ return true
}
},
requestEjectBlock: componentId => {
@@ -1278,7 +1325,6 @@ export const getFrontendStore = () => {
},
handleEjectBlock: async (componentId, ejectedDefinition) => {
let nextSelectedComponentId
-
await store.actions.screens.patch(screen => {
const block = findComponent(screen.props, componentId)
const parent = findComponentParent(screen.props, componentId)
diff --git a/packages/builder/src/components/common/NavItem.svelte b/packages/builder/src/components/common/NavItem.svelte
index 35846525af..02cef82e80 100644
--- a/packages/builder/src/components/common/NavItem.svelte
+++ b/packages/builder/src/components/common/NavItem.svelte
@@ -23,6 +23,7 @@
export let showTooltip = false
export let selectedBy = null
export let compact = false
+ export let hovering = false
const scrollApi = getContext("scroll")
const dispatch = createEventDispatcher()
@@ -61,6 +62,7 @@
{item.label || item.field}
-
+ {
+ e.stopPropagation()
+ }}
+ on:change={onToggle(item)}
+ text=""
+ value={item.active}
+ thin
+ />
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte
index 45fe005ceb..65ea172012 100644
--- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte
@@ -36,12 +36,14 @@
// Determine selected component ID
$: selectedComponentId = $store.selectedComponentId
+ $: hoverComponentId = $store.hoverComponentId
$: previewData = {
appId: $store.appId,
layout,
screen,
selectedComponentId,
+ hoverComponentId,
theme: $store.theme,
customTheme: $store.customTheme,
previewDevice: $store.previewDevice,
@@ -117,6 +119,8 @@
error = event.error || "An unknown error occurred"
} else if (type === "select-component" && data.id) {
$store.selectedComponentId = data.id
+ } else if (type === "hover-component" && data.id) {
+ $store.hoverComponentId = data.id
} else if (type === "update-prop") {
await store.actions.components.updateSetting(data.prop, data.value)
} else if (type === "update-styles") {
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/ComponentTree.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/ComponentTree.svelte
index 1ad522cbd4..8d4d64e4be 100644
--- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/ComponentTree.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/ComponentTree.svelte
@@ -89,6 +89,17 @@
}
return findComponentPath($selectedComponent, component._id)?.length > 0
}
+
+ const handleMouseover = componentId => {
+ if ($store.hoverComponentId !== componentId) {
+ $store.hoverComponentId = componentId
+ }
+ }
+ const handleMouseout = componentId => {
+ if ($store.hoverComponentId === componentId) {
+ $store.hoverComponentId = null
+ }
+ }
@@ -109,6 +120,9 @@
on:dragover={dragover(component, index)}
on:iconClick={() => toggleNodeOpen(component._id)}
on:drop={onDrop}
+ hovering={$store.hoverComponentId === component._id}
+ on:mouseenter={() => handleMouseover(component._id)}
+ on:mouseleave={() => handleMouseout(component._id)}
text={getComponentText(component)}
icon={getComponentIcon(component)}
iconTooltip={getComponentName(component)}
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/index.svelte
index 67b501e141..1e2ea47e63 100644
--- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/index.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/index.svelte
@@ -32,6 +32,17 @@
const handleScroll = e => {
scrolling = e.target.scrollTop !== 0
}
+
+ const handleMouseover = componentId => {
+ if ($store.hoverComponentId !== componentId) {
+ $store.hoverComponentId = componentId
+ }
+ }
+ const handleMouseout = componentId => {
+ if ($store.hoverComponentId === componentId) {
+ $store.hoverComponentId = null
+ }
+ }
@@ -57,6 +68,12 @@
on:click={() => {
$store.selectedComponentId = `${$store.selectedScreenId}-screen`
}}
+ hovering={$store.hoverComponentId ===
+ `${$store.selectedScreenId}-screen`}
+ on:mouseenter={() =>
+ handleMouseover(`${$store.selectedScreenId}-screen`)}
+ on:mouseleave={() =>
+ handleMouseout(`${$store.selectedScreenId}-screen`)}
id={`component-screen`}
selectedBy={$userSelectedResourceMap[
`${$store.selectedScreenId}-screen`
@@ -78,6 +95,12 @@
on:click={() => {
$store.selectedComponentId = `${$store.selectedScreenId}-navigation`
}}
+ hovering={$store.hoverComponentId ===
+ `${$store.selectedScreenId}-navigation`}
+ on:mouseenter={() =>
+ handleMouseover(`${$store.selectedScreenId}-navigation`)}
+ on:mouseleave={() =>
+ handleMouseout(`${$store.selectedScreenId}-navigation`)}
id={`component-nav`}
selectedBy={$userSelectedResourceMap[
`${$store.selectedScreenId}-navigation`
diff --git a/packages/client/manifest.json b/packages/client/manifest.json
index fdb0ad9db1..3b99ddb7b5 100644
--- a/packages/client/manifest.json
+++ b/packages/client/manifest.json
@@ -6112,54 +6112,32 @@
}
]
},
+ {
+ "tag": "style",
+ "type": "select",
+ "label": "Button position",
+ "key": "buttonPosition",
+ "options": [
+ {
+ "label": "Bottom",
+ "value": "bottom"
+ },
+ {
+ "label": "Top",
+ "value": "top"
+ }
+ ],
+ "defaultValue": "bottom"
+ },
{
"section": true,
"name": "Buttons",
- "dependsOn": {
- "setting": "actionType",
- "value": "View",
- "invert": true
- },
"settings": [
{
- "type": "text",
- "key": "saveButtonLabel",
- "label": "Save button",
+ "type": "buttonConfiguration",
+ "key": "buttons",
"nested": true,
- "defaultValue": "Save"
- },
- {
- "type": "text",
- "key": "deleteButtonLabel",
- "label": "Delete button",
- "nested": true,
- "defaultValue": "Delete",
- "dependsOn": {
- "setting": "actionType",
- "value": "Update"
- }
- },
- {
- "type": "url",
- "label": "Navigate after button press",
- "key": "actionUrl",
- "placeholder": "Choose a screen",
- "dependsOn": {
- "setting": "actionType",
- "value": "View",
- "invert": true
- }
- },
- {
- "type": "boolean",
- "label": "Hide notifications",
- "key": "notificationOverride",
- "defaultValue": false,
- "dependsOn": {
- "setting": "actionType",
- "value": "View",
- "invert": true
- }
+ "resetOn": ["actionType", "dataSource"]
}
]
},
diff --git a/packages/client/src/components/app/blocks/TableBlock.svelte b/packages/client/src/components/app/blocks/TableBlock.svelte
index 1cb77cb3e5..c8b6a07e3d 100644
--- a/packages/client/src/components/app/blocks/TableBlock.svelte
+++ b/packages/client/src/components/app/blocks/TableBlock.svelte
@@ -5,6 +5,7 @@
import BlockComponent from "components/BlockComponent.svelte"
import { makePropSafe as safe } from "@budibase/string-templates"
import { enrichSearchColumns, enrichFilter } from "utils/blocks.js"
+ import { Utils } from "@budibase/frontend-core"
export let title
export let dataSource
@@ -33,6 +34,7 @@
export let notificationOverride
const { fetchDatasourceSchema, API } = getContext("sdk")
+ const component = getContext("component")
const stateKey = `ID_${generate()}`
let formId
@@ -259,16 +261,25 @@
name="Details form block"
type="formblock"
bind:id={detailsFormBlockId}
+ context="form-edit"
props={{
dataSource,
- saveButtonLabel: sidePanelSaveLabel || "Save", //always show
- deleteButtonLabel: deleteLabel,
+ buttonPosition: "top",
+ buttons: Utils.buildDynamicButtonConfig({
+ _id: $component.id + "-form-edit",
+ showDeleteButton: deleteLabel !== "",
+ showSaveButton: true,
+ saveButtonLabel: sidePanelSaveLabel || "Save",
+ deleteButtonLabel: deleteLabel,
+ notificationOverride,
+ actionType: "Update",
+ dataSource,
+ }),
actionType: "Update",
rowId: `{{ ${safe("state")}.${safe(stateKey)} }}`,
fields: sidePanelFields || normalFields,
title: editTitle,
labelPosition: "left",
- notificationOverride,
}}
/>
@@ -284,16 +295,23 @@
diff --git a/packages/client/src/components/app/blocks/form/FormBlock.svelte b/packages/client/src/components/app/blocks/form/FormBlock.svelte
index e4d3b55eff..f23ecf451d 100644
--- a/packages/client/src/components/app/blocks/form/FormBlock.svelte
+++ b/packages/client/src/components/app/blocks/form/FormBlock.svelte
@@ -4,28 +4,31 @@
import Block from "components/Block.svelte"
import { makePropSafe as safe } from "@budibase/string-templates"
import InnerFormBlock from "./InnerFormBlock.svelte"
+ import { Utils } from "@budibase/frontend-core"
export let actionType
export let dataSource
export let size
export let disabled
export let fields
+ export let buttons
+ export let buttonPosition
+
export let title
export let description
- export let showDeleteButton
- export let showSaveButton
- export let saveButtonLabel
- export let deleteButtonLabel
export let rowId
export let actionUrl
export let noRowsMessage
export let notificationOverride
- // Accommodate old config to ensure delete button does not reappear
- $: deleteLabel = showDeleteButton === false ? "" : deleteButtonLabel?.trim()
- $: saveLabel = showSaveButton === false ? "" : saveButtonLabel?.trim()
+ // Legacy
+ export let showDeleteButton
+ export let showSaveButton
+ export let saveButtonLabel
+ export let deleteButtonLabel
const { fetchDatasourceSchema } = getContext("sdk")
+ const component = getContext("component")
const convertOldFieldFormat = fields => {
if (!fields) {
@@ -98,11 +101,23 @@
fields: fieldsOrDefault,
title,
description,
- saveButtonLabel: saveLabel,
- deleteButtonLabel: deleteLabel,
schema,
repeaterId,
notificationOverride,
+ buttons:
+ buttons ||
+ Utils.buildDynamicButtonConfig({
+ _id: $component.id,
+ showDeleteButton,
+ showSaveButton,
+ saveButtonLabel,
+ deleteButtonLabel,
+ notificationOverride,
+ actionType,
+ actionUrl,
+ dataSource,
+ }),
+ buttonPosition: buttons ? buttonPosition : "top",
}
const fetchSchema = async () => {
schema = (await fetchDatasourceSchema(dataSource)) || {}
diff --git a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
index 52ef3ac80c..24d4cfa14c 100644
--- a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
+++ b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
@@ -1,22 +1,18 @@
-