Merge pull request #7427 from Budibase/cheeks-lab-day-eject-blocks
Block ejection
This commit is contained in:
commit
9e3e6b4de0
|
@ -330,6 +330,16 @@ export const getFrontendStore = () => {
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
sendEvent: (name, payload) => {
|
||||||
|
const { previewEventHandler } = get(store)
|
||||||
|
previewEventHandler?.(name, payload)
|
||||||
|
},
|
||||||
|
registerEventHandler: handler => {
|
||||||
|
store.update(state => {
|
||||||
|
state.previewEventHandler = handler
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
layouts: {
|
layouts: {
|
||||||
select: layoutId => {
|
select: layoutId => {
|
||||||
|
@ -891,6 +901,50 @@ export const getFrontendStore = () => {
|
||||||
component[name] = value
|
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: {
|
links: {
|
||||||
save: async (url, title) => {
|
save: async (url, title) => {
|
||||||
|
|
|
@ -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>
|
|
@ -20,6 +20,7 @@
|
||||||
export let componentBindings = []
|
export let componentBindings = []
|
||||||
export let nested = false
|
export let nested = false
|
||||||
export let highlighted = false
|
export let highlighted = false
|
||||||
|
export let info = null
|
||||||
|
|
||||||
$: nullishValue = value == null || value === ""
|
$: nullishValue = value == null || value === ""
|
||||||
$: allBindings = getAllBindings(bindings, componentBindings, nested)
|
$: allBindings = getAllBindings(bindings, componentBindings, nested)
|
||||||
|
@ -99,6 +100,9 @@
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{#if info}
|
||||||
|
<div class="text">{@html info}</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -123,4 +127,9 @@
|
||||||
.control {
|
.control {
|
||||||
position: relative;
|
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>
|
</style>
|
||||||
|
|
|
@ -98,11 +98,21 @@
|
||||||
`./components/${$selectedComponent?._id}/new`
|
`./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
|
// Update the iframe with the builder info to render the correct preview
|
||||||
const refreshContent = message => {
|
const refreshContent = message => {
|
||||||
if (iframe) {
|
iframe?.contentWindow.postMessage(message)
|
||||||
iframe.contentWindow.postMessage(message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const receiveMessage = message => {
|
const receiveMessage = message => {
|
||||||
|
@ -198,6 +208,9 @@
|
||||||
block: "center",
|
block: "center",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
} else if (type === "eject-block") {
|
||||||
|
const { id, definition } = data
|
||||||
|
await store.actions.components.handleEjectBlock(id, definition)
|
||||||
} else if (type === "reload-plugin") {
|
} else if (type === "reload-plugin") {
|
||||||
await store.actions.components.refreshDefinitions()
|
await store.actions.components.refreshDefinitions()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -4,7 +4,9 @@
|
||||||
|
|
||||||
export let component
|
export let component
|
||||||
|
|
||||||
|
$: definition = store.actions.components.getDefinition(component?._component)
|
||||||
$: noPaste = !$store.componentToPaste
|
$: noPaste = !$store.componentToPaste
|
||||||
|
$: isBlock = definition?.block === true
|
||||||
|
|
||||||
const keyboardEvent = (key, ctrlKey = false) => {
|
const keyboardEvent = (key, ctrlKey = false) => {
|
||||||
document.dispatchEvent(
|
document.dispatchEvent(
|
||||||
|
@ -30,6 +32,15 @@
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
{#if isBlock}
|
||||||
|
<MenuItem
|
||||||
|
icon="Export"
|
||||||
|
keyBind="Ctrl+E"
|
||||||
|
on:click={() => keyboardEvent("e", true)}
|
||||||
|
>
|
||||||
|
Eject block
|
||||||
|
</MenuItem>
|
||||||
|
{/if}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon="ChevronUp"
|
icon="ChevronUp"
|
||||||
keyBind="Ctrl+!ArrowUp"
|
keyBind="Ctrl+!ArrowUp"
|
||||||
|
|
|
@ -7,7 +7,9 @@
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
|
||||||
let confirmDeleteDialog
|
let confirmDeleteDialog
|
||||||
|
let confirmEjectDialog
|
||||||
let componentToDelete
|
let componentToDelete
|
||||||
|
let componentToEject
|
||||||
|
|
||||||
const keyHandlers = {
|
const keyHandlers = {
|
||||||
["^ArrowUp"]: async component => {
|
["^ArrowUp"]: async component => {
|
||||||
|
@ -29,6 +31,10 @@
|
||||||
store.actions.components.copy(component)
|
store.actions.components.copy(component)
|
||||||
await store.actions.components.paste(component, "below")
|
await store.actions.components.paste(component, "below")
|
||||||
},
|
},
|
||||||
|
["^e"]: component => {
|
||||||
|
componentToEject = component
|
||||||
|
confirmEjectDialog.show()
|
||||||
|
},
|
||||||
["^Enter"]: () => {
|
["^Enter"]: () => {
|
||||||
$goto("./new")
|
$goto("./new")
|
||||||
},
|
},
|
||||||
|
@ -124,3 +130,10 @@
|
||||||
okText="Delete Component"
|
okText="Delete Component"
|
||||||
onOk={() => store.actions.components.delete(componentToDelete)}
|
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"
|
||||||
|
/>
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import PropertyControl from "components/design/settings/controls/PropertyControl.svelte"
|
import PropertyControl from "components/design/settings/controls/PropertyControl.svelte"
|
||||||
import ResetFieldsButton from "components/design/settings/controls/ResetFieldsButton.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"
|
import { getComponentForSetting } from "components/design/settings/componentSettings"
|
||||||
|
|
||||||
export let componentDefinition
|
export let componentDefinition
|
||||||
|
@ -21,7 +22,6 @@
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: "General",
|
name: "General",
|
||||||
info: componentDefinition?.info,
|
|
||||||
settings: generalSettings,
|
settings: generalSettings,
|
||||||
},
|
},
|
||||||
...(customSections || []),
|
...(customSections || []),
|
||||||
|
@ -103,6 +103,7 @@
|
||||||
nested={setting.nested}
|
nested={setting.nested}
|
||||||
onChange={val => updateSetting(setting.key, val)}
|
onChange={val => updateSetting(setting.key, val)}
|
||||||
highlighted={$store.highlightedSettingKey === setting.key}
|
highlighted={$store.highlightedSettingKey === setting.key}
|
||||||
|
info={setting.info}
|
||||||
props={{
|
props={{
|
||||||
// Generic settings
|
// Generic settings
|
||||||
placeholder: setting.placeholder || null,
|
placeholder: setting.placeholder || null,
|
||||||
|
@ -124,17 +125,8 @@
|
||||||
{#if idx === 0 && componentDefinition?.component?.endsWith("/fieldgroup")}
|
{#if idx === 0 && componentDefinition?.component?.endsWith("/fieldgroup")}
|
||||||
<ResetFieldsButton {componentInstance} />
|
<ResetFieldsButton {componentInstance} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if section?.info}
|
{#if idx === 0 && componentDefinition?.block}
|
||||||
<div class="text">
|
<EjectBlockButton />
|
||||||
{@html section.info}
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</DetailSummary>
|
</DetailSummary>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<style>
|
|
||||||
.text {
|
|
||||||
font-size: var(--spectrum-global-dimension-font-size-75);
|
|
||||||
color: var(--grey-6);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -3442,7 +3442,6 @@
|
||||||
},
|
},
|
||||||
"s3upload": {
|
"s3upload": {
|
||||||
"name": "S3 File Upload",
|
"name": "S3 File Upload",
|
||||||
"info": "This component can't be used with S3 datasources that use custom endpoints.",
|
|
||||||
"icon": "UploadToCloud",
|
"icon": "UploadToCloud",
|
||||||
"styles": [
|
"styles": [
|
||||||
"size"
|
"size"
|
||||||
|
@ -3463,7 +3462,8 @@
|
||||||
{
|
{
|
||||||
"type": "dataSource/s3",
|
"type": "dataSource/s3",
|
||||||
"label": "S3 Datasource",
|
"label": "S3 Datasource",
|
||||||
"key": "datasourceId"
|
"key": "datasourceId",
|
||||||
|
"info": "This component can't be used with S3 datasources that use custom endpoints"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
@ -3501,7 +3501,6 @@
|
||||||
},
|
},
|
||||||
"dataprovider": {
|
"dataprovider": {
|
||||||
"name": "Data Provider",
|
"name": "Data Provider",
|
||||||
"info": "Pagination is only available for data stored in tables.",
|
|
||||||
"icon": "Data",
|
"icon": "Data",
|
||||||
"illegalChildren": [
|
"illegalChildren": [
|
||||||
"section"
|
"section"
|
||||||
|
@ -3547,7 +3546,8 @@
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Paginate",
|
"label": "Paginate",
|
||||||
"key": "paginate",
|
"key": "paginate",
|
||||||
"defaultValue": true
|
"defaultValue": true,
|
||||||
|
"info": "Pagination is only available for data stored in tables"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"context": {
|
"context": {
|
||||||
|
@ -3589,7 +3589,6 @@
|
||||||
],
|
],
|
||||||
"hasChildren": true,
|
"hasChildren": true,
|
||||||
"showEmptyState": false,
|
"showEmptyState": false,
|
||||||
"info": "Row selection is only compatible with internal or SQL tables",
|
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "dataProvider",
|
"type": "dataProvider",
|
||||||
|
@ -3646,7 +3645,8 @@
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Allow row selection",
|
"label": "Allow row selection",
|
||||||
"key": "allowSelectRows",
|
"key": "allowSelectRows",
|
||||||
"defaultValue": false
|
"defaultValue": false,
|
||||||
|
"info": "Row selection is only compatible with internal or SQL tables"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
|
@ -3687,13 +3687,13 @@
|
||||||
"size"
|
"size"
|
||||||
],
|
],
|
||||||
"hasChildren": false,
|
"hasChildren": false,
|
||||||
"info": "Your data provider will be automatically filtered to the given date range.",
|
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "dataProvider",
|
"type": "dataProvider",
|
||||||
"label": "Provider",
|
"label": "Provider",
|
||||||
"key": "dataProvider",
|
"key": "dataProvider",
|
||||||
"required": true
|
"required": true,
|
||||||
|
"info": "Your data provider will be automatically filtered to the given date range."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "field",
|
"type": "field",
|
||||||
|
@ -3828,7 +3828,6 @@
|
||||||
"styles": [
|
"styles": [
|
||||||
"size"
|
"size"
|
||||||
],
|
],
|
||||||
"info": "Only the first 3 search columns will be used.",
|
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
@ -3845,7 +3844,8 @@
|
||||||
"type": "searchfield",
|
"type": "searchfield",
|
||||||
"label": "Search Columns",
|
"label": "Search Columns",
|
||||||
"key": "searchColumns",
|
"key": "searchColumns",
|
||||||
"placeholder": "Choose search columns"
|
"placeholder": "Choose search columns",
|
||||||
|
"info": "Only the first 3 search columns will be used"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "filter",
|
"type": "filter",
|
||||||
|
@ -3892,7 +3892,6 @@
|
||||||
{
|
{
|
||||||
"section": true,
|
"section": true,
|
||||||
"name": "Table",
|
"name": "Table",
|
||||||
"info": "Row selection is only compatible with internal or SQL tables",
|
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "number",
|
"type": "number",
|
||||||
|
@ -3926,7 +3925,8 @@
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Allow row selection",
|
"label": "Allow row selection",
|
||||||
"key": "allowSelectRows"
|
"key": "allowSelectRows",
|
||||||
|
"info": "Row selection is only compatible with internal or SQL tables"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
|
@ -3993,7 +3993,6 @@
|
||||||
"styles": [
|
"styles": [
|
||||||
"size"
|
"size"
|
||||||
],
|
],
|
||||||
"info": "Only the first 3 search columns will be used.",
|
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
@ -4010,7 +4009,8 @@
|
||||||
"type": "searchfield",
|
"type": "searchfield",
|
||||||
"label": "Search Columns",
|
"label": "Search Columns",
|
||||||
"key": "searchColumns",
|
"key": "searchColumns",
|
||||||
"placeholder": "Choose search columns"
|
"placeholder": "Choose search columns",
|
||||||
|
"info": "Only the first 3 search columns will be used"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "filter",
|
"type": "filter",
|
||||||
|
@ -4157,6 +4157,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeaterblock": {
|
"repeaterblock": {
|
||||||
|
"block": true,
|
||||||
"name": "Repeater block",
|
"name": "Repeater block",
|
||||||
"icon": "ViewList",
|
"icon": "ViewList",
|
||||||
"illegalChildren": [
|
"illegalChildren": [
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { createAPIClient } from "@budibase/frontend-core"
|
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"
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
export const API = createAPIClient({
|
export const API = createAPIClient({
|
||||||
|
|
|
@ -1,12 +1,92 @@
|
||||||
<script>
|
<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 component = getContext("component")
|
||||||
|
const { styleable } = getContext("sdk")
|
||||||
|
|
||||||
// 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
|
|
||||||
// any depth
|
const registerBlockComponent = (id, order, parentId, instance) => {
|
||||||
setContext("block", { id: $component.id })
|
// 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>
|
</script>
|
||||||
|
|
||||||
<slot />
|
<div use:styleable={$component.styles}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
|
@ -1,17 +1,21 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
|
import { builderStore } from "../stores/builder.js"
|
||||||
import Component from "components/Component.svelte"
|
import Component from "components/Component.svelte"
|
||||||
|
|
||||||
export let type
|
export let type
|
||||||
export let props
|
export let props
|
||||||
export let styles
|
export let styles
|
||||||
export let context
|
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
|
// 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,13 +25,22 @@
|
||||||
$: instance = {
|
$: instance = {
|
||||||
_component: `@budibase/standard-components/${type}`,
|
_component: `@budibase/standard-components/${type}`,
|
||||||
_id: id,
|
_id: id,
|
||||||
|
_instanceName: type[0].toUpperCase() + type.slice(1),
|
||||||
_styles: {
|
_styles: {
|
||||||
normal: {
|
...styles,
|
||||||
...styles,
|
normal: styles?.normal || {},
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
_containsSlot: containsSlot,
|
||||||
...props,
|
...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>
|
</script>
|
||||||
|
|
||||||
<Component {instance} isBlock>
|
<Component {instance} isBlock>
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import Block from "components/Block.svelte"
|
import Block from "components/Block.svelte"
|
||||||
import BlockComponent from "components/BlockComponent.svelte"
|
import BlockComponent from "components/BlockComponent.svelte"
|
||||||
import { Heading } from "@budibase/bbui"
|
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
import { enrichSearchColumns, enrichFilter } from "utils/blocks.js"
|
import { enrichSearchColumns, enrichFilter } from "utils/blocks.js"
|
||||||
|
|
||||||
|
@ -31,9 +30,7 @@
|
||||||
export let cardButtonOnClick
|
export let cardButtonOnClick
|
||||||
export let linkColumn
|
export let linkColumn
|
||||||
|
|
||||||
const { fetchDatasourceSchema, styleable } = getContext("sdk")
|
const { fetchDatasourceSchema } = getContext("sdk")
|
||||||
const context = getContext("context")
|
|
||||||
const component = getContext("component")
|
|
||||||
|
|
||||||
let formId
|
let formId
|
||||||
let dataProviderId
|
let dataProviderId
|
||||||
|
@ -84,163 +81,132 @@
|
||||||
|
|
||||||
{#if schemaLoaded}
|
{#if schemaLoaded}
|
||||||
<Block>
|
<Block>
|
||||||
<div class="card-list" use:styleable={$component.styles}>
|
<BlockComponent
|
||||||
<BlockComponent
|
type="form"
|
||||||
type="form"
|
bind:id={formId}
|
||||||
bind:id={formId}
|
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}>
|
|
||||||
<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
|
<BlockComponent
|
||||||
type="dataprovider"
|
type="container"
|
||||||
bind:id={dataProviderId}
|
|
||||||
props={{
|
props={{
|
||||||
dataSource,
|
direction: "row",
|
||||||
filter: enrichedFilter,
|
hAlign: "stretch",
|
||||||
sortColumn,
|
vAlign: "middle",
|
||||||
sortOrder,
|
gap: "M",
|
||||||
paginate,
|
wrap: true,
|
||||||
limit,
|
|
||||||
}}
|
}}
|
||||||
|
styles={{
|
||||||
|
normal: {
|
||||||
|
"margin-bottom": "20px",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
order={0}
|
||||||
>
|
>
|
||||||
<BlockComponent
|
<BlockComponent
|
||||||
type="repeater"
|
type="heading"
|
||||||
bind:id={repeaterId}
|
props={{
|
||||||
context="repeater"
|
text: title,
|
||||||
|
}}
|
||||||
|
order={0}
|
||||||
|
/>
|
||||||
|
<BlockComponent
|
||||||
|
type="container"
|
||||||
props={{
|
props={{
|
||||||
dataProvider: `{{ literal ${safe(dataProviderId)} }}`,
|
|
||||||
direction: "row",
|
direction: "row",
|
||||||
hAlign: "stretch",
|
hAlign: "left",
|
||||||
vAlign: "top",
|
vAlign: "middle",
|
||||||
gap: "M",
|
gap: "M",
|
||||||
noRowsMessage: "No rows found",
|
wrap: true,
|
||||||
}}
|
|
||||||
styles={{
|
|
||||||
display: "grid",
|
|
||||||
"grid-template-columns": `repeat(auto-fill, minmax(min(${cardWidth}px, 100%), 1fr))`,
|
|
||||||
}}
|
}}
|
||||||
|
order={1}
|
||||||
>
|
>
|
||||||
<BlockComponent
|
{#if enrichedSearchColumns?.length}
|
||||||
type="spectrumcard"
|
{#each enrichedSearchColumns as column, idx}
|
||||||
props={{
|
<BlockComponent
|
||||||
title: cardTitle,
|
type={column.componentType}
|
||||||
subtitle: cardSubtitle,
|
props={{
|
||||||
description: cardDescription,
|
field: column.name,
|
||||||
imageURL: cardImageURL,
|
placeholder: column.name,
|
||||||
horizontal: cardHorizontal,
|
text: column.name,
|
||||||
showButton: showCardButton,
|
autoWidth: true,
|
||||||
buttonText: cardButtonText,
|
}}
|
||||||
buttonOnClick: cardButtonOnClick,
|
order={idx}
|
||||||
linkURL: fullCardURL,
|
styles={{
|
||||||
linkPeek: cardPeek,
|
normal: {
|
||||||
}}
|
width: "192px",
|
||||||
styles={{
|
},
|
||||||
width: "auto",
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{#if showTitleButton}
|
||||||
|
<BlockComponent
|
||||||
|
type="button"
|
||||||
|
props={{
|
||||||
|
onClick: titleButtonAction,
|
||||||
|
text: titleButtonText,
|
||||||
|
type: "cta",
|
||||||
|
}}
|
||||||
|
order={enrichedSearchColumns?.length ?? 0}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</BlockComponent>
|
</BlockComponent>
|
||||||
</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>
|
</BlockComponent>
|
||||||
</div>
|
</BlockComponent>
|
||||||
</Block>
|
</Block>
|
||||||
{/if}
|
{/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>
|
|
||||||
|
|
|
@ -17,45 +17,43 @@
|
||||||
export let vAlign
|
export let vAlign
|
||||||
export let gap
|
export let gap
|
||||||
|
|
||||||
let providerId
|
|
||||||
|
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const { styleable } = getContext("sdk")
|
|
||||||
|
let providerId
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Block>
|
<Block>
|
||||||
<div use:styleable={$component.styles}>
|
<BlockComponent
|
||||||
<BlockComponent
|
type="dataprovider"
|
||||||
type="dataprovider"
|
context="provider"
|
||||||
context="provider"
|
bind:id={providerId}
|
||||||
bind:id={providerId}
|
props={{
|
||||||
props={{
|
dataSource,
|
||||||
dataSource,
|
filter,
|
||||||
filter,
|
sortColumn,
|
||||||
sortColumn,
|
sortOrder,
|
||||||
sortOrder,
|
limit,
|
||||||
limit,
|
paginate,
|
||||||
paginate,
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{#if $component.empty}
|
||||||
{#if $component.empty}
|
<Placeholder />
|
||||||
<Placeholder />
|
{:else}
|
||||||
{:else}
|
<BlockComponent
|
||||||
<BlockComponent
|
type="repeater"
|
||||||
type="repeater"
|
context="repeater"
|
||||||
context="repeater"
|
containsSlot
|
||||||
props={{
|
props={{
|
||||||
dataProvider: `{{ literal ${safe(providerId)} }}`,
|
dataProvider: `{{ literal ${safe(providerId)} }}`,
|
||||||
noRowsMessage,
|
noRowsMessage,
|
||||||
direction,
|
direction,
|
||||||
hAlign,
|
hAlign,
|
||||||
vAlign,
|
vAlign,
|
||||||
gap,
|
gap,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</BlockComponent>
|
</BlockComponent>
|
||||||
{/if}
|
{/if}
|
||||||
</BlockComponent>
|
</BlockComponent>
|
||||||
</div>
|
|
||||||
</Block>
|
</Block>
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import Block from "components/Block.svelte"
|
import Block from "components/Block.svelte"
|
||||||
import BlockComponent from "components/BlockComponent.svelte"
|
import BlockComponent from "components/BlockComponent.svelte"
|
||||||
import { Heading } from "@budibase/bbui"
|
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
import { enrichSearchColumns, enrichFilter } from "utils/blocks.js"
|
import { enrichSearchColumns, enrichFilter } from "utils/blocks.js"
|
||||||
|
|
||||||
|
@ -29,9 +28,7 @@
|
||||||
export let titleButtonURL
|
export let titleButtonURL
|
||||||
export let titleButtonPeek
|
export let titleButtonPeek
|
||||||
|
|
||||||
const { fetchDatasourceSchema, styleable } = getContext("sdk")
|
const { fetchDatasourceSchema } = getContext("sdk")
|
||||||
const context = getContext("context")
|
|
||||||
const component = getContext("component")
|
|
||||||
|
|
||||||
let formId
|
let formId
|
||||||
let dataProviderId
|
let dataProviderId
|
||||||
|
@ -64,145 +61,116 @@
|
||||||
|
|
||||||
{#if schemaLoaded}
|
{#if schemaLoaded}
|
||||||
<Block>
|
<Block>
|
||||||
<div class={size} use:styleable={$component.styles}>
|
<BlockComponent
|
||||||
<BlockComponent
|
type="form"
|
||||||
type="form"
|
bind:id={formId}
|
||||||
bind:id={formId}
|
props={{
|
||||||
props={{ dataSource, disableValidation: true, editAutoColumns: true }}
|
dataSource,
|
||||||
>
|
disableValidation: true,
|
||||||
{#if title || enrichedSearchColumns?.length || showTitleButton}
|
editAutoColumns: true,
|
||||||
<div class="header" class:mobile={$context.device.mobile}>
|
size,
|
||||||
<div class="title">
|
}}
|
||||||
<Heading>{title || ""}</Heading>
|
>
|
||||||
</div>
|
{#if title || enrichedSearchColumns?.length || showTitleButton}
|
||||||
<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
|
<BlockComponent
|
||||||
type="dataprovider"
|
type="container"
|
||||||
bind:id={dataProviderId}
|
|
||||||
props={{
|
props={{
|
||||||
dataSource,
|
direction: "row",
|
||||||
filter: enrichedFilter,
|
hAlign: "stretch",
|
||||||
sortColumn,
|
vAlign: "middle",
|
||||||
sortOrder,
|
gap: "M",
|
||||||
paginate,
|
wrap: true,
|
||||||
limit: rowCount,
|
|
||||||
}}
|
}}
|
||||||
|
styles={{
|
||||||
|
normal: {
|
||||||
|
"margin-bottom": "20px",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
order={0}
|
||||||
>
|
>
|
||||||
<BlockComponent
|
<BlockComponent
|
||||||
type="table"
|
type="heading"
|
||||||
context="table"
|
|
||||||
props={{
|
props={{
|
||||||
dataProvider: `{{ literal ${safe(dataProviderId)} }}`,
|
text: title,
|
||||||
columns: tableColumns,
|
|
||||||
showAutoColumns,
|
|
||||||
rowCount,
|
|
||||||
quiet,
|
|
||||||
compact,
|
|
||||||
allowSelectRows,
|
|
||||||
size,
|
|
||||||
linkRows,
|
|
||||||
linkURL,
|
|
||||||
linkColumn,
|
|
||||||
linkPeek,
|
|
||||||
}}
|
}}
|
||||||
|
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>
|
</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>
|
</BlockComponent>
|
||||||
</div>
|
</BlockComponent>
|
||||||
</Block>
|
</Block>
|
||||||
{/if}
|
{/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>
|
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import ClientApp from "./components/ClientApp.svelte"
|
import ClientApp from "./components/ClientApp.svelte"
|
||||||
import {
|
import {
|
||||||
componentStore,
|
|
||||||
builderStore,
|
builderStore,
|
||||||
appStore,
|
appStore,
|
||||||
devToolsStore,
|
devToolsStore,
|
||||||
|
blockStore,
|
||||||
|
componentStore,
|
||||||
environmentStore,
|
environmentStore,
|
||||||
} from "./stores"
|
} from "./stores"
|
||||||
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-rollup.js"
|
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-rollup.js"
|
||||||
|
@ -50,6 +51,17 @@ const loadBudibase = async () => {
|
||||||
const enableDevTools = !get(builderStore).inBuilder && get(appStore).isDevApp
|
const enableDevTools = !get(builderStore).inBuilder && get(appStore).isDevApp
|
||||||
devToolsStore.actions.setEnabled(enableDevTools)
|
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
|
// Register any custom components
|
||||||
if (window["##BUDIBASE_CUSTOM_COMPONENTS##"]) {
|
if (window["##BUDIBASE_CUSTOM_COMPONENTS##"]) {
|
||||||
window["##BUDIBASE_CUSTOM_COMPONENTS##"].forEach(component => {
|
window["##BUDIBASE_CUSTOM_COMPONENTS##"].forEach(component => {
|
||||||
|
|
|
@ -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()
|
|
@ -85,6 +85,9 @@ const createBuilderStore = () => {
|
||||||
highlightSetting: setting => {
|
highlightSetting: setting => {
|
||||||
dispatchEvent("highlight-setting", { setting })
|
dispatchEvent("highlight-setting", { setting })
|
||||||
},
|
},
|
||||||
|
ejectBlock: (id, definition) => {
|
||||||
|
dispatchEvent("eject-block", { id, definition })
|
||||||
|
},
|
||||||
updateUsedPlugin: (name, hash) => {
|
updateUsedPlugin: (name, hash) => {
|
||||||
// Check if we used this plugin
|
// Check if we used this plugin
|
||||||
const used = get(store)?.usedPlugins?.find(x => x.name === name)
|
const used = get(store)?.usedPlugins?.find(x => x.name === name)
|
||||||
|
|
|
@ -17,6 +17,7 @@ export { devToolsStore } from "./devTools"
|
||||||
export { componentStore } from "./components"
|
export { componentStore } from "./components"
|
||||||
export { uploadStore } from "./uploads.js"
|
export { uploadStore } from "./uploads.js"
|
||||||
export { rowSelectionStore } from "./rowSelection.js"
|
export { rowSelectionStore } from "./rowSelection.js"
|
||||||
|
export { blockStore } from "./blocks.js"
|
||||||
export { environmentStore } from "./environment"
|
export { environmentStore } from "./environment"
|
||||||
|
|
||||||
// Context stores are layered and duplicated, so it is not a singleton
|
// Context stores are layered and duplicated, so it is not a singleton
|
||||||
|
|
|
@ -56,6 +56,16 @@
|
||||||
return
|
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
|
// Extract data from message
|
||||||
const {
|
const {
|
||||||
selectedComponentId,
|
selectedComponentId,
|
||||||
|
|
Loading…
Reference in New Issue