Merge pull request #7427 from Budibase/cheeks-lab-day-eject-blocks

Block ejection
This commit is contained in:
Andrew Kingston 2022-10-07 12:44:39 +01:00 committed by GitHub
commit 1412502055
19 changed files with 553 additions and 360 deletions

View File

@ -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) => {

View File

@ -0,0 +1,13 @@
<script>
import { ActionButton } from "@budibase/bbui"
const eject = () => {
document.dispatchEvent(
new KeyboardEvent("keydown", { key: "e", ctrlKey: true })
)
}
</script>
<div>
<ActionButton secondary on:click={eject}>Eject block</ActionButton>
</div>

View File

@ -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}
/>
</div>
{#if info}
<div class="text">{@html info}</div>
{/if}
</div>
<style>
@ -123,4 +127,9 @@
.control {
position: relative;
}
.text {
margin-top: var(--spectrum-global-dimension-size-65);
font-size: var(--spectrum-global-dimension-font-size-75);
color: var(--grey-6);
}
</style>

View File

@ -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 {

View File

@ -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
</MenuItem>
{#if isBlock}
<MenuItem
icon="Export"
keyBind="Ctrl+E"
on:click={() => keyboardEvent("e", true)}
>
Eject block
</MenuItem>
{/if}
<MenuItem
icon="ChevronUp"
keyBind="Ctrl+!ArrowUp"

View File

@ -7,7 +7,9 @@
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
let confirmDeleteDialog
let confirmEjectDialog
let componentToDelete
let componentToEject
const keyHandlers = {
["^ArrowUp"]: async component => {
@ -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)}
/>
<ConfirmDialog
bind:this={confirmEjectDialog}
title="Eject block"
body={`Ejecting a block breaks it down into multiple components and cannot be undone. Are you sure you want to eject "${componentToEject?._instanceName}"?`}
onOk={() => store.actions.components.requestEjectBlock(componentToEject?._id)}
okText="Eject block"
/>

View File

@ -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")}
<ResetFieldsButton {componentInstance} />
{/if}
{#if section?.info}
<div class="text">
{@html section.info}
</div>
{#if idx === 0 && componentDefinition?.block}
<EjectBlockButton />
{/if}
</DetailSummary>
{/each}
<style>
.text {
font-size: var(--spectrum-global-dimension-font-size-75);
color: var(--grey-6);
}
</style>

View File

@ -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": [

View File

@ -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({

View File

@ -1,12 +1,92 @@
<script>
import { getContext, setContext } from "svelte"
import { getContext, onDestroy, onMount, setContext } from "svelte"
import { builderStore } from "stores/builder.js"
import { blockStore } from "stores/blocks.js"
const component = getContext("component")
const { styleable } = getContext("sdk")
// We need to set a block context to know we're inside a block, but also
// to be able to reference the actual component ID of the block from
// any depth
setContext("block", { id: $component.id })
let structureLookupMap = {}
const registerBlockComponent = (id, order, parentId, instance) => {
// Ensure child array exists
if (!structureLookupMap[parentId]) {
structureLookupMap[parentId] = {}
}
// Add this instance in this order, overwriting any existing instance in
// this order in case of repeaters
structureLookupMap[parentId][order] = instance
}
const eject = () => {
// Start the new structure with the root component
let definition = structureLookupMap[$component.id][0]
// Copy styles from block to root component
definition._styles = {
...definition._styles,
normal: {
...definition._styles?.normal,
...$component.styles?.normal,
},
custom:
definition._styles?.custom || "" + $component.styles?.custom || "",
}
// Create component tree
attachChildren(definition, structureLookupMap)
builderStore.actions.ejectBlock($component.id, definition)
}
const attachChildren = (rootComponent, map) => {
// Transform map into children array
let id = rootComponent._id
const children = Object.entries(map[id] || {}).map(([order, instance]) => ({
order,
instance,
}))
if (!children.length) {
return
}
// Sort children by order
children.sort((a, b) => (a.order < b.order ? -1 : 1))
// Attach all children of this component
rootComponent._children = children.map(x => x.instance)
// Recurse for each child
rootComponent._children.forEach(child => {
attachChildren(child, map)
})
}
setContext("block", {
// We need to set a block context to know we're inside a block, but also
// to be able to reference the actual component ID of the block from
// any depth
id: $component.id,
// We register block components with their raw props so that we can eject
// blocks later on
registerComponent: registerBlockComponent,
})
onMount(() => {
// We register and unregister blocks to the block store when inside the
// builder preview to allow for block ejection
if ($builderStore.inBuilder) {
blockStore.actions.registerBlock($component.id, { eject })
}
})
onDestroy(() => {
if ($builderStore.inBuilder) {
blockStore.actions.unregisterBlock($component.id)
}
})
</script>
<slot />
<div use:styleable={$component.styles}>
<slot />
</div>

View File

@ -1,17 +1,21 @@
<script>
import { getContext } from "svelte"
import { generate } from "shortid"
import { builderStore } from "../stores/builder.js"
import Component from "components/Component.svelte"
export let type
export let props
export let styles
export let context
export let order = 0
export let containsSlot = false
// ID is only exposed as a prop so that it can be bound to from parent
// block components
export let id
const component = getContext("component")
const block = getContext("block")
const rand = generate()
@ -21,13 +25,22 @@
$: instance = {
_component: `@budibase/standard-components/${type}`,
_id: id,
_instanceName: type[0].toUpperCase() + type.slice(1),
_styles: {
normal: {
...styles,
},
...styles,
normal: styles?.normal || {},
},
_containsSlot: containsSlot,
...props,
}
// Register this block component if we're inside the builder so it can be
// ejected later
$: {
if ($builderStore.inBuilder) {
block.registerComponent(id, order ?? 0, $component?.id, instance)
}
}
</script>
<Component {instance} isBlock>

View File

@ -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}
<Block>
<div class="card-list" use:styleable={$component.styles}>
<BlockComponent
type="form"
bind:id={formId}
props={{ dataSource, disableValidation: true }}
>
{#if title || enrichedSearchColumns?.length || showTitleButton}
<div class="header" class:mobile={$context.device.mobile}>
<div class="title">
<Heading>{title || ""}</Heading>
</div>
<div class="controls">
{#if enrichedSearchColumns?.length}
<div
class="search"
style="--cols:{enrichedSearchColumns?.length}"
>
{#each enrichedSearchColumns as column}
<BlockComponent
type={column.componentType}
props={{
field: column.name,
placeholder: column.name,
text: column.name,
autoWidth: true,
}}
/>
{/each}
</div>
{/if}
{#if showTitleButton}
<BlockComponent
type="button"
props={{
onClick: titleButtonAction,
text: titleButtonText,
type: "cta",
}}
/>
{/if}
</div>
</div>
{/if}
<BlockComponent
type="form"
bind:id={formId}
props={{ dataSource, disableValidation: true }}
>
{#if title || enrichedSearchColumns?.length || showTitleButton}
<BlockComponent
type="dataprovider"
bind:id={dataProviderId}
type="container"
props={{
dataSource,
filter: enrichedFilter,
sortColumn,
sortOrder,
paginate,
limit,
direction: "row",
hAlign: "stretch",
vAlign: "middle",
gap: "M",
wrap: true,
}}
styles={{
normal: {
"margin-bottom": "20px",
},
}}
order={0}
>
<BlockComponent
type="repeater"
bind:id={repeaterId}
context="repeater"
type="heading"
props={{
text: title,
}}
order={0}
/>
<BlockComponent
type="container"
props={{
dataProvider: `{{ literal ${safe(dataProviderId)} }}`,
direction: "row",
hAlign: "stretch",
vAlign: "top",
hAlign: "left",
vAlign: "middle",
gap: "M",
noRowsMessage: "No rows found",
}}
styles={{
display: "grid",
"grid-template-columns": `repeat(auto-fill, minmax(min(${cardWidth}px, 100%), 1fr))`,
wrap: true,
}}
order={1}
>
<BlockComponent
type="spectrumcard"
props={{
title: cardTitle,
subtitle: cardSubtitle,
description: cardDescription,
imageURL: cardImageURL,
horizontal: cardHorizontal,
showButton: showCardButton,
buttonText: cardButtonText,
buttonOnClick: cardButtonOnClick,
linkURL: fullCardURL,
linkPeek: cardPeek,
}}
styles={{
width: "auto",
}}
/>
{#if enrichedSearchColumns?.length}
{#each enrichedSearchColumns as column, idx}
<BlockComponent
type={column.componentType}
props={{
field: column.name,
placeholder: column.name,
text: column.name,
autoWidth: true,
}}
order={idx}
styles={{
normal: {
width: "192px",
},
}}
/>
{/each}
{/if}
{#if showTitleButton}
<BlockComponent
type="button"
props={{
onClick: titleButtonAction,
text: titleButtonText,
type: "cta",
}}
order={enrichedSearchColumns?.length ?? 0}
/>
{/if}
</BlockComponent>
</BlockComponent>
{/if}
<BlockComponent
type="dataprovider"
bind:id={dataProviderId}
props={{
dataSource,
filter: enrichedFilter,
sortColumn,
sortOrder,
paginate,
limit,
}}
order={1}
>
<BlockComponent
type="repeater"
bind:id={repeaterId}
context="repeater"
props={{
dataProvider: `{{ literal ${safe(dataProviderId)} }}`,
direction: "row",
hAlign: "stretch",
vAlign: "top",
gap: "M",
noRowsMessage: "No rows found",
}}
styles={{
custom: `display: grid;\ngrid-template-columns: repeat(auto-fill, minmax(min(${cardWidth}px, 100%), 1fr));`,
}}
order={0}
>
<BlockComponent
type="spectrumcard"
props={{
title: cardTitle,
subtitle: cardSubtitle,
description: cardDescription,
imageURL: cardImageURL,
horizontal: cardHorizontal,
showButton: showCardButton,
buttonText: cardButtonText,
buttonOnClick: cardButtonOnClick,
linkURL: fullCardURL,
linkPeek: cardPeek,
}}
styles={{
normal: {
width: "auto",
},
}}
order={0}
/>
</BlockComponent>
</BlockComponent>
</div>
</BlockComponent>
</Block>
{/if}
<style>
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 20px;
margin-bottom: 20px;
}
.title {
overflow: hidden;
}
.title :global(.spectrum-Heading) {
flex: 1 1 auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.controls {
flex: 0 1 auto;
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
gap: 20px;
}
.controls :global(.spectrum-InputGroup .spectrum-InputGroup-input) {
width: 100%;
}
.search {
flex: 0 1 auto;
gap: 10px;
max-width: 100%;
display: grid;
grid-template-columns: repeat(var(--cols), minmax(120px, 200px));
}
.search :global(.spectrum-InputGroup) {
min-width: 0;
}
/* Mobile styles */
.mobile {
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
}
.mobile .controls {
flex-direction: column-reverse;
justify-content: flex-start;
align-items: stretch;
}
.mobile .search {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
position: relative;
width: 100%;
}
</style>

View File

@ -17,45 +17,43 @@
export let vAlign
export let gap
let providerId
const component = getContext("component")
const { styleable } = getContext("sdk")
let providerId
</script>
<Block>
<div use:styleable={$component.styles}>
<BlockComponent
type="dataprovider"
context="provider"
bind:id={providerId}
props={{
dataSource,
filter,
sortColumn,
sortOrder,
limit,
paginate,
}}
>
{#if $component.empty}
<Placeholder />
{:else}
<BlockComponent
type="repeater"
context="repeater"
props={{
dataProvider: `{{ literal ${safe(providerId)} }}`,
noRowsMessage,
direction,
hAlign,
vAlign,
gap,
}}
>
<slot />
</BlockComponent>
{/if}
</BlockComponent>
</div>
<BlockComponent
type="dataprovider"
context="provider"
bind:id={providerId}
props={{
dataSource,
filter,
sortColumn,
sortOrder,
limit,
paginate,
}}
>
{#if $component.empty}
<Placeholder />
{:else}
<BlockComponent
type="repeater"
context="repeater"
containsSlot
props={{
dataProvider: `{{ literal ${safe(providerId)} }}`,
noRowsMessage,
direction,
hAlign,
vAlign,
gap,
}}
>
<slot />
</BlockComponent>
{/if}
</BlockComponent>
</Block>

View File

@ -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}
<Block>
<div class={size} use:styleable={$component.styles}>
<BlockComponent
type="form"
bind:id={formId}
props={{ dataSource, disableValidation: true, editAutoColumns: true }}
>
{#if title || enrichedSearchColumns?.length || showTitleButton}
<div class="header" class:mobile={$context.device.mobile}>
<div class="title">
<Heading>{title || ""}</Heading>
</div>
<div class="controls">
{#if enrichedSearchColumns?.length}
<div
class="search"
style="--cols:{enrichedSearchColumns?.length}"
>
{#each enrichedSearchColumns as column}
<BlockComponent
type={column.componentType}
props={{
field: column.name,
placeholder: column.name,
text: column.name,
autoWidth: true,
}}
/>
{/each}
</div>
{/if}
{#if showTitleButton}
<BlockComponent
type="button"
props={{
onClick: titleButtonAction,
text: titleButtonText,
type: "cta",
}}
/>
{/if}
</div>
</div>
{/if}
<BlockComponent
type="form"
bind:id={formId}
props={{
dataSource,
disableValidation: true,
editAutoColumns: true,
size,
}}
>
{#if title || enrichedSearchColumns?.length || showTitleButton}
<BlockComponent
type="dataprovider"
bind:id={dataProviderId}
type="container"
props={{
dataSource,
filter: enrichedFilter,
sortColumn,
sortOrder,
paginate,
limit: rowCount,
direction: "row",
hAlign: "stretch",
vAlign: "middle",
gap: "M",
wrap: true,
}}
styles={{
normal: {
"margin-bottom": "20px",
},
}}
order={0}
>
<BlockComponent
type="table"
context="table"
type="heading"
props={{
dataProvider: `{{ literal ${safe(dataProviderId)} }}`,
columns: tableColumns,
showAutoColumns,
rowCount,
quiet,
compact,
allowSelectRows,
size,
linkRows,
linkURL,
linkColumn,
linkPeek,
text: title,
}}
order={0}
/>
<BlockComponent
type="container"
props={{
direction: "row",
hAlign: "left",
vAlign: "center",
gap: "M",
wrap: true,
}}
order={1}
>
{#if enrichedSearchColumns?.length}
{#each enrichedSearchColumns as column, idx}
<BlockComponent
type={column.componentType}
props={{
field: column.name,
placeholder: column.name,
text: column.name,
autoWidth: true,
}}
styles={{
normal: {
width: "192px",
},
}}
order={idx}
/>
{/each}
{/if}
{#if showTitleButton}
<BlockComponent
type="button"
props={{
onClick: titleButtonAction,
text: titleButtonText,
type: "cta",
}}
order={enrichedSearchColumns?.length ?? 0}
/>
{/if}
</BlockComponent>
</BlockComponent>
{/if}
<BlockComponent
type="dataprovider"
bind:id={dataProviderId}
props={{
dataSource,
filter: enrichedFilter,
sortColumn,
sortOrder,
paginate,
limit: rowCount,
}}
order={1}
>
<BlockComponent
type="table"
context="table"
props={{
dataProvider: `{{ literal ${safe(dataProviderId)} }}`,
columns: tableColumns,
showAutoColumns,
rowCount,
quiet,
compact,
allowSelectRows,
size,
linkRows,
linkURL,
linkColumn,
linkPeek,
}}
/>
</BlockComponent>
</div>
</BlockComponent>
</Block>
{/if}
<style>
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 20px;
margin-bottom: 20px;
}
.title {
overflow: hidden;
}
.title :global(.spectrum-Heading) {
flex: 1 1 auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.controls {
flex: 0 1 auto;
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
gap: 20px;
}
.controls :global(.spectrum-InputGroup .spectrum-InputGroup-input) {
width: 100%;
}
.search {
flex: 0 1 auto;
gap: 10px;
max-width: 100%;
display: grid;
grid-template-columns: repeat(var(--cols), minmax(120px, 200px));
}
.search :global(.spectrum-InputGroup) {
min-width: 0;
}
/* Mobile styles */
.mobile {
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
}
.mobile .controls {
flex-direction: column-reverse;
justify-content: flex-start;
align-items: stretch;
}
.mobile .search {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
position: relative;
width: 100%;
}
</style>

View File

@ -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 => {

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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,