diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index aefdba9fb2..b1365c3ece 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -330,6 +330,16 @@ export const getFrontendStore = () => { return state }) }, + sendEvent: (name, payload) => { + const { previewEventHandler } = get(store) + previewEventHandler?.(name, payload) + }, + registerEventHandler: handler => { + store.update(state => { + state.previewEventHandler = handler + return state + }) + }, }, layouts: { select: layoutId => { @@ -891,6 +901,50 @@ export const getFrontendStore = () => { component[name] = value }) }, + requestEjectBlock: componentId => { + store.actions.preview.sendEvent("eject-block", componentId) + }, + handleEjectBlock: async (componentId, ejectedDefinition) => { + let nextSelectedComponentId + + await store.actions.screens.patch(screen => { + const block = findComponent(screen.props, componentId) + const parent = findComponentParent(screen.props, componentId) + + // Sanity check + if (!block || !parent?._children?.length) { + return false + } + + // Attach block children back into ejected definition, using the + // _containsSlot flag to know where to insert them + const slotContainer = findAllMatchingComponents( + ejectedDefinition, + x => x._containsSlot + )[0] + if (slotContainer) { + delete slotContainer._containsSlot + slotContainer._children = [ + ...(slotContainer._children || []), + ...(block._children || []), + ] + } + + // Replace block with ejected definition + makeComponentUnique(ejectedDefinition) + const index = parent._children.findIndex(x => x._id === componentId) + parent._children[index] = ejectedDefinition + nextSelectedComponentId = ejectedDefinition._id + }) + + // Select new root component + if (nextSelectedComponentId) { + store.update(state => { + state.selectedComponentId = nextSelectedComponentId + return state + }) + } + }, }, links: { save: async (url, title) => { diff --git a/packages/builder/src/components/design/settings/controls/EjectBlockButton.svelte b/packages/builder/src/components/design/settings/controls/EjectBlockButton.svelte new file mode 100644 index 0000000000..e19d4b584b --- /dev/null +++ b/packages/builder/src/components/design/settings/controls/EjectBlockButton.svelte @@ -0,0 +1,13 @@ + + +
+ Eject block +
diff --git a/packages/builder/src/components/design/settings/controls/PropertyControl.svelte b/packages/builder/src/components/design/settings/controls/PropertyControl.svelte index 3927e0b3a5..8be770e3a0 100644 --- a/packages/builder/src/components/design/settings/controls/PropertyControl.svelte +++ b/packages/builder/src/components/design/settings/controls/PropertyControl.svelte @@ -20,6 +20,7 @@ export let componentBindings = [] export let nested = false export let highlighted = false + export let info = null $: nullishValue = value == null || value === "" $: allBindings = getAllBindings(bindings, componentBindings, nested) @@ -99,6 +100,9 @@ {...props} /> + {#if info} +
{@html info}
+ {/if} 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 9f21a6a29f..309b676a70 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 @@ -98,11 +98,21 @@ `./components/${$selectedComponent?._id}/new` ) + // Register handler to send custom to the preview + $: store.actions.preview.registerEventHandler((name, payload) => { + iframe?.contentWindow.postMessage( + JSON.stringify({ + name, + payload, + isBudibaseEvent: true, + runtimeEvent: true, + }) + ) + }) + // Update the iframe with the builder info to render the correct preview const refreshContent = message => { - if (iframe) { - iframe.contentWindow.postMessage(message) - } + iframe?.contentWindow.postMessage(message) } const receiveMessage = message => { @@ -198,6 +208,9 @@ block: "center", }) } + } else if (type === "eject-block") { + const { id, definition } = data + await store.actions.components.handleEjectBlock(id, definition) } else if (type === "reload-plugin") { await store.actions.components.refreshDefinitions() } else { diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentDropdownMenu.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentDropdownMenu.svelte index c19cba1aac..aeaa577455 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentDropdownMenu.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentDropdownMenu.svelte @@ -4,7 +4,9 @@ export let component + $: definition = store.actions.components.getDefinition(component?._component) $: noPaste = !$store.componentToPaste + $: isBlock = definition?.block === true const keyboardEvent = (key, ctrlKey = false) => { document.dispatchEvent( @@ -30,6 +32,15 @@ > Delete + {#if isBlock} + keyboardEvent("e", true)} + > + Eject block + + {/if} { @@ -29,6 +31,10 @@ store.actions.components.copy(component) await store.actions.components.paste(component, "below") }, + ["^e"]: component => { + componentToEject = component + confirmEjectDialog.show() + }, ["^Enter"]: () => { $goto("./new") }, @@ -124,3 +130,10 @@ okText="Delete Component" onOk={() => store.actions.components.delete(componentToDelete)} /> + store.actions.components.requestEjectBlock(componentToEject?._id)} + okText="Eject block" +/> diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentSettingsSection.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentSettingsSection.svelte index b4c8e7abad..c561efda0c 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentSettingsSection.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentSettingsSection.svelte @@ -4,6 +4,7 @@ import { store } from "builderStore" import PropertyControl from "components/design/settings/controls/PropertyControl.svelte" import ResetFieldsButton from "components/design/settings/controls/ResetFieldsButton.svelte" + import EjectBlockButton from "components/design/settings/controls/EjectBlockButton.svelte" import { getComponentForSetting } from "components/design/settings/componentSettings" export let componentDefinition @@ -21,7 +22,6 @@ return [ { name: "General", - info: componentDefinition?.info, settings: generalSettings, }, ...(customSections || []), @@ -103,6 +103,7 @@ nested={setting.nested} onChange={val => updateSetting(setting.key, val)} highlighted={$store.highlightedSettingKey === setting.key} + info={setting.info} props={{ // Generic settings placeholder: setting.placeholder || null, @@ -124,17 +125,8 @@ {#if idx === 0 && componentDefinition?.component?.endsWith("/fieldgroup")} {/if} - {#if section?.info} -
- {@html section.info} -
+ {#if idx === 0 && componentDefinition?.block} + {/if} {/each} - - diff --git a/packages/client/manifest.json b/packages/client/manifest.json index b6d4941e4c..01d2b6a685 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -3442,7 +3442,6 @@ }, "s3upload": { "name": "S3 File Upload", - "info": "This component can't be used with S3 datasources that use custom endpoints.", "icon": "UploadToCloud", "styles": [ "size" @@ -3463,7 +3462,8 @@ { "type": "dataSource/s3", "label": "S3 Datasource", - "key": "datasourceId" + "key": "datasourceId", + "info": "This component can't be used with S3 datasources that use custom endpoints" }, { "type": "text", @@ -3501,7 +3501,6 @@ }, "dataprovider": { "name": "Data Provider", - "info": "Pagination is only available for data stored in tables.", "icon": "Data", "illegalChildren": [ "section" @@ -3547,7 +3546,8 @@ "type": "boolean", "label": "Paginate", "key": "paginate", - "defaultValue": true + "defaultValue": true, + "info": "Pagination is only available for data stored in tables" } ], "context": { @@ -3589,7 +3589,6 @@ ], "hasChildren": true, "showEmptyState": false, - "info": "Row selection is only compatible with internal or SQL tables", "settings": [ { "type": "dataProvider", @@ -3646,7 +3645,8 @@ "type": "boolean", "label": "Allow row selection", "key": "allowSelectRows", - "defaultValue": false + "defaultValue": false, + "info": "Row selection is only compatible with internal or SQL tables" }, { "type": "boolean", @@ -3687,13 +3687,13 @@ "size" ], "hasChildren": false, - "info": "Your data provider will be automatically filtered to the given date range.", "settings": [ { "type": "dataProvider", "label": "Provider", "key": "dataProvider", - "required": true + "required": true, + "info": "Your data provider will be automatically filtered to the given date range." }, { "type": "field", @@ -3828,7 +3828,6 @@ "styles": [ "size" ], - "info": "Only the first 3 search columns will be used.", "settings": [ { "type": "text", @@ -3845,7 +3844,8 @@ "type": "searchfield", "label": "Search Columns", "key": "searchColumns", - "placeholder": "Choose search columns" + "placeholder": "Choose search columns", + "info": "Only the first 3 search columns will be used" }, { "type": "filter", @@ -3892,7 +3892,6 @@ { "section": true, "name": "Table", - "info": "Row selection is only compatible with internal or SQL tables", "settings": [ { "type": "number", @@ -3926,7 +3925,8 @@ { "type": "boolean", "label": "Allow row selection", - "key": "allowSelectRows" + "key": "allowSelectRows", + "info": "Row selection is only compatible with internal or SQL tables" }, { "type": "boolean", @@ -3993,7 +3993,6 @@ "styles": [ "size" ], - "info": "Only the first 3 search columns will be used.", "settings": [ { "type": "text", @@ -4010,7 +4009,8 @@ "type": "searchfield", "label": "Search Columns", "key": "searchColumns", - "placeholder": "Choose search columns" + "placeholder": "Choose search columns", + "info": "Only the first 3 search columns will be used" }, { "type": "filter", @@ -4157,6 +4157,7 @@ } }, "repeaterblock": { + "block": true, "name": "Repeater block", "icon": "ViewList", "illegalChildren": [ diff --git a/packages/client/src/api/api.js b/packages/client/src/api/api.js index 563126fdd0..8d29d37bd6 100644 --- a/packages/client/src/api/api.js +++ b/packages/client/src/api/api.js @@ -1,5 +1,7 @@ import { createAPIClient } from "@budibase/frontend-core" -import { notificationStore, authStore, devToolsStore } from "../stores" +import { notificationStore } from "../stores/notification.js" +import { authStore } from "../stores/auth.js" +import { devToolsStore } from "../stores/devTools.js" import { get } from "svelte/store" export const API = createAPIClient({ diff --git a/packages/client/src/components/Block.svelte b/packages/client/src/components/Block.svelte index b5e610c1bb..05d92f208c 100644 --- a/packages/client/src/components/Block.svelte +++ b/packages/client/src/components/Block.svelte @@ -1,12 +1,92 @@ - +
+ +
diff --git a/packages/client/src/components/BlockComponent.svelte b/packages/client/src/components/BlockComponent.svelte index c23f18f55c..2f756ce296 100644 --- a/packages/client/src/components/BlockComponent.svelte +++ b/packages/client/src/components/BlockComponent.svelte @@ -1,17 +1,21 @@ diff --git a/packages/client/src/components/app/blocks/CardsBlock.svelte b/packages/client/src/components/app/blocks/CardsBlock.svelte index a13364833a..9c110d7097 100644 --- a/packages/client/src/components/app/blocks/CardsBlock.svelte +++ b/packages/client/src/components/app/blocks/CardsBlock.svelte @@ -2,7 +2,6 @@ import { getContext } from "svelte" import Block from "components/Block.svelte" import BlockComponent from "components/BlockComponent.svelte" - import { Heading } from "@budibase/bbui" import { makePropSafe as safe } from "@budibase/string-templates" import { enrichSearchColumns, enrichFilter } from "utils/blocks.js" @@ -31,9 +30,7 @@ export let cardButtonOnClick export let linkColumn - const { fetchDatasourceSchema, styleable } = getContext("sdk") - const context = getContext("context") - const component = getContext("component") + const { fetchDatasourceSchema } = getContext("sdk") let formId let dataProviderId @@ -84,163 +81,132 @@ {#if schemaLoaded} -
- - {#if title || enrichedSearchColumns?.length || showTitleButton} -
-
- {title || ""} -
-
- {#if enrichedSearchColumns?.length} - - {/if} - {#if showTitleButton} - - {/if} -
-
- {/if} + + {#if title || enrichedSearchColumns?.length || showTitleButton} + - + {#if enrichedSearchColumns?.length} + {#each enrichedSearchColumns as column, idx} + + {/each} + {/if} + {#if showTitleButton} + + {/if} + {/if} + + + + -
+
{/if} - - diff --git a/packages/client/src/components/app/blocks/RepeaterBlock.svelte b/packages/client/src/components/app/blocks/RepeaterBlock.svelte index 247a8b0d51..30fbdddcdc 100644 --- a/packages/client/src/components/app/blocks/RepeaterBlock.svelte +++ b/packages/client/src/components/app/blocks/RepeaterBlock.svelte @@ -17,45 +17,43 @@ export let vAlign export let gap - let providerId - const component = getContext("component") - const { styleable } = getContext("sdk") + + let providerId -
- - {#if $component.empty} - - {:else} - - - - {/if} - -
+ + {#if $component.empty} + + {:else} + + + + {/if} +
diff --git a/packages/client/src/components/app/blocks/TableBlock.svelte b/packages/client/src/components/app/blocks/TableBlock.svelte index e67124fc4f..f75a71a3ee 100644 --- a/packages/client/src/components/app/blocks/TableBlock.svelte +++ b/packages/client/src/components/app/blocks/TableBlock.svelte @@ -2,7 +2,6 @@ import { getContext } from "svelte" import Block from "components/Block.svelte" import BlockComponent from "components/BlockComponent.svelte" - import { Heading } from "@budibase/bbui" import { makePropSafe as safe } from "@budibase/string-templates" import { enrichSearchColumns, enrichFilter } from "utils/blocks.js" @@ -29,9 +28,7 @@ export let titleButtonURL export let titleButtonPeek - const { fetchDatasourceSchema, styleable } = getContext("sdk") - const context = getContext("context") - const component = getContext("component") + const { fetchDatasourceSchema } = getContext("sdk") let formId let dataProviderId @@ -64,145 +61,116 @@ {#if schemaLoaded} -
- - {#if title || enrichedSearchColumns?.length || showTitleButton} -
-
- {title || ""} -
-
- {#if enrichedSearchColumns?.length} - - {/if} - {#if showTitleButton} - - {/if} -
-
- {/if} + + {#if title || enrichedSearchColumns?.length || showTitleButton} + + {#if enrichedSearchColumns?.length} + {#each enrichedSearchColumns as column, idx} + + {/each} + {/if} + {#if showTitleButton} + + {/if} + + {/if} + + -
+
{/if} - - diff --git a/packages/client/src/index.js b/packages/client/src/index.js index 0e8ab8c258..b671d5554a 100644 --- a/packages/client/src/index.js +++ b/packages/client/src/index.js @@ -1,9 +1,10 @@ import ClientApp from "./components/ClientApp.svelte" import { - componentStore, builderStore, appStore, devToolsStore, + blockStore, + componentStore, environmentStore, } from "./stores" import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-rollup.js" @@ -50,6 +51,17 @@ const loadBudibase = async () => { const enableDevTools = !get(builderStore).inBuilder && get(appStore).isDevApp devToolsStore.actions.setEnabled(enableDevTools) + // Register handler for runtime events from the builder + window.handleBuilderRuntimeEvent = (name, payload) => { + if (!window["##BUDIBASE_IN_BUILDER##"]) { + return + } + if (name === "eject-block") { + const block = blockStore.actions.getBlock(payload) + block?.eject() + } + } + // Register any custom components if (window["##BUDIBASE_CUSTOM_COMPONENTS##"]) { window["##BUDIBASE_CUSTOM_COMPONENTS##"].forEach(component => { diff --git a/packages/client/src/stores/blocks.js b/packages/client/src/stores/blocks.js new file mode 100644 index 0000000000..98381ec79b --- /dev/null +++ b/packages/client/src/stores/blocks.js @@ -0,0 +1,34 @@ +import { get, writable } from "svelte/store" + +const createBlockStore = () => { + const store = writable({}) + + const registerBlock = (id, instance) => { + store.update(state => ({ + ...state, + [id]: instance, + })) + } + + const unregisterBlock = id => { + store.update(state => { + delete state[id] + return state + }) + } + + const getBlock = id => { + return get(store)[id] + } + + return { + subscribe: store.subscribe, + actions: { + registerBlock, + unregisterBlock, + getBlock, + }, + } +} + +export const blockStore = createBlockStore() diff --git a/packages/client/src/stores/builder.js b/packages/client/src/stores/builder.js index fea070c27c..5aaea2bdb0 100644 --- a/packages/client/src/stores/builder.js +++ b/packages/client/src/stores/builder.js @@ -85,6 +85,9 @@ const createBuilderStore = () => { highlightSetting: setting => { dispatchEvent("highlight-setting", { setting }) }, + ejectBlock: (id, definition) => { + dispatchEvent("eject-block", { id, definition }) + }, updateUsedPlugin: (name, hash) => { // Check if we used this plugin const used = get(store)?.usedPlugins?.find(x => x.name === name) diff --git a/packages/client/src/stores/index.js b/packages/client/src/stores/index.js index 378d3febd2..5b77762223 100644 --- a/packages/client/src/stores/index.js +++ b/packages/client/src/stores/index.js @@ -17,6 +17,7 @@ export { devToolsStore } from "./devTools" export { componentStore } from "./components" export { uploadStore } from "./uploads.js" export { rowSelectionStore } from "./rowSelection.js" +export { blockStore } from "./blocks.js" export { environmentStore } from "./environment" // Context stores are layered and duplicated, so it is not a singleton diff --git a/packages/server/src/api/controllers/static/templates/preview.hbs b/packages/server/src/api/controllers/static/templates/preview.hbs index d48dde7cf3..829db5fc88 100644 --- a/packages/server/src/api/controllers/static/templates/preview.hbs +++ b/packages/server/src/api/controllers/static/templates/preview.hbs @@ -56,6 +56,16 @@ return } + // If this is a custom event, try and handle it + if (parsed.runtimeEvent) { + const { name, payload } = parsed + if (window.handleBuilderRuntimeEvent) { + window.handleBuilderRuntimeEvent(name, payload) + } + return + } + + // Otherwise this is a full reload message // Extract data from message const { selectedComponentId,