Merge branch 'master' of github.com:Budibase/budibase into fix/attachment-improvements

This commit is contained in:
mike12345567 2023-12-08 12:05:16 +00:00
commit dde2fdb6a3
18 changed files with 375 additions and 184 deletions

View File

@ -30,6 +30,13 @@ elif [[ "${TARGETBUILD}" = "single" ]]; then
# mount, so we use that for all persistent data. # mount, so we use that for all persistent data.
sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini
sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini
elif [[ "${TARGETBUILD}" = "docker-compose" ]]; then
# We remove the database_dir and view_index_dir settings from the local.ini
# in docker-compose because it will default to /opt/couchdb/data which is what
# our docker-compose was using prior to us switching to using our own CouchDB
# image.
sed -i "s#^database_dir.*\$##g" /opt/couchdb/etc/local.ini
sed -i "s#^view_index_dir.*\$##g" /opt/couchdb/etc/local.ini
elif [[ -n $KUBERNETES_SERVICE_HOST ]]; then elif [[ -n $KUBERNETES_SERVICE_HOST ]]; then
# In Kubernetes the directory /opt/couchdb/data has a persistent volume # In Kubernetes the directory /opt/couchdb/data has a persistent volume
# mount for storing database data. # mount for storing database data.

View File

@ -8,6 +8,7 @@ import { derived, get } from "svelte/store"
import { findComponent, findComponentPath } from "./componentUtils" import { findComponent, findComponentPath } from "./componentUtils"
import { RoleUtils } from "@budibase/frontend-core" import { RoleUtils } from "@budibase/frontend-core"
import { createHistoryStore } from "builderStore/store/history" import { createHistoryStore } from "builderStore/store/history"
import { cloneDeep } from "lodash/fp"
export const store = getFrontendStore() export const store = getFrontendStore()
export const automationStore = getAutomationStore() export const automationStore = getAutomationStore()
@ -69,7 +70,14 @@ export const selectedComponent = derived(
if (!$selectedScreen || !$store.selectedComponentId) { if (!$selectedScreen || !$store.selectedComponentId) {
return null 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
} }
) )

View File

