Merge branch 'cheeks-lab-day-eject-blocks' of github.com:Budibase/budibase into form-block

This commit is contained in:
Andrew Kingston 2022-08-25 08:53:07 +01:00
commit 8f86a415aa
31 changed files with 598 additions and 399 deletions

View File

@ -68,13 +68,28 @@ jobs:
] ]
env: env:
KUBECONFIG_FILE: '${{ secrets.RELEASE_KUBECONFIG }}' KUBECONFIG_FILE: '${{ secrets.RELEASE_KUBECONFIG }}'
- name: Re roll the services - name: Re roll app-service
uses: actions-hub/kubectl@master uses: actions-hub/kubectl@master
env: env:
KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }} KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }}
with: with:
args: rollout restart deployment proxy-service -n budibase && kubectl rollout restart deployment app-service -n budibase && kubectl rollout restart deployment worker-service -n budibase args: rollout restart deployment app-service -n budibase
- name: Re roll proxy-service
uses: actions-hub/kubectl@master
env:
KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }}
with:
args: rollout restart deployment proxy-service -n budibase
- name: Re roll worker-service
uses: actions-hub/kubectl@master
env:
KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }}
with:
args: rollout restart deployment worker-service -n budibase
- name: Discord Webhook Action - name: Discord Webhook Action
uses: tsickert/discord-webhook@v4.0.0 uses: tsickert/discord-webhook@v4.0.0

View File

@ -121,12 +121,26 @@ jobs:
env: env:
KUBECONFIG_FILE: '${{ secrets.RELEASE_KUBECONFIG }}' KUBECONFIG_FILE: '${{ secrets.RELEASE_KUBECONFIG }}'
- name: Re roll the services - name: Re roll app-service
uses: actions-hub/kubectl@master uses: actions-hub/kubectl@master
env: env:
KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }} KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }}
with: with:
args: rollout restart deployment proxy-service -n budibase && kubectl rollout restart deployment app-service -n budibase && kubectl rollout restart deployment worker-service -n budibase args: rollout restart deployment app-service -n budibase
- name: Re roll proxy-service
uses: actions-hub/kubectl@master
env:
KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }}
with:
args: rollout restart deployment proxy-service -n budibase
- name: Re roll worker-service
uses: actions-hub/kubectl@master
env:
KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }}
with:
args: rollout restart deployment worker-service -n budibase
- name: Discord Webhook Action - name: Discord Webhook Action
uses: tsickert/discord-webhook@v4.0.0 uses: tsickert/discord-webhook@v4.0.0

View File

