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() 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: { links: {
save: async (url, title) => { save: async (url, title) => {

View File

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

View File

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

View File

@ -1,12 +1,65 @@
<script> <script>
import { getContext, setContext } from "svelte" import { getContext, setContext } from "svelte"
import { builderStore } from "../stores/builder.js"
import { Button } from "@budibase/bbui"
const component = getContext("component") const component = getContext("component")
// We need to set a block context to know we're inside a block, but also let structureLookupMap = {}
// to be able to reference the actual component ID of the block from const registerBlockComponent = (id, order, parentId, instance) => {
// any depth // Ensure child array exists
setContext("block", { id: $component.id }) 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> </script>
<slot /> <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 props
export let styles export let styles
export let context export let context
export let order = 0
// ID is only exposed as a prop so that it can be bound to from parent // ID is only exposed as a prop so that it can be bound to from parent
// block components // block components
export let id export let id
const component = getContext("component")
const block = getContext("block") const block = getContext("block")
const rand = generate() const rand = generate()
@ -21,6 +23,7 @@
$: instance = { $: instance = {
_component: `@budibase/standard-components/${type}`, _component: `@budibase/standard-components/${type}`,
_id: id, _id: id,
_instanceName: type,
_styles: { _styles: {
normal: { normal: {
...styles, ...styles,
@ -28,6 +31,7 @@
}, },
...props, ...props,
} }
$: block.registerComponent(id, order, $component?.id, instance)
</script> </script>
<Component {instance} isBlock> <Component {instance} isBlock>

View File

@ -112,28 +112,52 @@
props={{ dataSource, disableValidation: true }} props={{ dataSource, disableValidation: true }}
> >
{#if title || enrichedSearchColumns?.length || showTitleButton} {#if title || enrichedSearchColumns?.length || showTitleButton}
<div class="header" class:mobile={$context.device.mobile}> <BlockComponent
<div class="title"> type="container"
<Heading>{title || ""}</Heading> props={{
</div> direction: "row",
<div class="controls"> 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} {#if enrichedSearchColumns?.length}
<div {#each enrichedSearchColumns as column, idx}
class="search" <BlockComponent
style="--cols:{enrichedSearchColumns?.length}" type={column.componentType}
> props={{
{#each enrichedSearchColumns as column} field: column.name,
<BlockComponent placeholder: column.name,
type={column.componentType} text: column.name,
props={{ autoWidth: true,
field: column.name, }}
placeholder: column.name, styles={{
text: column.name, width: "192px",
autoWidth: true, }}
}} order={idx}
/> />
{/each} {/each}
</div>
{/if} {/if}
{#if showTitleButton} {#if showTitleButton}
<BlockComponent <BlockComponent
@ -143,10 +167,11 @@
text: titleButtonText, text: titleButtonText,
type: "cta", type: "cta",
}} }}
order={3}
/> />
{/if} {/if}
</div> </BlockComponent>
</div> </BlockComponent>
{/if} {/if}
<BlockComponent <BlockComponent
type="dataprovider" type="dataprovider"
@ -159,6 +184,7 @@
paginate, paginate,
limit: rowCount, limit: rowCount,
}} }}
order={1}
> >
<BlockComponent <BlockComponent
type="table" type="table"
@ -185,33 +211,6 @@
{/if} {/if}
<style> <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) { .controls :global(.spectrum-InputGroup .spectrum-InputGroup-input) {
width: 100%; width: 100%;
} }

View File

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