Merge branch 'develop' of github.com:Budibase/budibase into feature/app-backups
This commit is contained in:
commit
2d4eb10aba
51 changed files with 1179 additions and 561 deletions
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.0.24-alpha.0",
|
||||
"version": "2.0.24-alpha.3",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/backend-core",
|
||||
"version": "2.0.24-alpha.0",
|
||||
"version": "2.0.24-alpha.3",
|
||||
"description": "Budibase backend core libraries used in server and worker",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
|
@ -20,7 +20,7 @@
|
|||
"test:watch": "jest --watchAll"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/types": "2.0.24-alpha.0",
|
||||
"@budibase/types": "2.0.24-alpha.3",
|
||||
"@shopify/jest-koa-mocks": "5.0.1",
|
||||
"@techpass/passport-openidconnect": "0.3.2",
|
||||
"aws-sdk": "2.1030.0",
|
||||
|
|
|
@ -214,6 +214,31 @@ export = class RedisWrapper {
|
|||
}
|
||||
}
|
||||
|
||||
async bulkGet(keys: string[]) {
|
||||
const db = this._db
|
||||
const prefixedKeys = keys.map(key => addDbPrefix(db, key))
|
||||
let response = await this.getClient().mget(prefixedKeys)
|
||||
if (Array.isArray(response)) {
|
||||
let final: any = {}
|
||||
let count = 0
|
||||
for (let result of response) {
|
||||
if (result) {
|
||||
let parsed
|
||||
try {
|
||||
parsed = JSON.parse(result)
|
||||
} catch (err) {
|
||||
parsed = result
|
||||
}
|
||||
final[keys[count]] = parsed
|
||||
}
|
||||
count++
|
||||
}
|
||||
return final
|
||||
} else {
|
||||
throw new Error(`Invalid response: ${response}`)
|
||||
}
|
||||
}
|
||||
|
||||
async store(key: string, value: any, expirySeconds: number | null = null) {
|
||||
const db = this._db
|
||||
if (typeof value === "object") {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/bbui",
|
||||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "2.0.24-alpha.0",
|
||||
"version": "2.0.24-alpha.3",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"module": "dist/bbui.es.js",
|
||||
|
@ -38,7 +38,7 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
||||
"@budibase/string-templates": "2.0.24-alpha.0",
|
||||
"@budibase/string-templates": "2.0.24-alpha.3",
|
||||
"@spectrum-css/actionbutton": "^1.0.1",
|
||||
"@spectrum-css/actiongroup": "^1.0.1",
|
||||
"@spectrum-css/avatar": "^3.0.2",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import filterTests from "../support/filterTests"
|
||||
const interact = require('../support/interact')
|
||||
const interact = require("../support/interact")
|
||||
|
||||
filterTests(["smoke", "all"], () => {
|
||||
context("Query Level Transformers", () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "2.0.24-alpha.0",
|
||||
"version": "2.0.24-alpha.3",
|
||||
"license": "GPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -71,10 +71,10 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "2.0.24-alpha.0",
|
||||
"@budibase/client": "2.0.24-alpha.0",
|
||||
"@budibase/frontend-core": "2.0.24-alpha.0",
|
||||
"@budibase/string-templates": "2.0.24-alpha.0",
|
||||
"@budibase/bbui": "2.0.24-alpha.3",
|
||||
"@budibase/client": "2.0.24-alpha.3",
|
||||
"@budibase/frontend-core": "2.0.24-alpha.3",
|
||||
"@budibase/string-templates": "2.0.24-alpha.3",
|
||||
"@sentry/browser": "5.19.1",
|
||||
"@spectrum-css/page": "^3.0.1",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
|
|
|
@ -185,43 +185,42 @@ export const makeComponentUnique = component => {
|
|||
// Replace component ID
|
||||
const oldId = component._id
|
||||
const newId = Helpers.uuid()
|
||||
component._id = newId
|
||||
let definition = JSON.stringify(component)
|
||||
|
||||
if (component._children?.length) {
|
||||
let children = JSON.stringify(component._children)
|
||||
// Replace all instances of this ID in HBS bindings
|
||||
definition = definition.replace(new RegExp(oldId, "g"), newId)
|
||||
|
||||
// Replace all instances of this ID in child HBS bindings
|
||||
children = children.replace(new RegExp(oldId, "g"), newId)
|
||||
// Replace all instances of this ID in JS bindings
|
||||
const bindings = findHBSBlocks(definition)
|
||||
bindings.forEach(binding => {
|
||||
// JSON.stringify will have escaped double quotes, so we need
|
||||
// to account for that
|
||||
let sanitizedBinding = binding.replace(/\\"/g, '"')
|
||||
|
||||
// Replace all instances of this ID in child JS bindings
|
||||
const bindings = findHBSBlocks(children)
|
||||
bindings.forEach(binding => {
|
||||
// JSON.stringify will have escaped double quotes, so we need
|
||||
// to account for that
|
||||
let sanitizedBinding = binding.replace(/\\"/g, '"')
|
||||
// Check if this is a valid JS binding
|
||||
let js = decodeJSBinding(sanitizedBinding)
|
||||
if (js != null) {
|
||||
// Replace ID inside JS binding
|
||||
js = js.replace(new RegExp(oldId, "g"), newId)
|
||||
|
||||
// Check if this is a valid JS binding
|
||||
let js = decodeJSBinding(sanitizedBinding)
|
||||
if (js != null) {
|
||||
// Replace ID inside JS binding
|
||||
js = js.replace(new RegExp(oldId, "g"), newId)
|
||||
// Create new valid JS binding
|
||||
let newBinding = encodeJSBinding(js)
|
||||
|
||||
// Create new valid JS binding
|
||||
let newBinding = encodeJSBinding(js)
|
||||
// Replace escaped double quotes
|
||||
newBinding = newBinding.replace(/"/g, '\\"')
|
||||
|
||||
// Replace escaped double quotes
|
||||
newBinding = newBinding.replace(/"/g, '\\"')
|
||||
// Insert new JS back into binding.
|
||||
// A single string replace here is better than a regex as
|
||||
// the binding contains special characters, and we only need
|
||||
// to replace a single instance.
|
||||
definition = definition.replace(binding, newBinding)
|
||||
}
|
||||
})
|
||||
|
||||
// Insert new JS back into binding.
|
||||
// A single string replace here is better than a regex as
|
||||
// the binding contains special characters, and we only need
|
||||
// to replace a single instance.
|
||||
children = children.replace(binding, newBinding)
|
||||
}
|
||||
})
|
||||
|
||||
// Recurse on all children
|
||||
component._children = JSON.parse(children)
|
||||
component._children.forEach(makeComponentUnique)
|
||||
// Recurse on all children
|
||||
component = JSON.parse(definition)
|
||||
return {
|
||||
...component,
|
||||
_children: component._children?.map(makeComponentUnique),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -169,7 +169,12 @@ export const getComponentBindableProperties = (asset, componentId) => {
|
|||
/**
|
||||
* Gets all data provider components above a component.
|
||||
*/
|
||||
export const getContextProviderComponents = (asset, componentId, type) => {
|
||||
export const getContextProviderComponents = (
|
||||
asset,
|
||||
componentId,
|
||||
type,
|
||||
options = { includeSelf: false }
|
||||
) => {
|
||||
if (!asset || !componentId) {
|
||||
return []
|
||||
}
|
||||
|
@ -177,7 +182,9 @@ export const getContextProviderComponents = (asset, componentId, type) => {
|
|||
// Get the component tree leading up to this component, ignoring the component
|
||||
// itself
|
||||
const path = findComponentPath(asset.props, componentId)
|
||||
path.pop()
|
||||
if (!options?.includeSelf) {
|
||||
path.pop()
|
||||
}
|
||||
|
||||
// Filter by only data provider components
|
||||
return path.filter(component => {
|
||||
|
@ -798,6 +805,17 @@ export const buildFormSchema = component => {
|
|||
if (!component) {
|
||||
return schema
|
||||
}
|
||||
|
||||
// If this is a form block, simply use the fields setting
|
||||
if (component._component.endsWith("formblock")) {
|
||||
let schema = {}
|
||||
component.fields?.forEach(field => {
|
||||
schema[field] = { type: "string" }
|
||||
})
|
||||
return schema
|
||||
}
|
||||
|
||||
// Otherwise find all field component children
|
||||
const settings = getComponentSettings(component._component)
|
||||
const fieldSetting = settings.find(
|
||||
setting => setting.key === "field" && setting.type.startsWith("field/")
|
||||
|
|
|
@ -330,6 +330,16 @@ export const getFrontendStore = () => {
|
|||
return state
|
||||
})
|
||||
},
|
||||
sendEvent: (name, payload) => {
|
||||
const { previewEventHandler } = get(store)
|
||||
previewEventHandler?.(name, payload)
|
||||
},
|
||||
registerEventHandler: handler => {
|
||||
store.update(state => {
|
||||
state.previewEventHandler = handler
|
||||
return state
|
||||
})
|
||||
},
|
||||
},
|
||||
layouts: {
|
||||
select: layoutId => {
|
||||
|
@ -611,7 +621,7 @@ export const getFrontendStore = () => {
|
|||
|
||||
// Make new component unique if copying
|
||||
if (!cut) {
|
||||
makeComponentUnique(componentToPaste)
|
||||
componentToPaste = makeComponentUnique(componentToPaste)
|
||||
}
|
||||
newComponentId = componentToPaste._id
|
||||
|
||||
|
@ -891,6 +901,50 @@ export const getFrontendStore = () => {
|
|||
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
|
||||
ejectedDefinition = 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: {
|
||||
save: async (url, title) => {
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
export let key
|
||||
export let actions
|
||||
export let bindings = []
|
||||
export let nested
|
||||
|
||||
$: showAvailableActions = !actions?.length
|
||||
|
||||
|
@ -187,6 +188,7 @@
|
|||
this={selectedActionComponent}
|
||||
parameters={selectedAction.parameters}
|
||||
bindings={allBindings}
|
||||
{nested}
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
export let value = []
|
||||
export let name
|
||||
export let bindings
|
||||
export let nested
|
||||
|
||||
let drawer
|
||||
let tmpValue
|
||||
|
@ -90,6 +91,7 @@
|
|||
eventType={name}
|
||||
{bindings}
|
||||
{key}
|
||||
{nested}
|
||||
/>
|
||||
</Drawer>
|
||||
|
||||
|
|
|
@ -10,11 +10,13 @@
|
|||
|
||||
export let parameters
|
||||
export let bindings = []
|
||||
export let nested
|
||||
|
||||
$: formComponents = getContextProviderComponents(
|
||||
$currentAsset,
|
||||
$store.selectedComponentId,
|
||||
"form"
|
||||
"form",
|
||||
{ includeSelf: nested }
|
||||
)
|
||||
$: schemaComponents = getContextProviderComponents(
|
||||
$currentAsset,
|
||||
|
|
|
@ -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 nested = false
|
||||
export let highlighted = false
|
||||
export let info = null
|
||||
|
||||
$: nullishValue = value == null || value === ""
|
||||
$: allBindings = getAllBindings(bindings, componentBindings, nested)
|
||||
|
@ -94,11 +95,15 @@
|
|||
bindings={allBindings}
|
||||
name={key}
|
||||
text={label}
|
||||
{nested}
|
||||
{key}
|
||||
{type}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
{#if info}
|
||||
<div class="text">{@html info}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
@ -123,4 +128,9 @@
|
|||
.control {
|
||||
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>
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
export let value
|
||||
export let bindings
|
||||
export let placeholder
|
||||
|
||||
$: urlOptions = $store.screens
|
||||
.map(screen => screen.routing?.route)
|
||||
|
@ -13,6 +14,7 @@
|
|||
<DrawerBindableCombobox
|
||||
{value}
|
||||
{bindings}
|
||||
{placeholder}
|
||||
on:change
|
||||
options={urlOptions}
|
||||
appendBindingsAsOptions={false}
|
||||
|
|
|
@ -98,11 +98,21 @@
|
|||
`./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
|
||||
const refreshContent = message => {
|
||||
if (iframe) {
|
||||
iframe.contentWindow.postMessage(message)
|
||||
}
|
||||
iframe?.contentWindow.postMessage(message)
|
||||
}
|
||||
|
||||
const receiveMessage = message => {
|
||||
|
@ -198,6 +208,9 @@
|
|||
block: "center",
|
||||
})
|
||||
}
|
||||
} else if (type === "eject-block") {
|
||||
const { id, definition } = data
|
||||
await store.actions.components.handleEjectBlock(id, definition)
|
||||
} else if (type === "reload-plugin") {
|
||||
await store.actions.components.refreshDefinitions()
|
||||
} else {
|
||||
|
|
|
@ -4,7 +4,9 @@
|
|||
|
||||
export let component
|
||||
|
||||
$: definition = store.actions.components.getDefinition(component?._component)
|
||||
$: noPaste = !$store.componentToPaste
|
||||
$: isBlock = definition?.block === true
|
||||
|
||||
const keyboardEvent = (key, ctrlKey = false) => {
|
||||
document.dispatchEvent(
|
||||
|
@ -30,6 +32,15 @@
|
|||
>
|
||||
Delete
|
||||
</MenuItem>
|
||||
{#if isBlock}
|
||||
<MenuItem
|
||||
icon="Export"
|
||||
keyBind="Ctrl+E"
|
||||
on:click={() => keyboardEvent("e", true)}
|
||||
>
|
||||
Eject block
|
||||
</MenuItem>
|
||||
{/if}
|
||||
<MenuItem
|
||||
icon="ChevronUp"
|
||||
keyBind="Ctrl+!ArrowUp"
|
||||
|
|
|
@ -7,7 +7,9 @@
|
|||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
|
||||
let confirmDeleteDialog
|
||||
let confirmEjectDialog
|
||||
let componentToDelete
|
||||
let componentToEject
|
||||
|
||||
const keyHandlers = {
|
||||
["^ArrowUp"]: async component => {
|
||||
|
@ -29,6 +31,10 @@
|
|||
store.actions.components.copy(component)
|
||||
await store.actions.components.paste(component, "below")
|
||||
},
|
||||
["^e"]: component => {
|
||||
componentToEject = component
|
||||
confirmEjectDialog.show()
|
||||
},
|
||||
["^Enter"]: () => {
|
||||
$goto("./new")
|
||||
},
|
||||
|
@ -124,3 +130,10 @@
|
|||
okText="Delete Component"
|
||||
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 PropertyControl from "components/design/settings/controls/PropertyControl.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"
|
||||
|
||||
export let componentDefinition
|
||||
|
@ -12,20 +13,29 @@
|
|||
export let componentBindings
|
||||
export let isScreen = false
|
||||
|
||||
$: sections = getSections(componentDefinition)
|
||||
$: sections = getSections(componentInstance, componentDefinition, isScreen)
|
||||
|
||||
const getSections = definition => {
|
||||
const getSections = (instance, definition, isScreen) => {
|
||||
const settings = definition?.settings ?? []
|
||||
const generalSettings = settings.filter(setting => !setting.section)
|
||||
const customSections = settings.filter(setting => setting.section)
|
||||
return [
|
||||
let sections = [
|
||||
{
|
||||
name: "General",
|
||||
info: componentDefinition?.info,
|
||||
settings: generalSettings,
|
||||
},
|
||||
...(customSections || []),
|
||||
]
|
||||
|
||||
// Filter out settings which shouldn't be rendered
|
||||
sections.forEach(section => {
|
||||
section.settings.forEach(setting => {
|
||||
setting.visible = canRenderControl(instance, setting, isScreen)
|
||||
})
|
||||
section.visible = section.settings.some(setting => setting.visible)
|
||||
})
|
||||
|
||||
return sections
|
||||
}
|
||||
|
||||
const updateSetting = async (key, value) => {
|
||||
|
@ -36,7 +46,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
const canRenderControl = (setting, isScreen) => {
|
||||
const canRenderControl = (instance, setting, isScreen) => {
|
||||
// Prevent rendering on click setting for screens
|
||||
if (setting?.type === "event" && isScreen) {
|
||||
return false
|
||||
|
@ -51,6 +61,7 @@
|
|||
if (setting.dependsOn) {
|
||||
let dependantSetting = setting.dependsOn
|
||||
let dependantValue = null
|
||||
let invert = !!setting.dependsOn.invert
|
||||
if (typeof setting.dependsOn === "object") {
|
||||
dependantSetting = setting.dependsOn.setting
|
||||
dependantValue = setting.dependsOn.value
|
||||
|
@ -62,7 +73,7 @@
|
|||
// If no specific value is depended upon, check if a value exists at all
|
||||
// for the dependent setting
|
||||
if (dependantValue == null) {
|
||||
const currentValue = componentInstance[dependantSetting]
|
||||
const currentValue = instance[dependantSetting]
|
||||
if (currentValue === false) {
|
||||
return false
|
||||
}
|
||||
|
@ -73,7 +84,11 @@
|
|||
}
|
||||
|
||||
// Otherwise check the value matches
|
||||
return componentInstance[dependantSetting] === dependantValue
|
||||
if (invert) {
|
||||
return instance[dependantSetting] !== dependantValue
|
||||
} else {
|
||||
return instance[dependantSetting] === dependantValue
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
|
@ -81,60 +96,54 @@
|
|||
</script>
|
||||
|
||||
{#each sections as section, idx (section.name)}
|
||||
<DetailSummary name={section.name} collapsible={false}>
|
||||
{#if idx === 0 && !componentInstance._component.endsWith("/layout") && !isScreen}
|
||||
<PropertyControl
|
||||
control={Input}
|
||||
label="Name"
|
||||
key="_instanceName"
|
||||
value={componentInstance._instanceName}
|
||||
onChange={val => updateSetting("_instanceName", val)}
|
||||
/>
|
||||
{/if}
|
||||
{#each section.settings as setting (setting.key)}
|
||||
{#if canRenderControl(setting, isScreen)}
|
||||
{#if section.visible}
|
||||
<DetailSummary name={section.name} collapsible={false}>
|
||||
{#if idx === 0 && !componentInstance._component.endsWith("/layout") && !isScreen}
|
||||
<PropertyControl
|
||||
type={setting.type}
|
||||
control={getComponentForSetting(setting)}
|
||||
label={setting.label}
|
||||
key={setting.key}
|
||||
value={componentInstance[setting.key]}
|
||||
defaultValue={setting.defaultValue}
|
||||
nested={setting.nested}
|
||||
onChange={val => updateSetting(setting.key, val)}
|
||||
highlighted={$store.highlightedSettingKey === setting.key}
|
||||
props={{
|
||||
// Generic settings
|
||||
placeholder: setting.placeholder || null,
|
||||
|
||||
// Select settings
|
||||
options: setting.options || [],
|
||||
|
||||
// Number fields
|
||||
min: setting.min || null,
|
||||
max: setting.max || null,
|
||||
}}
|
||||
{bindings}
|
||||
{componentBindings}
|
||||
{componentInstance}
|
||||
{componentDefinition}
|
||||
control={Input}
|
||||
label="Name"
|
||||
key="_instanceName"
|
||||
value={componentInstance._instanceName}
|
||||
onChange={val => updateSetting("_instanceName", val)}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if idx === 0 && componentDefinition?.component?.endsWith("/fieldgroup")}
|
||||
<ResetFieldsButton {componentInstance} />
|
||||
{/if}
|
||||
{#if section?.info}
|
||||
<div class="text">
|
||||
{@html section.info}
|
||||
</div>
|
||||
{/if}
|
||||
</DetailSummary>
|
||||
{/each}
|
||||
{#each section.settings as setting (setting.key)}
|
||||
{#if setting.visible}
|
||||
<PropertyControl
|
||||
type={setting.type}
|
||||
control={getComponentForSetting(setting)}
|
||||
label={setting.label}
|
||||
key={setting.key}
|
||||
value={componentInstance[setting.key]}
|
||||
defaultValue={setting.defaultValue}
|
||||
nested={setting.nested}
|
||||
onChange={val => updateSetting(setting.key, val)}
|
||||
highlighted={$store.highlightedSettingKey === setting.key}
|
||||
info={setting.info}
|
||||
props={{
|
||||
// Generic settings
|
||||
placeholder: setting.placeholder || null,
|
||||
|
||||
<style>
|
||||
.text {
|
||||
font-size: var(--spectrum-global-dimension-font-size-75);
|
||||
color: var(--grey-6);
|
||||
}
|
||||
</style>
|
||||
// Select settings
|
||||
options: setting.options || [],
|
||||
|
||||
// Number fields
|
||||
min: setting.min || null,
|
||||
max: setting.max || null,
|
||||
}}
|
||||
{bindings}
|
||||
{componentBindings}
|
||||
{componentInstance}
|
||||
{componentDefinition}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if idx === 0 && componentDefinition?.component?.endsWith("/fieldgroup")}
|
||||
<ResetFieldsButton {componentInstance} />
|
||||
{/if}
|
||||
{#if idx === 0 && componentDefinition?.block}
|
||||
<EjectBlockButton />
|
||||
{/if}
|
||||
</DetailSummary>
|
||||
{/if}
|
||||
{/each}
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
"children": [
|
||||
"tableblock",
|
||||
"cardsblock",
|
||||
"repeaterblock"
|
||||
"repeaterblock",
|
||||
"formblock"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
let duplicateScreen = Helpers.cloneDeep(screen)
|
||||
delete duplicateScreen._id
|
||||
delete duplicateScreen._rev
|
||||
makeComponentUnique(duplicateScreen.props)
|
||||
duplicateScreen.props = makeComponentUnique(duplicateScreen.props)
|
||||
|
||||
// Attach the new name and URL
|
||||
duplicateScreen.routing.route = sanitizeUrl(screenUrl)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/cli",
|
||||
"version": "2.0.24-alpha.0",
|
||||
"version": "2.0.24-alpha.3",
|
||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
|
@ -26,9 +26,9 @@
|
|||
"outputPath": "build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/backend-core": "2.0.24-alpha.0",
|
||||
"@budibase/string-templates": "2.0.24-alpha.0",
|
||||
"@budibase/types": "2.0.24-alpha.0",
|
||||
"@budibase/backend-core": "2.0.24-alpha.3",
|
||||
"@budibase/string-templates": "2.0.24-alpha.3",
|
||||
"@budibase/types": "2.0.24-alpha.3",
|
||||
"axios": "0.21.2",
|
||||
"chalk": "4.1.0",
|
||||
"cli-progress": "3.11.2",
|
||||
|
|
|
@ -3442,7 +3442,6 @@
|
|||
},
|
||||
"s3upload": {
|
||||
"name": "S3 File Upload",
|
||||
"info": "This component can't be used with S3 datasources that use custom endpoints.",
|
||||
"icon": "UploadToCloud",
|
||||
"styles": [
|
||||
"size"
|
||||
|
@ -3463,7 +3462,8 @@
|
|||
{
|
||||
"type": "dataSource/s3",
|
||||
"label": "S3 Datasource",
|
||||
"key": "datasourceId"
|
||||
"key": "datasourceId",
|
||||
"info": "This component can't be used with S3 datasources that use custom endpoints"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -3501,7 +3501,6 @@
|
|||
},
|
||||
"dataprovider": {
|
||||
"name": "Data Provider",
|
||||
"info": "Pagination is only available for data stored in tables.",
|
||||
"icon": "Data",
|
||||
"illegalChildren": [
|
||||
"section"
|
||||
|
@ -3547,7 +3546,8 @@
|
|||
"type": "boolean",
|
||||
"label": "Paginate",
|
||||
"key": "paginate",
|
||||
"defaultValue": true
|
||||
"defaultValue": true,
|
||||
"info": "Pagination is only available for data stored in tables"
|
||||
}
|
||||
],
|
||||
"context": {
|
||||
|
@ -3589,7 +3589,6 @@
|
|||
],
|
||||
"hasChildren": true,
|
||||
"showEmptyState": false,
|
||||
"info": "Row selection is only compatible with internal or SQL tables",
|
||||
"settings": [
|
||||
{
|
||||
"type": "dataProvider",
|
||||
|
@ -3646,7 +3645,8 @@
|
|||
"type": "boolean",
|
||||
"label": "Allow row selection",
|
||||
"key": "allowSelectRows",
|
||||
"defaultValue": false
|
||||
"defaultValue": false,
|
||||
"info": "Row selection is only compatible with internal or SQL tables"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
|
@ -3687,13 +3687,13 @@
|
|||
"size"
|
||||
],
|
||||
"hasChildren": false,
|
||||
"info": "Your data provider will be automatically filtered to the given date range.",
|
||||
"settings": [
|
||||
{
|
||||
"type": "dataProvider",
|
||||
"label": "Provider",
|
||||
"key": "dataProvider",
|
||||
"required": true
|
||||
"required": true,
|
||||
"info": "Your data provider will be automatically filtered to the given date range."
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
|
@ -3828,7 +3828,6 @@
|
|||
"styles": [
|
||||
"size"
|
||||
],
|
||||
"info": "Only the first 3 search columns will be used.",
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -3845,7 +3844,8 @@
|
|||
"type": "searchfield",
|
||||
"label": "Search Columns",
|
||||
"key": "searchColumns",
|
||||
"placeholder": "Choose search columns"
|
||||
"placeholder": "Choose search columns",
|
||||
"info": "Only the first 5 search columns will be used"
|
||||
},
|
||||
{
|
||||
"type": "filter",
|
||||
|
@ -3892,7 +3892,6 @@
|
|||
{
|
||||
"section": true,
|
||||
"name": "Table",
|
||||
"info": "Row selection is only compatible with internal or SQL tables",
|
||||
"settings": [
|
||||
{
|
||||
"type": "number",
|
||||
|
@ -3926,7 +3925,8 @@
|
|||
{
|
||||
"type": "boolean",
|
||||
"label": "Allow row selection",
|
||||
"key": "allowSelectRows"
|
||||
"key": "allowSelectRows",
|
||||
"info": "Row selection is only compatible with internal or SQL tables"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
|
@ -3993,7 +3993,6 @@
|
|||
"styles": [
|
||||
"size"
|
||||
],
|
||||
"info": "Only the first 3 search columns will be used.",
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -4010,7 +4009,8 @@
|
|||
"type": "searchfield",
|
||||
"label": "Search Columns",
|
||||
"key": "searchColumns",
|
||||
"placeholder": "Choose search columns"
|
||||
"placeholder": "Choose search columns",
|
||||
"info": "Only the first 5 search columns will be used"
|
||||
},
|
||||
{
|
||||
"type": "filter",
|
||||
|
@ -4157,6 +4157,7 @@
|
|||
}
|
||||
},
|
||||
"repeaterblock": {
|
||||
"block": true,
|
||||
"name": "Repeater block",
|
||||
"icon": "ViewList",
|
||||
"illegalChildren": [
|
||||
|
@ -4394,5 +4395,145 @@
|
|||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"formblock": {
|
||||
"name": "Form Block",
|
||||
"icon": "Form",
|
||||
"styles": ["size"],
|
||||
"block": true,
|
||||
"info": "Form blocks are only compatible with internal or SQL tables",
|
||||
"settings": [
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Type",
|
||||
"key": "actionType",
|
||||
"options": ["Create", "Update", "View"],
|
||||
"defaultValue": "Create"
|
||||
},
|
||||
{
|
||||
"type": "table",
|
||||
"label": "Table",
|
||||
"key": "dataSource"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Row ID",
|
||||
"key": "rowId",
|
||||
"nested": true,
|
||||
"dependsOn": {
|
||||
"setting": "actionType",
|
||||
"value": "Create",
|
||||
"invert": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Title",
|
||||
"key": "title",
|
||||
"nested": true
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Size",
|
||||
"key": "size",
|
||||
"options": [
|
||||
{
|
||||
"label": "Medium",
|
||||
"value": "spectrum--medium"
|
||||
},
|
||||
{
|
||||
"label": "Large",
|
||||
"value": "spectrum--large"
|
||||
}
|
||||
],
|
||||
"defaultValue": "spectrum--medium"
|
||||
},
|
||||
{
|
||||
"section": true,
|
||||
"name": "Fields",
|
||||
"settings": [
|
||||
{
|
||||
"type": "multifield",
|
||||
"label": "Fields",
|
||||
"key": "fields"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Field labels",
|
||||
"key": "labelPosition",
|
||||
"defaultValue": "left",
|
||||
"options": [
|
||||
{
|
||||
"label": "Left",
|
||||
"value": "left"
|
||||
},
|
||||
{
|
||||
"label": "Above",
|
||||
"value": "above"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Disabled",
|
||||
"key": "disabled",
|
||||
"defaultValue": false,
|
||||
"dependsOn": {
|
||||
"setting": "actionType",
|
||||
"value": "View",
|
||||
"invert": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": true,
|
||||
"name": "Buttons",
|
||||
"settings": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Show save button",
|
||||
"key": "showSaveButton",
|
||||
"defaultValue": true,
|
||||
"dependsOn": {
|
||||
"setting": "actionType",
|
||||
"value": "View",
|
||||
"invert": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Show delete button",
|
||||
"key": "showDeleteButton",
|
||||
"defaultValue": false,
|
||||
"dependsOn": {
|
||||
"setting": "actionType",
|
||||
"value": "Update"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "url",
|
||||
"label": "Navigate after button press",
|
||||
"key": "actionUrl",
|
||||
"placeholder": "Choose a screen",
|
||||
"dependsOn": {
|
||||
"setting": "actionType",
|
||||
"value": "View",
|
||||
"invert": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"context": [
|
||||
{
|
||||
"type": "form",
|
||||
"suffix": "form"
|
||||
},
|
||||
{
|
||||
"type": "schema",
|
||||
"suffix": "repeater"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/client",
|
||||
"version": "2.0.24-alpha.0",
|
||||
"version": "2.0.24-alpha.3",
|
||||
"license": "MPL-2.0",
|
||||
"module": "dist/budibase-client.js",
|
||||
"main": "dist/budibase-client.js",
|
||||
|
@ -19,9 +19,9 @@
|
|||
"dev:builder": "rollup -cw"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "2.0.24-alpha.0",
|
||||
"@budibase/frontend-core": "2.0.24-alpha.0",
|
||||
"@budibase/string-templates": "2.0.24-alpha.0",
|
||||
"@budibase/bbui": "2.0.24-alpha.3",
|
||||
"@budibase/frontend-core": "2.0.24-alpha.3",
|
||||
"@budibase/string-templates": "2.0.24-alpha.3",
|
||||
"@spectrum-css/button": "^3.0.3",
|
||||
"@spectrum-css/card": "^3.0.3",
|
||||
"@spectrum-css/divider": "^1.0.3",
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
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"
|
||||
|
||||
export const API = createAPIClient({
|
||||
|
|
|
@ -1,12 +1,92 @@
|
|||
<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 { styleable } = getContext("sdk")
|
||||
|
||||
// We need to set a block context to know we're inside a block, but also
|
||||
// to be able to reference the actual component ID of the block from
|
||||
// any depth
|
||||
setContext("block", { id: $component.id })
|
||||
let structureLookupMap = {}
|
||||
|
||||
const registerBlockComponent = (id, order, parentId, instance) => {
|
||||
// Ensure child array exists
|
||||
if (!structureLookupMap[parentId]) {
|
||||
structureLookupMap[parentId] = {}
|
||||
}
|
||||
// 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>
|
||||
|
||||
<slot />
|
||||
<div use:styleable={$component.styles}>
|
||||
<slot />
|
||||
</div>
|
||||
|
|
|
@ -1,17 +1,21 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { generate } from "shortid"
|
||||
import { builderStore } from "../stores/builder.js"
|
||||
import Component from "components/Component.svelte"
|
||||
|
||||
export let type
|
||||
export let props
|
||||
export let styles
|
||||
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
|
||||
// block components
|
||||
export let id
|
||||
|
||||
const component = getContext("component")
|
||||
const block = getContext("block")
|
||||
const rand = generate()
|
||||
|
||||
|
@ -21,13 +25,22 @@
|
|||
$: instance = {
|
||||
_component: `@budibase/standard-components/${type}`,
|
||||
_id: id,
|
||||
_instanceName: type[0].toUpperCase() + type.slice(1),
|
||||
_styles: {
|
||||
normal: {
|
||||
...styles,
|
||||
},
|
||||
...styles,
|
||||
normal: styles?.normal || {},
|
||||
},
|
||||
_containsSlot: containsSlot,
|
||||
...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>
|
||||
|
||||
<Component {instance} isBlock>
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
import { getContext } from "svelte"
|
||||
import Block from "components/Block.svelte"
|
||||
import BlockComponent from "components/BlockComponent.svelte"
|
||||
import { Heading } from "@budibase/bbui"
|
||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||
import { enrichSearchColumns, enrichFilter } from "utils/blocks.js"
|
||||
|
||||
|
@ -31,9 +30,7 @@
|
|||
export let cardButtonOnClick
|
||||
export let linkColumn
|
||||
|
||||
const { fetchDatasourceSchema, styleable } = getContext("sdk")
|
||||
const context = getContext("context")
|
||||
const component = getContext("component")
|
||||
const { fetchDatasourceSchema } = getContext("sdk")
|
||||
|
||||
let formId
|
||||
let dataProviderId
|
||||
|
@ -84,163 +81,132 @@
|
|||
|
||||
{#if schemaLoaded}
|
||||
<Block>
|
||||
<div class="card-list" use:styleable={$component.styles}>
|
||||
<BlockComponent
|
||||
type="form"
|
||||
bind:id={formId}
|
||||
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}"
|
||||
>
|
||||
{#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
|
||||
type="form"
|
||||
bind:id={formId}
|
||||
props={{ dataSource, disableValidation: true }}
|
||||
>
|
||||
{#if title || enrichedSearchColumns?.length || showTitleButton}
|
||||
<BlockComponent
|
||||
type="dataprovider"
|
||||
bind:id={dataProviderId}
|
||||
type="container"
|
||||
props={{
|
||||
dataSource,
|
||||
filter: enrichedFilter,
|
||||
sortColumn,
|
||||
sortOrder,
|
||||
paginate,
|
||||
limit,
|
||||
direction: "row",
|
||||
hAlign: "stretch",
|
||||
vAlign: "middle",
|
||||
gap: "M",
|
||||
wrap: true,
|
||||
}}
|
||||
styles={{
|
||||
normal: {
|
||||
"margin-bottom": "20px",
|
||||
},
|
||||
}}
|
||||
order={0}
|
||||
>
|
||||
<BlockComponent
|
||||
type="repeater"
|
||||
bind:id={repeaterId}
|
||||
context="repeater"
|
||||
type="heading"
|
||||
props={{
|
||||
text: title,
|
||||
}}
|
||||
order={0}
|
||||
/>
|
||||
<BlockComponent
|
||||
type="container"
|
||||
props={{
|
||||
dataProvider: `{{ literal ${safe(dataProviderId)} }}`,
|
||||
direction: "row",
|
||||
hAlign: "stretch",
|
||||
vAlign: "top",
|
||||
hAlign: "left",
|
||||
vAlign: "middle",
|
||||
gap: "M",
|
||||
noRowsMessage: "No rows found",
|
||||
}}
|
||||
styles={{
|
||||
display: "grid",
|
||||
"grid-template-columns": `repeat(auto-fill, minmax(min(${cardWidth}px, 100%), 1fr))`,
|
||||
wrap: true,
|
||||
}}
|
||||
order={1}
|
||||
>
|
||||
<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={{
|
||||
width: "auto",
|
||||
}}
|
||||
/>
|
||||
{#if enrichedSearchColumns?.length}
|
||||
{#each enrichedSearchColumns as column, idx}
|
||||
<BlockComponent
|
||||
type={column.componentType}
|
||||
props={{
|
||||
field: column.name,
|
||||
placeholder: column.name,
|
||||
text: column.name,
|
||||
autoWidth: true,
|
||||
}}
|
||||
order={idx}
|
||||
styles={{
|
||||
normal: {
|
||||
width: "192px",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if showTitleButton}
|
||||
<BlockComponent
|
||||
type="button"
|
||||
props={{
|
||||
onClick: titleButtonAction,
|
||||
text: titleButtonText,
|
||||
type: "cta",
|
||||
}}
|
||||
order={enrichedSearchColumns?.length ?? 0}
|
||||
/>
|
||||
{/if}
|
||||
</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>
|
||||
</div>
|
||||
</BlockComponent>
|
||||
</Block>
|
||||
{/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>
|
||||
|
|
239
packages/client/src/components/app/blocks/FormBlock.svelte
Normal file
239
packages/client/src/components/app/blocks/FormBlock.svelte
Normal file
|
@ -0,0 +1,239 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import BlockComponent from "../../BlockComponent.svelte"
|
||||
import Block from "../../Block.svelte"
|
||||
import Placeholder from "../Placeholder.svelte"
|
||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||
|
||||
export let actionType
|
||||
export let dataSource
|
||||
export let size
|
||||
export let disabled
|
||||
export let fields
|
||||
export let labelPosition
|
||||
export let title
|
||||
export let showSaveButton
|
||||
export let showDeleteButton
|
||||
export let rowId
|
||||
export let actionUrl
|
||||
|
||||
const { fetchDatasourceSchema, builderStore } = getContext("sdk")
|
||||
const FieldTypeToComponentMap = {
|
||||
string: "stringfield",
|
||||
number: "numberfield",
|
||||
options: "optionsfield",
|
||||
array: "multifieldselect",
|
||||
boolean: "booleanfield",
|
||||
longform: "longformfield",
|
||||
datetime: "datetimefield",
|
||||
attachment: "attachmentfield",
|
||||
link: "relationshipfield",
|
||||
json: "jsonfield",
|
||||
}
|
||||
|
||||
let schema
|
||||
let formId
|
||||
let providerId
|
||||
let repeaterId
|
||||
|
||||
$: fetchSchema(dataSource)
|
||||
$: onSave = [
|
||||
{
|
||||
"##eventHandlerType": "Save Row",
|
||||
parameters: {
|
||||
providerId: formId,
|
||||
tableId: dataSource?.tableId,
|
||||
},
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Screen Modal",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Navigate To",
|
||||
parameters: {
|
||||
url: actionUrl,
|
||||
},
|
||||
},
|
||||
]
|
||||
$: onDelete = [
|
||||
{
|
||||
"##eventHandlerType": "Delete Row",
|
||||
parameters: {
|
||||
confirm: true,
|
||||
tableId: dataSource?.tableId,
|
||||
rowId: `{{ ${safe(repeaterId)}.${safe("_id")} }}`,
|
||||
revId: `{{ ${safe(repeaterId)}.${safe("_rev")} }}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Screen Modal",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Navigate To",
|
||||
parameters: {
|
||||
url: actionUrl,
|
||||
},
|
||||
},
|
||||
]
|
||||
$: filter = [
|
||||
{
|
||||
field: "_id",
|
||||
operator: "equal",
|
||||
type: "string",
|
||||
value: rowId,
|
||||
valueType: "Binding",
|
||||
},
|
||||
]
|
||||
// If we're using an "update" form, use the real data provider. If we're
|
||||
// using a create form, we just want a fake array so that our repeater
|
||||
// will actually render the form, but data doesn't matter.
|
||||
$: dataProvider =
|
||||
actionType !== "Create"
|
||||
? `{{ literal ${safe(providerId)} }}`
|
||||
: { rows: [{}] }
|
||||
$: renderDeleteButton = showDeleteButton && actionType === "Update"
|
||||
$: renderSaveButton = showSaveButton && actionType !== "View"
|
||||
$: renderButtons = renderDeleteButton || renderSaveButton
|
||||
$: renderHeader = renderButtons || title
|
||||
|
||||
const fetchSchema = async () => {
|
||||
schema = (await fetchDatasourceSchema(dataSource)) || {}
|
||||
}
|
||||
|
||||
const getComponentForField = field => {
|
||||
if (!field || !schema?.[field]) {
|
||||
return null
|
||||
}
|
||||
const type = schema[field].type
|
||||
return FieldTypeToComponentMap[type]
|
||||
}
|
||||
</script>
|
||||
|
||||
<Block>
|
||||
{#if fields?.length}
|
||||
<BlockComponent
|
||||
type="dataprovider"
|
||||
context="provider"
|
||||
bind:id={providerId}
|
||||
props={{
|
||||
dataSource,
|
||||
filter,
|
||||
limit: rowId ? 1 : $builderStore.inBuilder ? 1 : 0,
|
||||
paginate: false,
|
||||
}}
|
||||
>
|
||||
<BlockComponent
|
||||
type="repeater"
|
||||
context="repeater"
|
||||
bind:id={repeaterId}
|
||||
props={{
|
||||
dataProvider,
|
||||
noRowsMessage: "We couldn't find a row to display",
|
||||
}}
|
||||
>
|
||||
<BlockComponent
|
||||
type="form"
|
||||
props={{
|
||||
actionType: actionType === "Create" ? "Create" : "Update",
|
||||
dataSource,
|
||||
size,
|
||||
disabled: disabled || actionType === "View",
|
||||
}}
|
||||
context="form"
|
||||
bind:id={formId}
|
||||
>
|
||||
<BlockComponent
|
||||
type="container"
|
||||
props={{
|
||||
direction: "column",
|
||||
hAlign: "stretch",
|
||||
vAlign: "top",
|
||||
gap: "M",
|
||||
}}
|
||||
>
|
||||
{#if renderHeader}
|
||||
<BlockComponent
|
||||
type="container"
|
||||
props={{
|
||||
direction: "row",
|
||||
hAlign: "stretch",
|
||||
vAlign: "center",
|
||||
gap: "M",
|
||||
wrap: true,
|
||||
}}
|
||||
order={0}
|
||||
>
|
||||
<BlockComponent
|
||||
type="heading"
|
||||
props={{ text: title || "" }}
|
||||
order={0}
|
||||
/>
|
||||
{#if renderButtons}
|
||||
<BlockComponent
|
||||
type="container"
|
||||
props={{
|
||||
direction: "row",
|
||||
hAlign: "stretch",
|
||||
vAlign: "center",
|
||||
gap: "M",
|
||||
wrap: true,
|
||||
}}
|
||||
order={1}
|
||||
>
|
||||
{#if renderDeleteButton}
|
||||
<BlockComponent
|
||||
type="button"
|
||||
props={{
|
||||
text: "Delete",
|
||||
onClick: onDelete,
|
||||
quiet: true,
|
||||
type: "secondary",
|
||||
}}
|
||||
order={0}
|
||||
/>
|
||||
{/if}
|
||||
{#if renderSaveButton}
|
||||
<BlockComponent
|
||||
type="button"
|
||||
props={{
|
||||
text: "Save",
|
||||
onClick: onSave,
|
||||
type: "cta",
|
||||
}}
|
||||
order={1}
|
||||
/>
|
||||
{/if}
|
||||
</BlockComponent>
|
||||
{/if}
|
||||
</BlockComponent>
|
||||
{/if}
|
||||
<BlockComponent
|
||||
type="fieldgroup"
|
||||
props={{ labelPosition }}
|
||||
order={1}
|
||||
>
|
||||
{#each fields as field, idx}
|
||||
{#if getComponentForField(field)}
|
||||
<BlockComponent
|
||||
type={getComponentForField(field)}
|
||||
props={{
|
||||
field,
|
||||
label: field,
|
||||
placeholder: field,
|
||||
disabled,
|
||||
}}
|
||||
order={idx}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
</BlockComponent>
|
||||
</BlockComponent>
|
||||
</BlockComponent>
|
||||
</BlockComponent>
|
||||
</BlockComponent>
|
||||
{:else}
|
||||
<Placeholder
|
||||
text="Choose your table and add some fields to your form to get started"
|
||||
/>
|
||||
{/if}
|
||||
</Block>
|
|
@ -17,45 +17,43 @@
|
|||
export let vAlign
|
||||
export let gap
|
||||
|
||||
let providerId
|
||||
|
||||
const component = getContext("component")
|
||||
const { styleable } = getContext("sdk")
|
||||
|
||||
let providerId
|
||||
</script>
|
||||
|
||||
<Block>
|
||||
<div use:styleable={$component.styles}>
|
||||
<BlockComponent
|
||||
type="dataprovider"
|
||||
context="provider"
|
||||
bind:id={providerId}
|
||||
props={{
|
||||
dataSource,
|
||||
filter,
|
||||
sortColumn,
|
||||
sortOrder,
|
||||
limit,
|
||||
paginate,
|
||||
}}
|
||||
>
|
||||
{#if $component.empty}
|
||||
<Placeholder />
|
||||
{:else}
|
||||
<BlockComponent
|
||||
type="repeater"
|
||||
context="repeater"
|
||||
props={{
|
||||
dataProvider: `{{ literal ${safe(providerId)} }}`,
|
||||
noRowsMessage,
|
||||
direction,
|
||||
hAlign,
|
||||
vAlign,
|
||||
gap,
|
||||
}}
|
||||
>
|
||||
<slot />
|
||||
</BlockComponent>
|
||||
{/if}
|
||||
</BlockComponent>
|
||||
</div>
|
||||
<BlockComponent
|
||||
type="dataprovider"
|
||||
context="provider"
|
||||
bind:id={providerId}
|
||||
props={{
|
||||
dataSource,
|
||||
filter,
|
||||
sortColumn,
|
||||
sortOrder,
|
||||
limit,
|
||||
paginate,
|
||||
}}
|
||||
>
|
||||
{#if $component.empty}
|
||||
<Placeholder />
|
||||
{:else}
|
||||
<BlockComponent
|
||||
type="repeater"
|
||||
context="repeater"
|
||||
containsSlot
|
||||
props={{
|
||||
dataProvider: `{{ literal ${safe(providerId)} }}`,
|
||||
noRowsMessage,
|
||||
direction,
|
||||
hAlign,
|
||||
vAlign,
|
||||
gap,
|
||||
}}
|
||||
>
|
||||
<slot />
|
||||
</BlockComponent>
|
||||
{/if}
|
||||
</BlockComponent>
|
||||
</Block>
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
import { getContext } from "svelte"
|
||||
import Block from "components/Block.svelte"
|
||||
import BlockComponent from "components/BlockComponent.svelte"
|
||||
import { Heading } from "@budibase/bbui"
|
||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||
import { enrichSearchColumns, enrichFilter } from "utils/blocks.js"
|
||||
|
||||
|
@ -29,9 +28,7 @@
|
|||
export let titleButtonURL
|
||||
export let titleButtonPeek
|
||||
|
||||
const { fetchDatasourceSchema, styleable } = getContext("sdk")
|
||||
const context = getContext("context")
|
||||
const component = getContext("component")
|
||||
const { fetchDatasourceSchema } = getContext("sdk")
|
||||
|
||||
let formId
|
||||
let dataProviderId
|
||||
|
@ -64,145 +61,116 @@
|
|||
|
||||
{#if schemaLoaded}
|
||||
<Block>
|
||||
<div class={size} use:styleable={$component.styles}>
|
||||
<BlockComponent
|
||||
type="form"
|
||||
bind:id={formId}
|
||||
props={{ dataSource, disableValidation: true, editAutoColumns: 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}"
|
||||
>
|
||||
{#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
|
||||
type="form"
|
||||
bind:id={formId}
|
||||
props={{
|
||||
dataSource,
|
||||
disableValidation: true,
|
||||
editAutoColumns: true,
|
||||
size,
|
||||
}}
|
||||
>
|
||||
{#if title || enrichedSearchColumns?.length || showTitleButton}
|
||||
<BlockComponent
|
||||
type="dataprovider"
|
||||
bind:id={dataProviderId}
|
||||
type="container"
|
||||
props={{
|
||||
dataSource,
|
||||
filter: enrichedFilter,
|
||||
sortColumn,
|
||||
sortOrder,
|
||||
paginate,
|
||||
limit: rowCount,
|
||||
direction: "row",
|
||||
hAlign: "stretch",
|
||||
vAlign: "middle",
|
||||
gap: "M",
|
||||
wrap: true,
|
||||
}}
|
||||
styles={{
|
||||
normal: {
|
||||
"margin-bottom": "20px",
|
||||
},
|
||||
}}
|
||||
order={0}
|
||||
>
|
||||
<BlockComponent
|
||||
type="table"
|
||||
context="table"
|
||||
type="heading"
|
||||
props={{
|
||||
dataProvider: `{{ literal ${safe(dataProviderId)} }}`,
|
||||
columns: tableColumns,
|
||||
showAutoColumns,
|
||||
rowCount,
|
||||
quiet,
|
||||
compact,
|
||||
allowSelectRows,
|
||||
size,
|
||||
linkRows,
|
||||
linkURL,
|
||||
linkColumn,
|
||||
linkPeek,
|
||||
text: title,
|
||||
}}
|
||||
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>
|
||||
{/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>
|
||||
</div>
|
||||
</BlockComponent>
|
||||
</Block>
|
||||
{/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,3 +1,4 @@
|
|||
export { default as tableblock } from "./TableBlock.svelte"
|
||||
export { default as cardsblock } from "./CardsBlock.svelte"
|
||||
export { default as repeaterblock } from "./RepeaterBlock.svelte"
|
||||
export { default as formblock } from "./FormBlock.svelte"
|
||||
|
|
|
@ -48,36 +48,7 @@
|
|||
|
||||
// Fetches the form schema from this form's dataSource
|
||||
const fetchSchema = async dataSource => {
|
||||
if (!dataSource) {
|
||||
schema = {}
|
||||
}
|
||||
|
||||
// If the datasource is a query, then we instead use a schema of the query
|
||||
// parameters rather than the output schema
|
||||
else if (
|
||||
dataSource.type === "query" &&
|
||||
dataSource._id &&
|
||||
actionType === "Create"
|
||||
) {
|
||||
try {
|
||||
const query = await API.fetchQueryDefinition(dataSource._id)
|
||||
let paramSchema = {}
|
||||
const params = query.parameters || []
|
||||
params.forEach(param => {
|
||||
paramSchema[param.name] = { ...param, type: "string" }
|
||||
})
|
||||
schema = paramSchema
|
||||
} catch (error) {
|
||||
schema = {}
|
||||
}
|
||||
}
|
||||
|
||||
// For all other cases, just grab the normal schema
|
||||
else {
|
||||
const dataSourceSchema = await fetchDatasourceSchema(dataSource)
|
||||
schema = dataSourceSchema || {}
|
||||
}
|
||||
|
||||
schema = (await fetchDatasourceSchema(dataSource)) || {}
|
||||
if (!loaded) {
|
||||
loaded = true
|
||||
}
|
||||
|
@ -95,7 +66,7 @@
|
|||
|
||||
$: initialValues = getInitialValues(actionType, dataSource, $context)
|
||||
$: resetKey = Helpers.hashString(
|
||||
JSON.stringify(initialValues) + JSON.stringify(schema)
|
||||
JSON.stringify(initialValues) + JSON.stringify(schema) + disabled
|
||||
)
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import ClientApp from "./components/ClientApp.svelte"
|
||||
import {
|
||||
componentStore,
|
||||
builderStore,
|
||||
appStore,
|
||||
devToolsStore,
|
||||
blockStore,
|
||||
componentStore,
|
||||
environmentStore,
|
||||
} from "./stores"
|
||||
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-rollup.js"
|
||||
|
@ -50,6 +51,17 @@ const loadBudibase = async () => {
|
|||
const enableDevTools = !get(builderStore).inBuilder && get(appStore).isDevApp
|
||||
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
|
||||
if (window["##BUDIBASE_CUSTOM_COMPONENTS##"]) {
|
||||
window["##BUDIBASE_CUSTOM_COMPONENTS##"].forEach(component => {
|
||||
|
|
34
packages/client/src/stores/blocks.js
Normal file
34
packages/client/src/stores/blocks.js
Normal 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()
|
|
@ -85,6 +85,9 @@ const createBuilderStore = () => {
|
|||
highlightSetting: setting => {
|
||||
dispatchEvent("highlight-setting", { setting })
|
||||
},
|
||||
ejectBlock: (id, definition) => {
|
||||
dispatchEvent("eject-block", { id, definition })
|
||||
},
|
||||
updateUsedPlugin: (name, hash) => {
|
||||
// Check if we used this plugin
|
||||
const used = get(store)?.usedPlugins?.find(x => x.name === name)
|
||||
|
|
|
@ -17,6 +17,7 @@ export { devToolsStore } from "./devTools"
|
|||
export { componentStore } from "./components"
|
||||
export { uploadStore } from "./uploads.js"
|
||||
export { rowSelectionStore } from "./rowSelection.js"
|
||||
export { blockStore } from "./blocks.js"
|
||||
export { environmentStore } from "./environment"
|
||||
|
||||
// Context stores are layered and duplicated, so it is not a singleton
|
||||
|
|
|
@ -16,7 +16,7 @@ import JSONArrayFetch from "@budibase/frontend-core/src/fetch/JSONArrayFetch.js"
|
|||
*/
|
||||
export const fetchDatasourceSchema = async (
|
||||
datasource,
|
||||
options = { enrichRelationships: false }
|
||||
options = { enrichRelationships: false, formSchema: false }
|
||||
) => {
|
||||
const handler = {
|
||||
table: TableFetch,
|
||||
|
@ -34,7 +34,17 @@ export const fetchDatasourceSchema = async (
|
|||
|
||||
// Get the datasource definition and then schema
|
||||
const definition = await instance.getDefinition(datasource)
|
||||
let schema = instance.getSchema(datasource, definition)
|
||||
|
||||
// Get the normal schema as long as we aren't wanting a form schema
|
||||
let schema
|
||||
if (datasource?.type !== "query" || !options?.formSchema) {
|
||||
schema = instance.getSchema(datasource, definition)
|
||||
} else if (definition.parameters?.length) {
|
||||
schema = {}
|
||||
definition.parameters.forEach(param => {
|
||||
schema[param.name] = { ...param, type: "string" }
|
||||
})
|
||||
}
|
||||
if (!schema) {
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "@budibase/frontend-core",
|
||||
"version": "2.0.24-alpha.0",
|
||||
"version": "2.0.24-alpha.3",
|
||||
"description": "Budibase frontend core libraries used in builder and client",
|
||||
"author": "Budibase",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "2.0.24-alpha.0",
|
||||
"@budibase/bbui": "2.0.24-alpha.3",
|
||||
"lodash": "^4.17.21",
|
||||
"svelte": "^3.46.2"
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/sdk",
|
||||
"version": "2.0.24-alpha.0",
|
||||
"version": "2.0.24-alpha.3",
|
||||
"description": "Budibase Public API SDK",
|
||||
"author": "Budibase",
|
||||
"license": "MPL-2.0",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/server",
|
||||
"email": "hi@budibase.com",
|
||||
"version": "2.0.24-alpha.0",
|
||||
"version": "2.0.24-alpha.3",
|
||||
"description": "Budibase Web Server",
|
||||
"main": "src/index.ts",
|
||||
"repository": {
|
||||
|
@ -77,11 +77,11 @@
|
|||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-parser": "10.0.3",
|
||||
"@budibase/backend-core": "2.0.24-alpha.0",
|
||||
"@budibase/client": "2.0.24-alpha.0",
|
||||
"@budibase/pro": "2.0.23",
|
||||
"@budibase/string-templates": "2.0.24-alpha.0",
|
||||
"@budibase/types": "2.0.24-alpha.0",
|
||||
"@budibase/backend-core": "2.0.24-alpha.3",
|
||||
"@budibase/client": "2.0.24-alpha.3",
|
||||
"@budibase/pro": "2.0.24-alpha.3",
|
||||
"@budibase/string-templates": "2.0.24-alpha.3",
|
||||
"@budibase/types": "2.0.24-alpha.3",
|
||||
"@bull-board/api": "3.7.0",
|
||||
"@bull-board/koa": "3.9.4",
|
||||
"@elastic/elasticsearch": "7.10.0",
|
||||
|
|
|
@ -32,7 +32,7 @@ const {
|
|||
import { USERS_TABLE_SCHEMA } from "../../constants"
|
||||
import { removeAppFromUserRoles } from "../../utilities/workerRequests"
|
||||
import { clientLibraryPath, stringToReadStream } from "../../utilities"
|
||||
import { getAllLocks } from "../../utilities/redis"
|
||||
import { getLocksById } from "../../utilities/redis"
|
||||
import {
|
||||
updateClientLibrary,
|
||||
backupClientLibrary,
|
||||
|
@ -45,11 +45,10 @@ import { cleanupAutomations } from "../../automations/utils"
|
|||
import { context } from "@budibase/backend-core"
|
||||
import { checkAppMetadata } from "../../automations/logging"
|
||||
import { getUniqueRows } from "../../utilities/usageQuota/rows"
|
||||
import { quotas } from "@budibase/pro"
|
||||
import { quotas, groups } from "@budibase/pro"
|
||||
import { errors, events, migrations } from "@budibase/backend-core"
|
||||
import { App, Layout, Screen, MigrationType } from "@budibase/types"
|
||||
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
|
||||
import { groups } from "@budibase/pro"
|
||||
import { enrichPluginURLs } from "../../utilities/plugins"
|
||||
|
||||
const URL_REGEX_SLASH = /\/|\\/g
|
||||
|
@ -172,16 +171,16 @@ export const fetch = async (ctx: any) => {
|
|||
const all = ctx.query && ctx.query.status === AppStatus.ALL
|
||||
const apps = await getAllApps({ dev, all })
|
||||
|
||||
const appIds = apps
|
||||
.filter((app: any) => app.status === "development")
|
||||
.map((app: any) => app.appId)
|
||||
// get the locks for all the dev apps
|
||||
if (dev || all) {
|
||||
const locks = await getAllLocks()
|
||||
const locks = await getLocksById(appIds)
|
||||
for (let app of apps) {
|
||||
if (app.status !== "development") {
|
||||
continue
|
||||
}
|
||||
const lock = locks.find((lock: any) => lock.appId === app.appId)
|
||||
const lock = locks[app.appId]
|
||||
if (lock) {
|
||||
app.lockedBy = lock.user
|
||||
app.lockedBy = lock
|
||||
} else {
|
||||
// make sure its definitely not present
|
||||
delete app.lockedBy
|
||||
|
|
|
@ -103,7 +103,7 @@ exports.revert = async ctx => {
|
|||
target: appId,
|
||||
})
|
||||
try {
|
||||
if (!env.isTest()) {
|
||||
if (!env.isCypress()) {
|
||||
// in-memory db stalls on rollback
|
||||
await replication.rollback()
|
||||
}
|
||||
|
|
|
@ -56,6 +56,16 @@
|
|||
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
|
||||
const {
|
||||
selectedComponentId,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
jest.mock("../../../utilities/redis", () => ({
|
||||
init: jest.fn(),
|
||||
getAllLocks: () => {
|
||||
return []
|
||||
getLocksById: () => {
|
||||
return {}
|
||||
},
|
||||
doesUserHaveLock: () => {
|
||||
return true
|
||||
|
|
|
@ -34,12 +34,8 @@ exports.doesUserHaveLock = async (devAppId, user) => {
|
|||
return expected === userId
|
||||
}
|
||||
|
||||
exports.getAllLocks = async () => {
|
||||
const locks = await devAppClient.scan()
|
||||
return locks.map(lock => ({
|
||||
appId: lock.key,
|
||||
user: lock.value,
|
||||
}))
|
||||
exports.getLocksById = async appIds => {
|
||||
return await devAppClient.bulkGet(appIds)
|
||||
}
|
||||
|
||||
exports.updateLock = async (devAppId, user) => {
|
||||
|
|
|
@ -1094,12 +1094,12 @@
|
|||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||
|
||||
"@budibase/backend-core@2.0.23":
|
||||
version "2.0.23"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.0.23.tgz#915d4ea2e78547795894b2aebd81b8a8c9b568db"
|
||||
integrity sha512-wovJF+kS/vVs+cR6lHieEEs99vmXsOt5qKjMAkkMUVXmWBWPYQaLppz+dxTV9xXd8Ht9yFhnkbOo7QJxFE8+fA==
|
||||
"@budibase/backend-core@2.0.24-alpha.3":
|
||||
version "2.0.24-alpha.3"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.0.24-alpha.3.tgz#49a92082d1ca6bed0eb82519a6c02d7d14ead751"
|
||||
integrity sha512-Zm/ddRDMzMuXCoEXZa0CzA/B1SnpQ+yjZCNAPH7Y4yYTIKfeE/DQ6COLlUixQxVMAUN1L5+GXML+px6fRigA5w==
|
||||
dependencies:
|
||||
"@budibase/types" "^2.0.23"
|
||||
"@budibase/types" "2.0.24-alpha.3"
|
||||
"@shopify/jest-koa-mocks" "5.0.1"
|
||||
"@techpass/passport-openidconnect" "0.3.2"
|
||||
aws-sdk "2.1030.0"
|
||||
|
@ -1180,13 +1180,13 @@
|
|||
svelte-flatpickr "^3.2.3"
|
||||
svelte-portal "^1.0.0"
|
||||
|
||||
"@budibase/pro@2.0.23":
|
||||
version "2.0.23"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.0.23.tgz#215759f07470d37b6571f54ad80d7fb38a2f902e"
|
||||
integrity sha512-RP7lS076VP3W+9AI9d/ylM6k1YBotGlqkBrshgzX/pl1e2O9eg0MmG2/dP6X+w4LzGv2ruC7lfwyf3MgWgYFyQ==
|
||||
"@budibase/pro@2.0.24-alpha.3":
|
||||
version "2.0.24-alpha.3"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.0.24-alpha.3.tgz#16605029663b07d0e3bcf0c08893878b542b6752"
|
||||
integrity sha512-gBdqJzvtEAMJkhB2YTAyVYKoCANA25F1wT/K7kQtTOy5LrjDmcXah7CZoetfZQGbsKxUYEAZwJzxLSXmIxvWmQ==
|
||||
dependencies:
|
||||
"@budibase/backend-core" "2.0.23"
|
||||
"@budibase/types" "2.0.23"
|
||||
"@budibase/backend-core" "2.0.24-alpha.3"
|
||||
"@budibase/types" "2.0.24-alpha.3"
|
||||
"@koa/router" "8.0.8"
|
||||
joi "17.6.0"
|
||||
node-fetch "^2.6.1"
|
||||
|
@ -1209,10 +1209,10 @@
|
|||
svelte-apexcharts "^1.0.2"
|
||||
svelte-flatpickr "^3.1.0"
|
||||
|
||||
"@budibase/types@2.0.23", "@budibase/types@^2.0.23":
|
||||
version "2.0.23"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.0.23.tgz#2467342b9c48c965319182ef4cbb7166c8bf1e43"
|
||||
integrity sha512-Oyq9tVwV+zl38III1or4Cr8VOKemMv3L/o95CBXdyZRDmmVfdzQgJ7AByUMOj9quEe6IjctdkOI4ssE8Fds9Dw==
|
||||
"@budibase/types@2.0.24-alpha.3":
|
||||
version "2.0.24-alpha.3"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.0.24-alpha.3.tgz#84eec5f991a2cfaf48d968b07a075b343f847289"
|
||||
integrity sha512-f9PhtqzmqPI76ITXttuvxsvqMUJtkrDYf/4MHlI2v5ssNL9r0C/hbQEXllff3L3JqViEHWxkKFmfvfnDTV8rRQ==
|
||||
|
||||
"@bull-board/api@3.7.0":
|
||||
version "3.7.0"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/string-templates",
|
||||
"version": "2.0.24-alpha.0",
|
||||
"version": "2.0.24-alpha.3",
|
||||
"description": "Handlebars wrapper for Budibase templating.",
|
||||
"main": "src/index.cjs",
|
||||
"module": "dist/bundle.mjs",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/types",
|
||||
"version": "2.0.24-alpha.0",
|
||||
"version": "2.0.24-alpha.3",
|
||||
"description": "Budibase types",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/worker",
|
||||
"email": "hi@budibase.com",
|
||||
"version": "2.0.24-alpha.0",
|
||||
"version": "2.0.24-alpha.3",
|
||||
"description": "Budibase background service",
|
||||
"main": "src/index.ts",
|
||||
"repository": {
|
||||
|
@ -36,10 +36,10 @@
|
|||
"author": "Budibase",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@budibase/backend-core": "2.0.24-alpha.0",
|
||||
"@budibase/pro": "2.0.23",
|
||||
"@budibase/string-templates": "2.0.24-alpha.0",
|
||||
"@budibase/types": "2.0.24-alpha.0",
|
||||
"@budibase/backend-core": "2.0.24-alpha.3",
|
||||
"@budibase/pro": "2.0.24-alpha.3",
|
||||
"@budibase/string-templates": "2.0.24-alpha.3",
|
||||
"@budibase/types": "2.0.24-alpha.3",
|
||||
"@koa/router": "8.0.8",
|
||||
"@sentry/node": "6.17.7",
|
||||
"@techpass/passport-openidconnect": "0.3.2",
|
||||
|
|
|
@ -291,12 +291,12 @@
|
|||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||
|
||||
"@budibase/backend-core@2.0.23":
|
||||
version "2.0.23"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.0.23.tgz#915d4ea2e78547795894b2aebd81b8a8c9b568db"
|
||||
integrity sha512-wovJF+kS/vVs+cR6lHieEEs99vmXsOt5qKjMAkkMUVXmWBWPYQaLppz+dxTV9xXd8Ht9yFhnkbOo7QJxFE8+fA==
|
||||
"@budibase/backend-core@2.0.24-alpha.3":
|
||||
version "2.0.24-alpha.3"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.0.24-alpha.3.tgz#49a92082d1ca6bed0eb82519a6c02d7d14ead751"
|
||||
integrity sha512-Zm/ddRDMzMuXCoEXZa0CzA/B1SnpQ+yjZCNAPH7Y4yYTIKfeE/DQ6COLlUixQxVMAUN1L5+GXML+px6fRigA5w==
|
||||
dependencies:
|
||||
"@budibase/types" "^2.0.23"
|
||||
"@budibase/types" "2.0.24-alpha.3"
|
||||
"@shopify/jest-koa-mocks" "5.0.1"
|
||||
"@techpass/passport-openidconnect" "0.3.2"
|
||||
aws-sdk "2.1030.0"
|
||||
|
@ -327,21 +327,21 @@
|
|||
uuid "8.3.2"
|
||||
zlib "1.0.5"
|
||||
|
||||
"@budibase/pro@2.0.23":
|
||||
version "2.0.23"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.0.23.tgz#215759f07470d37b6571f54ad80d7fb38a2f902e"
|
||||
integrity sha512-RP7lS076VP3W+9AI9d/ylM6k1YBotGlqkBrshgzX/pl1e2O9eg0MmG2/dP6X+w4LzGv2ruC7lfwyf3MgWgYFyQ==
|
||||
"@budibase/pro@2.0.24-alpha.3":
|
||||
version "2.0.24-alpha.3"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.0.24-alpha.3.tgz#16605029663b07d0e3bcf0c08893878b542b6752"
|
||||
integrity sha512-gBdqJzvtEAMJkhB2YTAyVYKoCANA25F1wT/K7kQtTOy5LrjDmcXah7CZoetfZQGbsKxUYEAZwJzxLSXmIxvWmQ==
|
||||
dependencies:
|
||||
"@budibase/backend-core" "2.0.23"
|
||||
"@budibase/types" "2.0.23"
|
||||
"@budibase/backend-core" "2.0.24-alpha.3"
|
||||
"@budibase/types" "2.0.24-alpha.3"
|
||||
"@koa/router" "8.0.8"
|
||||
joi "17.6.0"
|
||||
node-fetch "^2.6.1"
|
||||
|
||||
"@budibase/types@2.0.23", "@budibase/types@^2.0.23":
|
||||
version "2.0.23"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.0.23.tgz#2467342b9c48c965319182ef4cbb7166c8bf1e43"
|
||||
integrity sha512-Oyq9tVwV+zl38III1or4Cr8VOKemMv3L/o95CBXdyZRDmmVfdzQgJ7AByUMOj9quEe6IjctdkOI4ssE8Fds9Dw==
|
||||
"@budibase/types@2.0.24-alpha.3":
|
||||
version "2.0.24-alpha.3"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.0.24-alpha.3.tgz#84eec5f991a2cfaf48d968b07a075b343f847289"
|
||||
integrity sha512-f9PhtqzmqPI76ITXttuvxsvqMUJtkrDYf/4MHlI2v5ssNL9r0C/hbQEXllff3L3JqViEHWxkKFmfvfnDTV8rRQ==
|
||||
|
||||
"@cspotcode/source-map-consumer@0.8.0":
|
||||
version "0.8.0"
|
||||
|
|
Loading…
Reference in a new issue