diff --git a/packages/bbui/src/Modal/Modal.svelte b/packages/bbui/src/Modal/Modal.svelte
index 4656be69d1..dec1455d0c 100644
--- a/packages/bbui/src/Modal/Modal.svelte
+++ b/packages/bbui/src/Modal/Modal.svelte
@@ -162,6 +162,7 @@
max-height: 100%;
}
.modal-inner-wrapper {
+ padding: 40px;
flex: 1 1 auto;
display: flex;
flex-direction: row;
@@ -176,7 +177,6 @@
border: 2px solid var(--spectrum-global-color-gray-200);
overflow: visible;
max-height: none;
- margin: 40px 0;
transform: none;
--spectrum-dialog-confirm-border-radius: var(
--spectrum-global-dimension-size-100
diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/CloseModal.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/CloseModal.svelte
new file mode 100644
index 0000000000..ed0ca2c72b
--- /dev/null
+++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/CloseModal.svelte
@@ -0,0 +1,8 @@
+
This action doesn't require any settings.
+
+
diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/OpenModal.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/OpenModal.svelte
new file mode 100644
index 0000000000..8e61b8763f
--- /dev/null
+++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/OpenModal.svelte
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/index.js b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/index.js
index 587993377d..606ee41d02 100644
--- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/index.js
+++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/index.js
@@ -21,5 +21,7 @@ export { default as ShowNotification } from "./ShowNotification.svelte"
export { default as PromptUser } from "./PromptUser.svelte"
export { default as OpenSidePanel } from "./OpenSidePanel.svelte"
export { default as CloseSidePanel } from "./CloseSidePanel.svelte"
+export { default as OpenModal } from "./OpenModal.svelte"
+export { default as CloseModal } from "./CloseModal.svelte"
export { default as ClearRowSelection } from "./ClearRowSelection.svelte"
export { default as DownloadFile } from "./DownloadFile.svelte"
diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json
index 2840a0d662..4022926e7f 100644
--- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json
+++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json
@@ -157,6 +157,18 @@
"component": "CloseSidePanel",
"dependsOnFeature": "sidePanel"
},
+ {
+ "name": "Open Modal",
+ "type": "application",
+ "component": "OpenModal",
+ "dependsOnFeature": "modal"
+ },
+ {
+ "name": "Close Modal",
+ "type": "application",
+ "component": "CloseModal",
+ "dependsOnFeature": "modal"
+ },
{
"name": "Clear Row Selection",
"type": "data",
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/NewComponentPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/NewComponentPanel.svelte
index c7c58a6e16..361e07a026 100644
--- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/NewComponentPanel.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/NewComponentPanel.svelte
@@ -59,7 +59,14 @@
// Build up list of illegal children from ancestors
let illegalChildren = definition.illegalChildren || []
path.forEach(ancestor => {
- if (ancestor._component === `@budibase/standard-components/sidepanel`) {
+ // Sidepanels and modals can be nested anywhere in the component tree, but really they are always rendered at the top level.
+ // Because of this, it doesn't make sense to carry over any parent illegal children to them, so the array is reset here.
+ if (
+ [
+ "@budibase/standard-components/sidepanel",
+ "@budibase/standard-components/modal",
+ ].includes(ancestor._component)
+ ) {
illegalChildren = []
}
const def = componentStore.getDefinition(ancestor._component)
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json
index ba6f403d81..ff58a66221 100644
--- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json
+++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json
@@ -14,7 +14,7 @@
{
"name": "Layout",
"icon": "ClassicGridView",
- "children": ["container", "section", "sidepanel"]
+ "children": ["container", "section", "sidepanel", "modal"]
},
{
"name": "Data",
diff --git a/packages/builder/src/stores/builder/screens.js b/packages/builder/src/stores/builder/screens.js
index 7339593960..b1bef10c36 100644
--- a/packages/builder/src/stores/builder/screens.js
+++ b/packages/builder/src/stores/builder/screens.js
@@ -125,7 +125,14 @@ export class ScreenStore extends BudiStore {
return
}
- if (type === "@budibase/standard-components/sidepanel") {
+ // Sidepanels and modals can be nested anywhere in the component tree, but really they are always rendered at the top level.
+ // Because of this, it doesn't make sense to carry over any parent illegal children to them, so the array is reset here.
+ if (
+ [
+ "@budibase/standard-components/sidepanel",
+ "@budibase/standard-components/modal",
+ ].includes(type)
+ ) {
illegalChildren = []
}
diff --git a/packages/client/manifest.json b/packages/client/manifest.json
index 38e9bd8a87..00b503626f 100644
--- a/packages/client/manifest.json
+++ b/packages/client/manifest.json
@@ -11,6 +11,7 @@
"continueIfAction": true,
"showNotificationAction": true,
"sidePanel": true,
+ "modal": true,
"skeletonLoader": true
},
"typeSupportPresets": {
@@ -6975,7 +6976,7 @@
"name": "Side Panel",
"icon": "RailRight",
"hasChildren": true,
- "illegalChildren": ["section", "sidepanel"],
+ "illegalChildren": ["section", "sidepanel", "modal"],
"showEmptyState": false,
"draggable": false,
"info": "Side panels are hidden by default. They will only be revealed when triggered by the 'Open Side Panel' action.",
@@ -6993,6 +6994,52 @@
}
]
},
+ "modal": {
+ "name": "Modal",
+ "icon": "MBox",
+ "hasChildren": true,
+ "illegalChildren": ["section", "modal", "sidepanel"],
+ "showEmptyState": false,
+ "draggable": false,
+ "info": "Modals are hidden by default. They will only be revealed when triggered by the 'Open Modal' action.",
+ "settings": [
+ {
+ "type": "boolean",
+ "key": "ignoreClicksOutside",
+ "label": "Ignore clicks outside",
+ "defaultValue": false
+ },
+ {
+ "type": "event",
+ "key": "onClose",
+ "label": "On close"
+ },
+ {
+ "type": "select",
+ "label": "Size",
+ "key": "size",
+ "defaultValue": "small",
+ "options": [
+ {
+ "label": "Small",
+ "value": "small"
+ },
+ {
+ "label": "Medium",
+ "value": "medium"
+ },
+ {
+ "label": "Large",
+ "value": "large"
+ },
+ {
+ "label": "Fullscreen",
+ "value": "fullscreen"
+ }
+ ]
+ }
+ ]
+ },
"rowexplorer": {
"block": true,
"name": "Row Explorer Block",
diff --git a/packages/client/src/components/ClientApp.svelte b/packages/client/src/components/ClientApp.svelte
index c1bdc92ac4..9bfb1192ea 100644
--- a/packages/client/src/components/ClientApp.svelte
+++ b/packages/client/src/components/ClientApp.svelte
@@ -20,6 +20,7 @@
devToolsEnabled,
environmentStore,
sidePanelStore,
+ modalStore,
} from "stores"
import NotificationDisplay from "components/overlay/NotificationDisplay.svelte"
import ConfirmationDisplay from "components/overlay/ConfirmationDisplay.svelte"
@@ -104,10 +105,15 @@
})
}
const handleHashChange = () => {
- const { open } = $sidePanelStore
- if (open) {
+ const { open: sidePanelOpen } = $sidePanelStore
+ if (sidePanelOpen) {
sidePanelStore.actions.close()
}
+
+ const { open: modalOpen } = $modalStore
+ if (modalOpen) {
+ modalStore.actions.close()
+ }
}
window.addEventListener("hashchange", handleHashChange)
return () => {
diff --git a/packages/client/src/components/app/Layout.svelte b/packages/client/src/components/app/Layout.svelte
index 72da3e9012..af74e14aa0 100644
--- a/packages/client/src/components/app/Layout.svelte
+++ b/packages/client/src/components/app/Layout.svelte
@@ -12,6 +12,7 @@
linkable,
builderStore,
sidePanelStore,
+ modalStore,
appStore,
} = sdk
const context = getContext("context")
@@ -77,6 +78,7 @@
!$builderStore.inBuilder &&
$sidePanelStore.open &&
!$sidePanelStore.ignoreClicksOutside
+
$: screenId = $builderStore.inBuilder
? `${$builderStore.screen?._id}-screen`
: "screen"
@@ -198,6 +200,7 @@
const handleClickLink = () => {
mobileOpen = false
sidePanelStore.actions.close()
+ modalStore.actions.close()
}
diff --git a/packages/client/src/components/app/Link.svelte b/packages/client/src/components/app/Link.svelte
index 7eddcc6fe5..1ddc63066d 100644
--- a/packages/client/src/components/app/Link.svelte
+++ b/packages/client/src/components/app/Link.svelte
@@ -1,7 +1,7 @@
+
+
+{#if !$builderStore.inBuilder || open}
+
+
+
+{/if}
+
+
diff --git a/packages/client/src/components/app/blocks/form/FormBlock.svelte b/packages/client/src/components/app/blocks/form/FormBlock.svelte
index d249569731..e3aa20ffa6 100644
--- a/packages/client/src/components/app/blocks/form/FormBlock.svelte
+++ b/packages/client/src/components/app/blocks/form/FormBlock.svelte
@@ -31,41 +31,23 @@
let schema
- $: formattedFields = convertOldFieldFormat(fields)
- $: fieldsOrDefault = getDefaultFields(formattedFields, schema)
$: fetchSchema(dataSource)
$: id = $component.id
- // We could simply spread $$props into the inner form and append our
- // additions, but that would create svelte warnings about unused props and
- // make maintenance in future more confusing as we typically always have a
- // proper mapping of schema settings to component exports, without having to
- // search multiple files
- $: innerProps = {
- dataSource,
- actionUrl,
- actionType,
- size,
- disabled,
- fields: fieldsOrDefault,
- title,
- description,
- schema,
- notificationOverride,
- buttons:
- buttons ||
- Utils.buildFormBlockButtonConfig({
- _id: id,
- showDeleteButton,
- showSaveButton,
- saveButtonLabel,
- deleteButtonLabel,
- notificationOverride,
- actionType,
- actionUrl,
- dataSource,
- }),
- buttonPosition: buttons ? buttonPosition : "top",
- }
+ $: formattedFields = convertOldFieldFormat(fields)
+ $: fieldsOrDefault = getDefaultFields(formattedFields, schema)
+ $: buttonsOrDefault =
+ buttons ||
+ Utils.buildFormBlockButtonConfig({
+ _id: id,
+ showDeleteButton,
+ showSaveButton,
+ saveButtonLabel,
+ deleteButtonLabel,
+ notificationOverride,
+ actionType,
+ actionUrl,
+ dataSource,
+ })
// Provide additional data context for live binding eval
export const getAdditionalDataContext = () => {
@@ -123,5 +105,18 @@
-
+
diff --git a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
index b0733f3f4b..0227107dd2 100644
--- a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
+++ b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
@@ -91,15 +91,13 @@
{#if description}
{/if}
- {#key fields}
-
-
- {#each fields as field, idx}
-
- {/each}
-
-
- {/key}
+
+
+ {#each fields as field, idx}
+
+ {/each}
+
+
{#if buttonPosition === "bottom"}
{
+ const initialState = {
+ contentId: null,
+ }
+ const store = writable(initialState)
+
+ const open = id => {
+ store.update(state => {
+ state.contentId = id
+ return state
+ })
+ }
+
+ const close = () => {
+ store.update(state => {
+ state.contentId = null
+ return state
+ })
+ }
+
+ return {
+ subscribe: store.subscribe,
+ actions: {
+ open,
+ close,
+ },
+ }
+}
+
+export const modalStore = createModalStore()
diff --git a/packages/client/src/utils/buttonActions.js b/packages/client/src/utils/buttonActions.js
index 482b36cdb8..bd220b8e85 100644
--- a/packages/client/src/utils/buttonActions.js
+++ b/packages/client/src/utils/buttonActions.js
@@ -12,6 +12,7 @@ import {
uploadStore,
rowSelectionStore,
sidePanelStore,
+ modalStore,
} from "stores"
import { API } from "api"
import { ActionTypes } from "constants"
@@ -436,6 +437,17 @@ const closeSidePanelHandler = () => {
sidePanelStore.actions.close()
}
+const openModalHandler = action => {
+ const { id } = action.parameters
+ if (id) {
+ modalStore.actions.open(id)
+ }
+}
+
+const closeModalHandler = () => {
+ modalStore.actions.close()
+}
+
const downloadFileHandler = async action => {
const { url, fileName } = action.parameters
try {
@@ -499,6 +511,8 @@ const handlerMap = {
["Prompt User"]: promptUserHandler,
["Open Side Panel"]: openSidePanelHandler,
["Close Side Panel"]: closeSidePanelHandler,
+ ["Open Modal"]: openModalHandler,
+ ["Close Modal"]: closeModalHandler,
["Download File"]: downloadFileHandler,
}
diff --git a/packages/frontend-core/src/utils/utils.js b/packages/frontend-core/src/utils/utils.js
index 65690cd535..1bee3d6c04 100644
--- a/packages/frontend-core/src/utils/utils.js
+++ b/packages/frontend-core/src/utils/utils.js
@@ -161,6 +161,9 @@ export const buildFormBlockButtonConfig = props => {
{
"##eventHandlerType": "Close Side Panel",
},
+ {
+ "##eventHandlerType": "Close Modal",
+ },
// Clear a create form once submitted
...(actionType !== "Create"
? []