Add ability to eject blocks into raw components

This commit is contained in:
Andrew Kingston 2022-06-30 19:31:25 +01:00
parent d78bcc2b06
commit 02e5e66992
7 changed files with 130 additions and 54 deletions

View File

@ -575,6 +575,14 @@ export const getFrontendStore = () => {
})
await store.actions.preview.saveSelected()
},
ejectBlock: async (id, definition) => {
const asset = get(currentAsset)
let parent = findComponentParent(asset.props, id)
const childIndex = parent._children.findIndex(x => x._id === id)
parent._children[childIndex] = definition
await store.actions.preview.saveSelected()
await store.actions.components.select(definition)
},
},
links: {
save: async (url, title) => {

View File

@ -202,6 +202,9 @@
block: "center",
})
}
} else if (type === "eject-block") {
const { id, definition } = data
await store.actions.components.ejectBlock(id, definition)
} else {
console.warn(`Client sent unknown event type: ${type}`)
}

View File

@ -12,6 +12,7 @@
$: definition = store.actions.components.getDefinition(component?._component)
$: noChildrenAllowed = !component || !definition?.hasChildren
$: noPaste = !$store.componentToPaste
$: isBlock = definition?.block === true
// "editable" has been repurposed for inline text editing.
// It remains here for legacy compatibility.
@ -83,6 +84,8 @@
notifications.error("Error saving component")
}
}
const ejectBlock = () => {}
</script>
{#if showMenu}
@ -93,6 +96,9 @@
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>
Delete
</MenuItem>
{#if isBlock}
<MenuItem icon="Delete" on:click={ejectBlock}>Eject block</MenuItem>
{/if}
<MenuItem noClose icon="ChevronUp" on:click={moveUpComponent}>
Move up
</MenuItem>

View File

@ -1,12 +1,65 @@
<script>
import { getContext, setContext } from "svelte"
import { builderStore } from "../stores/builder.js"
import { Button } from "@budibase/bbui"
const component = getContext("component")
// 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] = []
}
// Remove existing instance of this component in case props changed
structureLookupMap[parentId] = structureLookupMap[parentId].filter(
x => x.instance._id !== id
)
// Add new instance of this component
structureLookupMap[parentId].push({ order, instance })
}
const eject = () => {
// Start the new structure with the first top level component
let definition = structureLookupMap[$component.id][0].instance
attachChildren(definition, structureLookupMap)
builderStore.actions.ejectBlock($component.id, definition)
}
const attachChildren = (rootComponent, map) => {
let id = rootComponent._id
if (!map[id]?.length) {
return
}
// Sort children by order
map[id].sort((a, b) => (a.order < b.order ? -1 : 1))
// Attach all children of this component
rootComponent._children = map[id].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,
})
</script>
<slot />
{#if $component.selected}
<div>
<Button cta on:click={eject}>Eject block</Button>
</div>
{/if}

View File

@ -7,11 +7,13 @@
export let props
export let styles
export let context
export let order = 0
// 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,6 +23,7 @@
$: instance = {
_component: `@budibase/standard-components/${type}`,
_id: id,
_instanceName: type,
_styles: {
normal: {
...styles,
@ -28,6 +31,7 @@
},
...props,
}
$: block.registerComponent(id, order, $component?.id, instance)
</script>
<Component {instance} isBlock>

View File

@ -112,28 +112,52 @@
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">
<BlockComponent
type="container"
props={{
direction: "row",
hAlign: "stretch",
vAlign: "middle",
gap: "M",
}}
styles={{
"margin-bottom": "20px",
}}
order={0}
>
<BlockComponent
type="heading"
props={{
text: title,
}}
order={0}
/>
<BlockComponent
type="container"
props={{
direction: "row",
hAlign: "right",
vAlign: "center",
gap: "M",
}}
order={1}
>
{#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>
{#each enrichedSearchColumns as column, idx}
<BlockComponent
type={column.componentType}
props={{
field: column.name,
placeholder: column.name,
text: column.name,
autoWidth: true,
}}
styles={{
width: "192px",
}}
order={idx}
/>
{/each}
{/if}
{#if showTitleButton}
<BlockComponent
@ -143,10 +167,11 @@
text: titleButtonText,
type: "cta",
}}
order={3}
/>
{/if}
</div>
</div>
</BlockComponent>
</BlockComponent>
{/if}
<BlockComponent
type="dataprovider"
@ -159,6 +184,7 @@
paginate,
limit: rowCount,
}}
order={1}
>
<BlockComponent
type="table"
@ -185,33 +211,6 @@
{/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%;
}

View File

@ -71,6 +71,9 @@ const createBuilderStore = () => {
highlightSetting: setting => {
dispatchEvent("highlight-setting", { setting })
},
ejectBlock: (id, definition) => {
dispatchEvent("eject-block", { id, definition })
},
}
return {
...store,