@ -1,5 +1,5 @@
{ {
"version": "1.2.44-alpha.4", "version": "1.2.44-alpha.5",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/backend-core", "name": "@budibase/backend-core",
"version": "1.2.44-alpha.4", "version": "1.2.44-alpha.5",
"description": "Budibase backend core libraries used in server and worker", "description": "Budibase backend core libraries used in server and worker",
"main": "dist/src/index.js", "main": "dist/src/index.js",
"types": "dist/src/index.d.ts", "types": "dist/src/index.d.ts",
@ -20,7 +20,7 @@
"test:watch": "jest --watchAll" "test:watch": "jest --watchAll"
}, },
"dependencies": { "dependencies": {
"@budibase/types": "1.2.44-alpha.4", "@budibase/types": "1.2.44-alpha.5",
"@techpass/passport-openidconnect": "0.3.2", "@techpass/passport-openidconnect": "0.3.2",
"aws-sdk": "2.1030.0", "aws-sdk": "2.1030.0",
"bcrypt": "5.0.1", "bcrypt": "5.0.1",

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/bbui", "name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.", "description": "A UI solution used in the different Budibase projects.",
"version": "1.2.44-alpha.4", "version": "1.2.44-alpha.5",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"module": "dist/bbui.es.js", "module": "dist/bbui.es.js",
@ -38,7 +38,7 @@
], ],
"dependencies": { "dependencies": {
"@adobe/spectrum-css-workflow-icons": "^1.2.1", "@adobe/spectrum-css-workflow-icons": "^1.2.1",
"@budibase/string-templates": "1.2.44-alpha.4", "@budibase/string-templates": "1.2.44-alpha.5",
"@spectrum-css/actionbutton": "^1.0.1", "@spectrum-css/actionbutton": "^1.0.1",
"@spectrum-css/actiongroup": "^1.0.1", "@spectrum-css/actiongroup": "^1.0.1",
"@spectrum-css/avatar": "^3.0.2", "@spectrum-css/avatar": "^3.0.2",

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "1.2.44-alpha.4", "version": "1.2.44-alpha.5",
"license": "GPL-3.0", "license": "GPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -69,10 +69,10 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "1.2.44-alpha.4", "@budibase/bbui": "1.2.44-alpha.5",
"@budibase/client": "1.2.44-alpha.4", "@budibase/client": "1.2.44-alpha.5",
"@budibase/frontend-core": "1.2.44-alpha.4", "@budibase/frontend-core": "1.2.44-alpha.5",
"@budibase/string-templates": "1.2.44-alpha.4", "@budibase/string-templates": "1.2.44-alpha.5",
"@sentry/browser": "5.19.1", "@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1", "@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1", "@spectrum-css/vars": "^3.0.1",

View File

@ -354,6 +354,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 => {
@ -895,6 +905,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) => {

View File

@ -96,11 +96,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 => {
@ -196,6 +206,9 @@
block: "center", block: "center",
}) })
} }
} else if (type === "eject-block") {
const { id, definition } = data
await store.actions.components.handleEjectBlock(id, definition)
} else { } else {
console.warn(`Client sent unknown event type: ${type}`) console.warn(`Client sent unknown event type: ${type}`)
} }

View File

@ -2,7 +2,11 @@
import { store } from "builderStore" import { store } from "builderStore"
import { ActionMenu, MenuItem, Icon } from "@budibase/bbui" import { ActionMenu, MenuItem, Icon } from "@budibase/bbui"
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(new KeyboardEvent("keydown", { key, ctrlKey })) document.dispatchEvent(new KeyboardEvent("keydown", { key, ctrlKey }))
@ -20,6 +24,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"

View File