@ -85,6 +85,7 @@ const INITIAL_FRONTEND_STATE = {
selectedScreenId: null, selectedScreenId: null,
selectedComponentId: null, selectedComponentId: null,
selectedLayoutId: null, selectedLayoutId: null,
hoverComponentId: null,
// Client state // Client state
selectedComponentInstance: null, selectedComponentInstance: null,
@ -112,7 +113,7 @@ export const getFrontendStore = () => {
} }
let clone = cloneDeep(screen) let clone = cloneDeep(screen)
const result = patchFn(clone) const result = patchFn(clone)
// An explicit false result means skip this change
if (result === false) { if (result === false) {
return return
} }
@ -601,6 +602,36 @@ export const getFrontendStore = () => {
// Finally try an external table // Finally try an external table
return validTables.find(table => table.sourceType === DB_TYPE_EXTERNAL) 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) => { enrichEmptySettings: (component, opts) => {
if (!component?._component) { if (!component?._component) {
return return
@ -672,7 +703,6 @@ export const getFrontendStore = () => {
component[setting.key] = setting.defaultValue component[setting.key] = setting.defaultValue
} }
} }
// Validate non-empty settings // Validate non-empty settings
else { else {
if (setting.type === "dataProvider") { if (setting.type === "dataProvider") {
@ -722,6 +752,9 @@ export const getFrontendStore = () => {
useDefaultValues: true, useDefaultValues: true,
}) })
// Migrate nested component settings
store.actions.components.migrateSettings(instance)
// Add any extra properties the component needs // Add any extra properties the component needs
let extras = {} let extras = {}
if (definition.hasChildren) { if (definition.hasChildren) {
@ -845,7 +878,16 @@ export const getFrontendStore = () => {
if (!component) { if (!component) {
return false 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) await store.actions.screens.patch(patchScreen, screenId)
}, },
@ -1247,9 +1289,13 @@ export const getFrontendStore = () => {
const settings = getComponentSettings(component._component) const settings = getComponentSettings(component._component)
const updatedSetting = settings.find(setting => setting.key === name) const updatedSetting = settings.find(setting => setting.key === name)
const resetFields = settings.filter( // Can be a single string or array of strings
setting => name === setting.resetOn const resetFields = settings.filter(setting => {
return (
name === setting.resetOn ||
(Array.isArray(setting.resetOn) && setting.resetOn.includes(name))
) )
})
resetFields?.forEach(setting => { resetFields?.forEach(setting => {
component[setting.key] = null component[setting.key] = null
}) })
@ -1271,6 +1317,7 @@ export const getFrontendStore = () => {
}) })
} }
component[name] = value component[name] = value
return true
} }
}, },
requestEjectBlock: componentId => { requestEjectBlock: componentId => {
@ -1278,7 +1325,6 @@ export const getFrontendStore = () => {
}, },
handleEjectBlock: async (componentId, ejectedDefinition) => { handleEjectBlock: async (componentId, ejectedDefinition) => {
let nextSelectedComponentId let nextSelectedComponentId
await store.actions.screens.patch(screen => { await store.actions.screens.patch(screen => {
const block = findComponent(screen.props, componentId) const block = findComponent(screen.props, componentId)
const parent = findComponentParent(screen.props, componentId) const parent = findComponentParent(screen.props, componentId)

View File

@ -23,6 +23,7 @@
export let showTooltip = false export let showTooltip = false
export let selectedBy = null export let selectedBy = null
export let compact = false export let compact = false
export let hovering = false
const scrollApi = getContext("scroll") const scrollApi = getContext("scroll")
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -61,6 +62,7 @@
<div <div
class="nav-item" class="nav-item"
class:hovering
class:border class:border
class:selected class:selected
class:withActions class:withActions
@ -71,6 +73,8 @@
on:dragstart on:dragstart
on:dragover on:dragover
on:drop on:drop
on:mouseenter
on:mouseleave
on:click={onClick} on:click={onClick}
ondragover="return false" ondragover="return false"
ondragenter="return false" ondragenter="return false"
@ -152,15 +156,17 @@
--avatars-background: var(--spectrum-global-color-gray-200); --avatars-background: var(--spectrum-global-color-gray-200);
} }
.nav-item.selected { .nav-item.selected {
background-color: var(--spectrum-global-color-gray-300); background-color: var(--spectrum-global-color-gray-300) !important;
--avatars-background: var(--spectrum-global-color-gray-300); --avatars-background: var(--spectrum-global-color-gray-300);
color: var(--ink); color: var(--ink);
} }
.nav-item:hover { .nav-item:hover,
background-color: var(--spectrum-global-color-gray-300); .hovering {
background-color: var(--spectrum-global-color-gray-200);
--avatars-background: var(--spectrum-global-color-gray-300); --avatars-background: var(--spectrum-global-color-gray-300);
} }
.nav-item:hover .actions { .nav-item:hover .actions,
.hovering .actions {
visibility: visible; visibility: visible;
} }
.nav-item-content { .nav-item-content {

View File

@ -49,7 +49,15 @@
<div class="field-label">{item.label || item.field}</div> <div class="field-label">{item.label || item.field}</div>
</div> </div>
<div class="list-item-right"> <div class="list-item-right">
<Toggle on:change={onToggle(item)} text="" value={item.active} thin /> <Toggle
on:click={e => {
e.stopPropagation()
}}
on:change={onToggle(item)}
text=""
value={item.active}
thin
/>
</div> </div>
</div> </div>

View File

@ -36,12 +36,14 @@
// Determine selected component ID // Determine selected component ID
$: selectedComponentId = $store.selectedComponentId $: selectedComponentId = $store.selectedComponentId
$: hoverComponentId = $store.hoverComponentId
$: previewData = { $: previewData = {
appId: $store.appId, appId: $store.appId,
layout, layout,
screen, screen,
selectedComponentId, selectedComponentId,
hoverComponentId,
theme: $store.theme, theme: $store.theme,
customTheme: $store.customTheme, customTheme: $store.customTheme,
previewDevice: $store.previewDevice, previewDevice: $store.previewDevice,
@ -117,6 +119,8 @@
error = event.error || "An unknown error occurred" error = event.error || "An unknown error occurred"
} else if (type === "select-component" && data.id) { } else if (type === "select-component" && data.id) {
$store.selectedComponentId = data.id $store.selectedComponentId = data.id
} else if (type === "hover-component" && data.id) {
$store.hoverComponentId = data.id
} else if (type === "update-prop") { } else if (type === "update-prop") {
await store.actions.components.updateSetting(data.prop, data.value) await store.actions.components.updateSetting(data.prop, data.value)
} else if (type === "update-styles") { } else if (type === "update-styles") {

View File

@ -89,6 +89,17 @@
} }
return findComponentPath($selectedComponent, component._id)?.length > 0 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
}
}
</script> </script>
<ul> <ul>
@ -109,6 +120,9 @@
on:dragover={dragover(component, index)} on:dragover={dragover(component, index)}
on:iconClick={() => toggleNodeOpen(component._id)} on:iconClick={() => toggleNodeOpen(component._id)}
on:drop={onDrop} on:drop={onDrop}
hovering={$store.hoverComponentId === component._id}
on:mouseenter={() => handleMouseover(component._id)}
on:mouseleave={() => handleMouseout(component._id)}
text={getComponentText(component)} text={getComponentText(component)}
icon={getComponentIcon(component)} icon={getComponentIcon(component)}
iconTooltip={getComponentName(component)} iconTooltip={getComponentName(component)}

View File

@ -32,6 +32,17 @@
const handleScroll = e => { const handleScroll = e => {
scrolling = e.target.scrollTop !== 0 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
}
}
</script> </script>
<div class="components"> <div class="components">
@ -57,6 +68,12 @@
on:click={() => { on:click={() => {
$store.selectedComponentId = `${$store.selectedScreenId}-screen` $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`} id={`component-screen`}
selectedBy={$userSelectedResourceMap[ selectedBy={$userSelectedResourceMap[
`${$store.selectedScreenId}-screen` `${$store.selectedScreenId}-screen`
@ -78,6 +95,12 @@
on:click={() => { on:click={() => {
$store.selectedComponentId = `${$store.selectedScreenId}-navigation` $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`} id={`component-nav`}
selectedBy={$userSelectedResourceMap[ selectedBy={$userSelectedResourceMap[
`${$store.selectedScreenId}-navigation` `${$store.selectedScreenId}-navigation`

View File

@ -15,7 +15,8 @@
"pkg": "pkg . --out-path build --no-bytecode --public --public-packages \"*\" -C GZip", "pkg": "pkg . --out-path build --no-bytecode --public --public-packages \"*\" -C GZip",
"build": "yarn prebuild && yarn rename && yarn tsc && yarn pkg && yarn postbuild", "build": "yarn prebuild && yarn rename && yarn tsc && yarn pkg && yarn postbuild",
"check:types": "tsc -p tsconfig.json --noEmit --paths null", "check:types": "tsc -p tsconfig.json --noEmit --paths null",
"postbuild": "rm -rf prebuilds 2> /dev/null" "postbuild": "rm -rf prebuilds 2> /dev/null",
"start": "ts-node ./src/index.ts"
}, },
"pkg": { "pkg": {
"targets": [ "targets": [

View File

@ -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, "section": true,
"name": "Buttons", "name": "Buttons",
"dependsOn": {
"setting": "actionType",
"value": "View",
"invert": true
},
"settings": [ "settings": [
{ {
"type": "text", "type": "buttonConfiguration",
"key": "saveButtonLabel", "key": "buttons",
"label": "Save button",
"nested": true, "nested": true,
"defaultValue": "Save" "resetOn": ["actionType", "dataSource"]
},
{
"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
}
} }
] ]
}, },

View File

@ -5,6 +5,7 @@
import BlockComponent from "components/BlockComponent.svelte" import BlockComponent from "components/BlockComponent.svelte"
import { makePropSafe as safe } from "@budibase/string-templates" import { makePropSafe as safe } from "@budibase/string-templates"
import { enrichSearchColumns, enrichFilter } from "utils/blocks.js" import { enrichSearchColumns, enrichFilter } from "utils/blocks.js"
import { Utils } from "@budibase/frontend-core"
export let title export let title
export let dataSource export let dataSource
@ -33,6 +34,7 @@
export let notificationOverride export let notificationOverride
const { fetchDatasourceSchema, API } = getContext("sdk") const { fetchDatasourceSchema, API } = getContext("sdk")
const component = getContext("component")
const stateKey = `ID_${generate()}` const stateKey = `ID_${generate()}`
let formId let formId
@ -259,16 +261,25 @@
name="Details form block" name="Details form block"
type="formblock" type="formblock"
bind:id={detailsFormBlockId} bind:id={detailsFormBlockId}
context="form-edit"
props={{ props={{
dataSource, dataSource,
saveButtonLabel: sidePanelSaveLabel || "Save", //always show buttonPosition: "top",
buttons: Utils.buildDynamicButtonConfig({
_id: $component.id + "-form-edit",
showDeleteButton: deleteLabel !== "",
showSaveButton: true,
saveButtonLabel: sidePanelSaveLabel || "Save",
deleteButtonLabel: deleteLabel, deleteButtonLabel: deleteLabel,
notificationOverride,
actionType: "Update",
dataSource,
}),
actionType: "Update", actionType: "Update",
rowId: `{{ ${safe("state")}.${safe(stateKey)} }}`, rowId: `{{ ${safe("state")}.${safe(stateKey)} }}`,
fields: sidePanelFields || normalFields, fields: sidePanelFields || normalFields,
title: editTitle, title: editTitle,
labelPosition: "left", labelPosition: "left",
notificationOverride,
}} }}
/> />
</BlockComponent> </BlockComponent>
@ -284,16 +295,23 @@
<BlockComponent <BlockComponent
name="New row form block" name="New row form block"
type="formblock" type="formblock"
context="form-new"
props={{ props={{
dataSource, dataSource,
showSaveButton: true, buttonPosition: "top",
buttons: Utils.buildDynamicButtonConfig({
_id: $component.id + "-form-new",
showDeleteButton: false, showDeleteButton: false,
saveButtonLabel: sidePanelSaveLabel || "Save", //always show showSaveButton: true,
saveButtonLabel: "Save",
notificationOverride,
actionType: "Create",
dataSource,
}),
actionType: "Create", actionType: "Create",
fields: sidePanelFields || normalFields, fields: sidePanelFields || normalFields,
title: "Create Row", title: "Create Row",
labelPosition: "left", labelPosition: "left",
notificationOverride,
}} }}
/> />
</BlockComponent> </BlockComponent>

View File

@ -4,28 +4,31 @@
import Block from "components/Block.svelte" import Block from "components/Block.svelte"
import { makePropSafe as safe } from "@budibase/string-templates" import { makePropSafe as safe } from "@budibase/string-templates"
import InnerFormBlock from "./InnerFormBlock.svelte" import InnerFormBlock from "./InnerFormBlock.svelte"
import { Utils } from "@budibase/frontend-core"
export let actionType export let actionType
export let dataSource export let dataSource
export let size export let size
export let disabled export let disabled
export let fields export let fields
export let buttons
export let buttonPosition
export let title export let title
export let description export let description
export let showDeleteButton
export let showSaveButton
export let saveButtonLabel
export let deleteButtonLabel
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 // Legacy
$: deleteLabel = showDeleteButton === false ? "" : deleteButtonLabel?.trim() export let showDeleteButton
$: saveLabel = showSaveButton === false ? "" : saveButtonLabel?.trim() export let showSaveButton
export let saveButtonLabel
export let deleteButtonLabel
const { fetchDatasourceSchema } = getContext("sdk") const { fetchDatasourceSchema } = getContext("sdk")
const component = getContext("component")
const convertOldFieldFormat = fields => { const convertOldFieldFormat = fields => {
if (!fields) { if (!fields) {
@ -98,11 +101,23 @@
fields: fieldsOrDefault, fields: fieldsOrDefault,
title, title,
description, description,
saveButtonLabel: saveLabel,
deleteButtonLabel: deleteLabel,
schema, schema,
repeaterId, repeaterId,
notificationOverride, notificationOverride,
buttons:
buttons ||
Utils.buildDynamicButtonConfig({
_id: $component.id,
showDeleteButton,
showSaveButton,
saveButtonLabel,
deleteButtonLabel,
notificationOverride,
actionType,
actionUrl,
dataSource,
}),
buttonPosition: buttons ? buttonPosition : "top",
} }
const fetchSchema = async () => { const fetchSchema = async () => {
schema = (await fetchDatasourceSchema(dataSource)) || {} schema = (await fetchDatasourceSchema(dataSource)) || {}

View File

@ -1,22 +1,18 @@
<script> <script>
import BlockComponent from "components/BlockComponent.svelte" import BlockComponent from "components/BlockComponent.svelte"
import Placeholder from "components/app/Placeholder.svelte" import Placeholder from "components/app/Placeholder.svelte"
import { makePropSafe as safe } from "@budibase/string-templates"
import { getContext } from "svelte" import { getContext } from "svelte"
export let dataSource export let dataSource
export let actionUrl
export let actionType export let actionType
export let size export let size
export let disabled export let disabled
export let fields export let fields
export let title export let title
export let description export let description
export let saveButtonLabel export let buttons
export let deleteButtonLabel export let buttonPosition = "bottom"
export let schema export let schema
export let repeaterId
export let notificationOverride
const FieldTypeToComponentMap = { const FieldTypeToComponentMap = {
string: "stringfield", string: "stringfield",
@ -37,74 +33,7 @@
let formId let formId
$: onSave = [ $: renderHeader = buttons || title
{
"##eventHandlerType": "Validate Form",
parameters: {
componentId: formId,
},
},
{
"##eventHandlerType": "Save Row",
parameters: {
providerId: formId,
tableId: dataSource?.resourceId,
notificationOverride,
},
},
{
"##eventHandlerType": "Close Screen Modal",
},
{
"##eventHandlerType": "Close Side Panel",
},
// Clear a create form once submitted
...(actionType !== "Create"
? []
: [
{
"##eventHandlerType": "Clear Form",
parameters: {
componentId: formId,
},
},
]),
{
"##eventHandlerType": "Navigate To",
parameters: {
url: actionUrl,
},
},
]
$: onDelete = [
{
"##eventHandlerType": "Delete Row",
parameters: {
confirm: true,
tableId: dataSource?.resourceId,
rowId: `{{ ${safe(repeaterId)}.${safe("_id")} }}`,
revId: `{{ ${safe(repeaterId)}.${safe("_rev")} }}`,
notificationOverride,
},
},
{
"##eventHandlerType": "Close Screen Modal",
},
{
"##eventHandlerType": "Close Side Panel",
},
{
"##eventHandlerType": "Navigate To",
parameters: {
url: actionUrl,
},
},
]
$: renderDeleteButton = deleteButtonLabel && actionType === "Update"
$: renderSaveButton = saveButtonLabel && actionType !== "View"
$: renderButtons = renderDeleteButton || renderSaveButton
$: renderHeader = renderButtons || title
const getComponentForField = field => { const getComponentForField = field => {
const fieldSchemaName = field.field || field.name const fieldSchemaName = field.field || field.name
@ -184,43 +113,15 @@
props={{ text: title || "" }} props={{ text: title || "" }}
order={0} order={0}
/> />
{#if renderButtons} {#if buttonPosition == "top"}
<BlockComponent <BlockComponent
type="container" type="buttongroup"
props={{ props={{
direction: "row", buttons,
hAlign: "stretch",
vAlign: "center",
gap: "M",
wrap: true,
}}
order={1}
>
{#if renderDeleteButton}
<BlockComponent
type="button"
props={{
text: deleteButtonLabel,
onClick: onDelete,
quiet: true,
type: "secondary",
}} }}
order={0} order={0}
/> />
{/if} {/if}
{#if renderSaveButton}
<BlockComponent
type="button"
props={{
text: saveButtonLabel,
onClick: onSave,
type: "cta",
}}
order={1}
/>
{/if}
</BlockComponent>
{/if}
</BlockComponent> </BlockComponent>
</BlockComponent> </BlockComponent>
{/if} {/if}
@ -245,6 +146,20 @@
</BlockComponent> </BlockComponent>
{/key} {/key}
</BlockComponent> </BlockComponent>
{#if buttonPosition === "bottom"}
<BlockComponent
type="buttongroup"
props={{
buttons,
}}
styles={{
normal: {
"margin-top": "16",
},
}}
order={1}
/>
{/if}
</BlockComponent> </BlockComponent>
{:else} {:else}
<Placeholder <Placeholder

View File

@ -3,8 +3,7 @@
import IndicatorSet from "./IndicatorSet.svelte" import IndicatorSet from "./IndicatorSet.svelte"
import { builderStore, dndIsDragging } from "stores" import { builderStore, dndIsDragging } from "stores"
let componentId $: componentId = $builderStore.hoverComponentId
$: zIndex = componentId === $builderStore.selectedComponentId ? 900 : 920 $: zIndex = componentId === $builderStore.selectedComponentId ? 900 : 920
const onMouseOver = e => { const onMouseOver = e => {
@ -24,12 +23,12 @@
} }
if (newId !== componentId) { if (newId !== componentId) {
componentId = newId builderStore.actions.hoverComponent(newId)
} }
} }
const onMouseLeave = () => { const onMouseLeave = () => {
componentId = null builderStore.actions.hoverComponent(null)
} }
onMount(() => { onMount(() => {

View File

@ -32,6 +32,7 @@ const loadBudibase = async () => {
layout: window["##BUDIBASE_PREVIEW_LAYOUT##"], layout: window["##BUDIBASE_PREVIEW_LAYOUT##"],
screen: window["##BUDIBASE_PREVIEW_SCREEN##"], screen: window["##BUDIBASE_PREVIEW_SCREEN##"],
selectedComponentId: window["##BUDIBASE_SELECTED_COMPONENT_ID##"], selectedComponentId: window["##BUDIBASE_SELECTED_COMPONENT_ID##"],
hoverComponentId: window["##BUDIBASE_HOVER_COMPONENT_ID##"],
previewId: window["##BUDIBASE_PREVIEW_ID##"], previewId: window["##BUDIBASE_PREVIEW_ID##"],
theme: window["##BUDIBASE_PREVIEW_THEME##"], theme: window["##BUDIBASE_PREVIEW_THEME##"],
customTheme: window["##BUDIBASE_PREVIEW_CUSTOM_THEME##"], customTheme: window["##BUDIBASE_PREVIEW_CUSTOM_THEME##"],

View File

@ -8,6 +8,7 @@ const createBuilderStore = () => {
inBuilder: false, inBuilder: false,
screen: null, screen: null,
selectedComponentId: null, selectedComponentId: null,
hoverComponentId: null,
editMode: false, editMode: false,
previewId: null, previewId: null,
theme: null, theme: null,
@ -23,6 +24,16 @@ const createBuilderStore = () => {
} }
const store = writable(initialState) const store = writable(initialState)
const actions = { const actions = {
hoverComponent: id => {
if (id === get(store).hoverComponentId) {
return
}
store.update(state => ({
...state,
hoverComponentId: id,
}))
eventStore.actions.dispatchEvent("hover-component", { id })
},
selectComponent: id => { selectComponent: id => {
if (id === get(store).selectedComponentId) { if (id === get(store).selectedComponentId) {
return return

View File

@ -1,3 +1,6 @@
import { makePropSafe as safe } from "@budibase/string-templates"
import { Helpers } from "@budibase/bbui"
/** /**
* Utility to wrap an async function and ensure all invocations happen * Utility to wrap an async function and ensure all invocations happen
* sequentially. * sequentially.
@ -106,3 +109,135 @@ export const domDebounce = callback => {
} }
} }
} }
/**
* Build the default FormBlock button configs per actionType
* Parse any legacy button config and mirror its the outcome
*
* @param {any} props
* */
export const buildDynamicButtonConfig = props => {
const {
_id,
actionType,
dataSource,
notificationOverride,
actionUrl,
showDeleteButton,
deleteButtonLabel,
showSaveButton,
saveButtonLabel,
} = props || {}
if (!_id) {
console.log("MISSING ID")
return
}
const formId = `${_id}-form`
const repeaterId = `${_id}-repeater`
const resourceId = dataSource?.resourceId
// Accommodate old config to ensure delete button does not reappear
const deleteText = showDeleteButton === false ? "" : deleteButtonLabel?.trim()
const saveText = showSaveButton === false ? "" : saveButtonLabel?.trim()
const onSave = [
{
"##eventHandlerType": "Validate Form",
parameters: {
componentId: formId,
},
},
{
"##eventHandlerType": "Save Row",
parameters: {
providerId: formId,
tableId: resourceId,
notificationOverride,
},
},
{
"##eventHandlerType": "Close Screen Modal",
},
{
"##eventHandlerType": "Close Side Panel",
},
// Clear a create form once submitted
...(actionType !== "Create"
? []
: [
{
"##eventHandlerType": "Clear Form",
parameters: {
componentId: formId,
},
},
]),
...(actionUrl
? [
{
"##eventHandlerType": "Navigate To",
parameters: {
url: actionUrl,
},
},
]
: []),
]
const onDelete = [
{
"##eventHandlerType": "Delete Row",
parameters: {
confirm: true,
tableId: resourceId,
rowId: `{{ ${safe(repeaterId)}.${safe("_id")} }}`,
revId: `{{ ${safe(repeaterId)}.${safe("_rev")} }}`,
notificationOverride,
},
},
{
"##eventHandlerType": "Close Screen Modal",
},
{
"##eventHandlerType": "Close Side Panel",
},
...(actionUrl
? [
{
"##eventHandlerType": "Navigate To",
parameters: {
url: actionUrl,
},
},
]
: []),
]
const defaultButtons = []
if (["Update", "Create"].includes(actionType) && showSaveButton !== false) {
defaultButtons.push({
text: saveText || "Save",
_id: Helpers.uuid(),
_component: "@budibase/standard-components/button",
onClick: onSave,
type: "cta",
})
}
if (actionType == "Update" && showDeleteButton !== false) {
defaultButtons.push({
text: deleteText || "Delete",
_id: Helpers.uuid(),
_component: "@budibase/standard-components/button",
onClick: onDelete,
quiet: true,
type: "secondary",
})
}
return defaultButtons
}

View File

@ -63,6 +63,7 @@
// Extract data from message // Extract data from message
const { const {
selectedComponentId, selectedComponentId,
hoverComponentId,
layout, layout,
screen, screen,
appId, appId,
@ -81,6 +82,7 @@
window["##BUDIBASE_PREVIEW_LAYOUT##"] = layout window["##BUDIBASE_PREVIEW_LAYOUT##"] = layout
window["##BUDIBASE_PREVIEW_SCREEN##"] = screen window["##BUDIBASE_PREVIEW_SCREEN##"] = screen
window["##BUDIBASE_SELECTED_COMPONENT_ID##"] = selectedComponentId window["##BUDIBASE_SELECTED_COMPONENT_ID##"] = selectedComponentId
window["##BUDIBASE_HOVER_COMPONENT_ID##"] = hoverComponentId
window["##BUDIBASE_PREVIEW_ID##"] = Math.random() window["##BUDIBASE_PREVIEW_ID##"] = Math.random()
window["##BUDIBASE_PREVIEW_THEME##"] = theme window["##BUDIBASE_PREVIEW_THEME##"] = theme
window["##BUDIBASE_PREVIEW_CUSTOM_THEME##"] = customTheme window["##BUDIBASE_PREVIEW_CUSTOM_THEME##"] = customTheme