Add ability to eject blocks into raw components
This commit is contained in:
parent
d78bcc2b06
commit
02e5e66992
|
@ -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) => {
|
||||
|
|
|
@ -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}`)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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")
|
||||
|
||||
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
|
||||
setContext("block", { id: $component.id })
|
||||
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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -112,17 +112,38 @@
|
|||
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}"
|
||||
<BlockComponent
|
||||
type="container"
|
||||
props={{
|
||||
direction: "row",
|
||||
hAlign: "stretch",
|
||||
vAlign: "middle",
|
||||
gap: "M",
|
||||
}}
|
||||
styles={{
|
||||
"margin-bottom": "20px",
|
||||
}}
|
||||
order={0}
|
||||
>
|
||||
{#each enrichedSearchColumns as column}
|
||||
<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}
|
||||
{#each enrichedSearchColumns as column, idx}
|
||||
<BlockComponent
|
||||
type={column.componentType}
|
||||
props={{
|
||||
|
@ -131,9 +152,12 @@
|
|||
text: column.name,
|
||||
autoWidth: true,
|
||||
}}
|
||||
styles={{
|
||||
width: "192px",
|
||||
}}
|
||||
order={idx}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/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%;
|
||||
}
|
||||
|
|
|
@ -71,6 +71,9 @@ const createBuilderStore = () => {
|
|||
highlightSetting: setting => {
|
||||
dispatchEvent("highlight-setting", { setting })
|
||||
},
|
||||
ejectBlock: (id, definition) => {
|
||||
dispatchEvent("eject-block", { id, definition })
|
||||
},
|
||||
}
|
||||
return {
|
||||
...store,
|
||||
|
|
Loading…
Reference in New Issue