@ -112,6 +112,9 @@
} else if (e.key === "Enter") { } else if (e.key === "Enter") {
e.preventDefault() e.preventDefault()
$goto("./new") $goto("./new")
} else if (e.key === "e") {
e.preventDefault()
await store.actions.components.requestEjectBlock(component?._id)
} }
} else if (e.key === "Backspace" || e.key === "Delete") { } else if (e.key === "Backspace" || e.key === "Delete") {
// Don't show confirmation for the screen itself // Don't show confirmation for the screen itself

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/cli", "name": "@budibase/cli",
"version": "1.2.44-alpha.4", "version": "1.2.44-alpha.5",
"description": "Budibase CLI, for developers, self hosting and migrations.", "description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "src/index.js", "main": "src/index.js",
"bin": { "bin": {

View File

@ -4147,6 +4147,7 @@
} }
}, },
"repeaterblock": { "repeaterblock": {
"block": true,
"name": "Repeater block", "name": "Repeater block",
"icon": "ViewList", "icon": "ViewList",
"illegalChildren": [ "illegalChildren": [

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/client", "name": "@budibase/client",
"version": "1.2.44-alpha.4", "version": "1.2.44-alpha.5",
"license": "MPL-2.0", "license": "MPL-2.0",
"module": "dist/budibase-client.js", "module": "dist/budibase-client.js",
"main": "dist/budibase-client.js", "main": "dist/budibase-client.js",
@ -19,9 +19,9 @@
"dev:builder": "rollup -cw" "dev:builder": "rollup -cw"
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "1.2.44-alpha.4", "@budibase/bbui": "1.2.44-alpha.5",
"@budibase/frontend-core": "1.2.44-alpha.4", "@budibase/frontend-core": "1.2.44-alpha.5",
"@budibase/string-templates": "1.2.44-alpha.4", "@budibase/string-templates": "1.2.44-alpha.5",
"@spectrum-css/button": "^3.0.3", "@spectrum-css/button": "^3.0.3",
"@spectrum-css/card": "^3.0.3", "@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3", "@spectrum-css/divider": "^1.0.3",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import ClientApp from "./components/ClientApp.svelte" import ClientApp from "./components/ClientApp.svelte"
import { builderStore, appStore, devToolsStore } from "./stores" import { builderStore, appStore, devToolsStore, blockStore } from "./stores"
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-rollup.js" import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-rollup.js"
import { get } from "svelte/store" import { get } from "svelte/store"
@ -32,6 +32,17 @@ const loadBudibase = () => {
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()
}
}
// Create app if one hasn't been created yet // Create app if one hasn't been created yet
if (!app) { if (!app) {
app = new ClientApp({ app = new ClientApp({

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

@ -84,6 +84,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,

View File

@ -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"
// Context stores are layered and duplicated, so it is not a singleton // Context stores are layered and duplicated, so it is not a singleton
export { createContextStore } from "./context" export { createContextStore } from "./context"

View File

@ -1,12 +1,12 @@
{ {
"name": "@budibase/frontend-core", "name": "@budibase/frontend-core",
"version": "1.2.44-alpha.4", "version": "1.2.44-alpha.5",
"description": "Budibase frontend core libraries used in builder and client", "description": "Budibase frontend core libraries used in builder and client",
"author": "Budibase", "author": "Budibase",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"dependencies": { "dependencies": {
"@budibase/bbui": "1.2.44-alpha.4", "@budibase/bbui": "1.2.44-alpha.5",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"svelte": "^3.46.2" "svelte": "^3.46.2"
} }

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/server", "name": "@budibase/server",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "1.2.44-alpha.4", "version": "1.2.44-alpha.5",
"description": "Budibase Web Server", "description": "Budibase Web Server",
"main": "src/index.ts", "main": "src/index.ts",
"repository": { "repository": {
@ -77,11 +77,11 @@
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@apidevtools/swagger-parser": "10.0.3", "@apidevtools/swagger-parser": "10.0.3",
"@budibase/backend-core": "1.2.44-alpha.4", "@budibase/backend-core": "1.2.44-alpha.5",
"@budibase/client": "1.2.44-alpha.4", "@budibase/client": "1.2.44-alpha.5",
"@budibase/pro": "1.2.44-alpha.4", "@budibase/pro": "1.2.44-alpha.5",
"@budibase/string-templates": "1.2.44-alpha.4", "@budibase/string-templates": "1.2.44-alpha.5",
"@budibase/types": "1.2.44-alpha.4", "@budibase/types": "1.2.44-alpha.5",
"@bull-board/api": "3.7.0", "@bull-board/api": "3.7.0",
"@bull-board/koa": "3.9.4", "@bull-board/koa": "3.9.4",
"@elastic/elasticsearch": "7.10.0", "@elastic/elasticsearch": "7.10.0",

View File

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

View File

@ -1094,12 +1094,12 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@budibase/backend-core@1.2.44-alpha.4": "@budibase/backend-core@1.2.44-alpha.5":
version "1.2.44-alpha.4" version "1.2.44-alpha.5"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.2.44-alpha.4.tgz#aac2e3ed75b932f265de89cd6e96a6a98524ff0a" resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.2.44-alpha.5.tgz#f564ad4cc9b5e2cf080d1f5988fc6ffec44d6621"
integrity sha512-xPN5ge87rg0hIcCpxmmj3/9FxMf0IBvqvA69e64knrcCpDSFaWJeXfd9O9SH5GNzwjBy4FA1NxaQz40DCXjBCw== integrity sha512-RPO4h+kB6c0ivVRGSoZAOrFQFLwv8qkkS7WNjToX1AZKD1umrMQrnEwi/sldjNW6ZAKFiktZAHDyWO/Mb7AP/g==
dependencies: dependencies:
"@budibase/types" "1.2.44-alpha.4" "@budibase/types" "1.2.44-alpha.5"
"@techpass/passport-openidconnect" "0.3.2" "@techpass/passport-openidconnect" "0.3.2"
aws-sdk "2.1030.0" aws-sdk "2.1030.0"
bcrypt "5.0.1" bcrypt "5.0.1"
@ -1178,13 +1178,13 @@
svelte-flatpickr "^3.2.3" svelte-flatpickr "^3.2.3"
svelte-portal "^1.0.0" svelte-portal "^1.0.0"
"@budibase/pro@1.2.44-alpha.4": "@budibase/pro@1.2.44-alpha.5":
version "1.2.44-alpha.4" version "1.2.44-alpha.5"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.2.44-alpha.4.tgz#b0aee3d235be2bbccfdd6c70ed63e31e9da32c94" resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.2.44-alpha.5.tgz#7ae5e593eccddb749da17df0354929eaaadc4e60"
integrity sha512-ITrhf3PDNlcCxCiDcOCAxUpQZr3oiYobyr9Vyk92JVxKdsrT6GBOpRSii+zZdhoz3ZEjRP9ZhfYMDpTuGnCKLg== integrity sha512-lfHDUsD0dRHg/NE71i1zCzVal89ceWilnTHdJ0om3TdZ7qkqtsRUIE8/pz+NoqNAjZ13qG0p75Ue+NEjI24lAQ==
dependencies: dependencies:
"@budibase/backend-core" "1.2.44-alpha.4" "@budibase/backend-core" "1.2.44-alpha.5"
"@budibase/types" "1.2.44-alpha.4" "@budibase/types" "1.2.44-alpha.5"
"@koa/router" "8.0.8" "@koa/router" "8.0.8"
joi "17.6.0" joi "17.6.0"
node-fetch "^2.6.1" node-fetch "^2.6.1"
@ -1207,10 +1207,10 @@
svelte-apexcharts "^1.0.2" svelte-apexcharts "^1.0.2"
svelte-flatpickr "^3.1.0" svelte-flatpickr "^3.1.0"
"@budibase/types@1.2.44-alpha.4": "@budibase/types@1.2.44-alpha.5":
version "1.2.44-alpha.4" version "1.2.44-alpha.5"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.2.44-alpha.4.tgz#299092f6179c02a2eebf2c88fc35a50fba779cf4" resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.2.44-alpha.5.tgz#3de49c35291e37102b018cef4dd2f553e7121c23"
integrity sha512-yK0NKUX+ggKYQ8hrjcyaFhdQq8x7QgXAbI6suns/xC8BodHox3ISAyc8HtZ4U0BtS8ONHg5t71xNXl0nueJYmA== integrity sha512-ipOGpfUKvVnDtscujSKemTYgUMgFbmF+Sb/D5JUb58+A4vJz/F29hb5GjANv2x3Wm8BJtuG8NjhVm36rWV9kAg==
"@bull-board/api@3.7.0": "@bull-board/api@3.7.0":
version "3.7.0" version "3.7.0"

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/string-templates", "name": "@budibase/string-templates",
"version": "1.2.44-alpha.4", "version": "1.2.44-alpha.5",
"description": "Handlebars wrapper for Budibase templating.", "description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.cjs", "main": "src/index.cjs",
"module": "dist/bundle.mjs", "module": "dist/bundle.mjs",

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/types", "name": "@budibase/types",
"version": "1.2.44-alpha.4", "version": "1.2.44-alpha.5",
"description": "Budibase types", "description": "Budibase types",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/worker", "name": "@budibase/worker",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "1.2.44-alpha.4", "version": "1.2.44-alpha.5",
"description": "Budibase background service", "description": "Budibase background service",
"main": "src/index.ts", "main": "src/index.ts",
"repository": { "repository": {
@ -35,10 +35,10 @@
"author": "Budibase", "author": "Budibase",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@budibase/backend-core": "1.2.44-alpha.4", "@budibase/backend-core": "1.2.44-alpha.5",
"@budibase/pro": "1.2.44-alpha.4", "@budibase/pro": "1.2.44-alpha.5",
"@budibase/string-templates": "1.2.44-alpha.4", "@budibase/string-templates": "1.2.44-alpha.5",
"@budibase/types": "1.2.44-alpha.4", "@budibase/types": "1.2.44-alpha.5",
"@koa/router": "8.0.8", "@koa/router": "8.0.8",
"@sentry/node": "6.17.7", "@sentry/node": "6.17.7",
"@techpass/passport-openidconnect": "0.3.2", "@techpass/passport-openidconnect": "0.3.2",

View File

@ -291,12 +291,12 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@budibase/backend-core@1.2.44-alpha.4": "@budibase/backend-core@1.2.44-alpha.5":
version "1.2.44-alpha.4" version "1.2.44-alpha.5"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.2.44-alpha.4.tgz#aac2e3ed75b932f265de89cd6e96a6a98524ff0a" resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.2.44-alpha.5.tgz#f564ad4cc9b5e2cf080d1f5988fc6ffec44d6621"
integrity sha512-xPN5ge87rg0hIcCpxmmj3/9FxMf0IBvqvA69e64knrcCpDSFaWJeXfd9O9SH5GNzwjBy4FA1NxaQz40DCXjBCw== integrity sha512-RPO4h+kB6c0ivVRGSoZAOrFQFLwv8qkkS7WNjToX1AZKD1umrMQrnEwi/sldjNW6ZAKFiktZAHDyWO/Mb7AP/g==
dependencies: dependencies:
"@budibase/types" "1.2.44-alpha.4" "@budibase/types" "1.2.44-alpha.5"
"@techpass/passport-openidconnect" "0.3.2" "@techpass/passport-openidconnect" "0.3.2"
aws-sdk "2.1030.0" aws-sdk "2.1030.0"
bcrypt "5.0.1" bcrypt "5.0.1"
@ -325,21 +325,21 @@
uuid "8.3.2" uuid "8.3.2"
zlib "1.0.5" zlib "1.0.5"
"@budibase/pro@1.2.44-alpha.4": "@budibase/pro@1.2.44-alpha.5":
version "1.2.44-alpha.4" version "1.2.44-alpha.5"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.2.44-alpha.4.tgz#b0aee3d235be2bbccfdd6c70ed63e31e9da32c94" resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.2.44-alpha.5.tgz#7ae5e593eccddb749da17df0354929eaaadc4e60"
integrity sha512-ITrhf3PDNlcCxCiDcOCAxUpQZr3oiYobyr9Vyk92JVxKdsrT6GBOpRSii+zZdhoz3ZEjRP9ZhfYMDpTuGnCKLg== integrity sha512-lfHDUsD0dRHg/NE71i1zCzVal89ceWilnTHdJ0om3TdZ7qkqtsRUIE8/pz+NoqNAjZ13qG0p75Ue+NEjI24lAQ==
dependencies: dependencies:
"@budibase/backend-core" "1.2.44-alpha.4" "@budibase/backend-core" "1.2.44-alpha.5"
"@budibase/types" "1.2.44-alpha.4" "@budibase/types" "1.2.44-alpha.5"
"@koa/router" "8.0.8" "@koa/router" "8.0.8"
joi "17.6.0" joi "17.6.0"
node-fetch "^2.6.1" node-fetch "^2.6.1"
"@budibase/types@1.2.44-alpha.4": "@budibase/types@1.2.44-alpha.5":
version "1.2.44-alpha.4" version "1.2.44-alpha.5"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.2.44-alpha.4.tgz#299092f6179c02a2eebf2c88fc35a50fba779cf4" resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.2.44-alpha.5.tgz#3de49c35291e37102b018cef4dd2f553e7121c23"
integrity sha512-yK0NKUX+ggKYQ8hrjcyaFhdQq8x7QgXAbI6suns/xC8BodHox3ISAyc8HtZ4U0BtS8ONHg5t71xNXl0nueJYmA== integrity sha512-ipOGpfUKvVnDtscujSKemTYgUMgFbmF+Sb/D5JUb58+A4vJz/F29hb5GjANv2x3Wm8BJtuG8NjhVm36rWV9kAg==
"@cspotcode/source-map-consumer@0.8.0": "@cspotcode/source-map-consumer@0.8.0":
version "0.8.0" version "0.8.0"