Merge pull request #3403 from Budibase/cheeks-lab-day
Inline text editing + perf. enhancements + preview enhancements
This commit is contained in:
commit
5cf9aad280
|
@ -620,6 +620,9 @@ export const getFrontendStore = () => {
|
||||||
if (!name || !component) {
|
if (!name || !component) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (component[name] === value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
component[name] = value
|
component[name] = value
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
state.selectedComponentId = component._id
|
state.selectedComponentId = component._id
|
||||||
|
|
|
@ -32,15 +32,13 @@
|
||||||
.component("@budibase/standard-components/screenslot")
|
.component("@budibase/standard-components/screenslot")
|
||||||
.instanceName("Content Placeholder")
|
.instanceName("Content Placeholder")
|
||||||
.json()
|
.json()
|
||||||
|
|
||||||
// Messages that can be sent from the iframe preview to the builder
|
// Messages that can be sent from the iframe preview to the builder
|
||||||
// Budibase events are and initalisation events
|
// Budibase events are and initalisation events
|
||||||
const MessageTypes = {
|
const MessageTypes = {
|
||||||
IFRAME_LOADED: "iframe-loaded",
|
|
||||||
READY: "ready",
|
READY: "ready",
|
||||||
ERROR: "error",
|
ERROR: "error",
|
||||||
BUDIBASE: "type",
|
BUDIBASE: "type",
|
||||||
KEYDOWN: "keydown"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct iframe template
|
// Construct iframe template
|
||||||
|
@ -69,7 +67,7 @@
|
||||||
theme: $store.theme,
|
theme: $store.theme,
|
||||||
customTheme: $store.customTheme,
|
customTheme: $store.customTheme,
|
||||||
previewDevice: $store.previewDevice,
|
previewDevice: $store.previewDevice,
|
||||||
messagePassing: $store.clientFeatures.messagePassing
|
messagePassing: $store.clientFeatures.messagePassing,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Saving pages and screens to the DB causes them to have _revs.
|
// Saving pages and screens to the DB causes them to have _revs.
|
||||||
|
@ -111,7 +109,6 @@
|
||||||
loading = false
|
loading = false
|
||||||
error = event.error || "An unknown error occurred"
|
error = event.error || "An unknown error occurred"
|
||||||
},
|
},
|
||||||
[MessageTypes.KEYDOWN]: handleKeydownEvent
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageHandler = handlers[message.data.type] || handleBudibaseEvent
|
const messageHandler = handlers[message.data.type] || handleBudibaseEvent
|
||||||
|
@ -122,16 +119,25 @@
|
||||||
window.addEventListener("message", receiveMessage)
|
window.addEventListener("message", receiveMessage)
|
||||||
if (!$store.clientFeatures.messagePassing) {
|
if (!$store.clientFeatures.messagePassing) {
|
||||||
// Legacy - remove in later versions of BB
|
// Legacy - remove in later versions of BB
|
||||||
iframe.contentWindow.addEventListener("ready", () => {
|
iframe.contentWindow.addEventListener(
|
||||||
receiveMessage({ data: { type: MessageTypes.READY }})
|
"ready",
|
||||||
}, { once: true })
|
() => {
|
||||||
iframe.contentWindow.addEventListener("error", event => {
|
receiveMessage({ data: { type: MessageTypes.READY } })
|
||||||
receiveMessage({ data: { type: MessageTypes.ERROR, error: event.detail }})
|
},
|
||||||
}, { once: true })
|
{ once: true }
|
||||||
|
)
|
||||||
|
iframe.contentWindow.addEventListener(
|
||||||
|
"error",
|
||||||
|
event => {
|
||||||
|
receiveMessage({
|
||||||
|
data: { type: MessageTypes.ERROR, error: event.detail },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
)
|
||||||
// Add listener for events sent by client library in preview
|
// Add listener for events sent by client library in preview
|
||||||
iframe.contentWindow.addEventListener("bb-event", handleBudibaseEvent)
|
iframe.contentWindow.addEventListener("bb-event", handleBudibaseEvent)
|
||||||
iframe.contentWindow.addEventListener("keydown", handleKeydownEvent)
|
}
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Remove all iframe event listeners on component destroy
|
// Remove all iframe event listeners on component destroy
|
||||||
|
@ -140,14 +146,20 @@
|
||||||
window.removeEventListener("message", receiveMessage)
|
window.removeEventListener("message", receiveMessage)
|
||||||
if (!$store.clientFeatures.messagePassing) {
|
if (!$store.clientFeatures.messagePassing) {
|
||||||
// Legacy - remove in later versions of BB
|
// Legacy - remove in later versions of BB
|
||||||
iframe.contentWindow.removeEventListener("bb-event", handleBudibaseEvent)
|
iframe.contentWindow.removeEventListener(
|
||||||
iframe.contentWindow.removeEventListener("keydown", handleKeydownEvent)
|
"bb-event",
|
||||||
|
handleBudibaseEvent
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleBudibaseEvent = event => {
|
const handleBudibaseEvent = event => {
|
||||||
const { type, data } = event.data || event.detail
|
const { type, data } = event.data || event.detail
|
||||||
|
if (!type) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (type === "select-component" && data.id) {
|
if (type === "select-component" && data.id) {
|
||||||
store.actions.components.select({ _id: data.id })
|
store.actions.components.select({ _id: data.id })
|
||||||
} else if (type === "update-prop") {
|
} else if (type === "update-prop") {
|
||||||
|
@ -183,19 +195,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeydownEvent = event => {
|
|
||||||
const { key } = event.data || event
|
|
||||||
if (
|
|
||||||
(key === "Delete" || key === "Backspace") &&
|
|
||||||
selectedComponentId &&
|
|
||||||
["input", "textarea"].indexOf(
|
|
||||||
iframe.contentWindow.document.activeElement?.tagName.toLowerCase()
|
|
||||||
) === -1
|
|
||||||
) {
|
|
||||||
confirmDeleteComponent(selectedComponentId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmDeleteComponent = componentId => {
|
const confirmDeleteComponent = componentId => {
|
||||||
idToDelete = componentId
|
idToDelete = componentId
|
||||||
confirmDeleteDialog.show()
|
confirmDeleteDialog.show()
|
||||||
|
|
|
@ -84,7 +84,6 @@ export default `
|
||||||
if (window.loadBudibase) {
|
if (window.loadBudibase) {
|
||||||
window.loadBudibase()
|
window.loadBudibase()
|
||||||
document.documentElement.classList.add("loaded")
|
document.documentElement.classList.add("loaded")
|
||||||
window.parent.postMessage({ type: "iframe-loaded" })
|
|
||||||
} else {
|
} else {
|
||||||
throw "The client library couldn't be loaded"
|
throw "The client library couldn't be loaded"
|
||||||
}
|
}
|
||||||
|
@ -94,10 +93,6 @@ export default `
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("message", receiveMessage)
|
window.addEventListener("message", receiveMessage)
|
||||||
window.addEventListener("keydown", evt => {
|
|
||||||
window.parent.postMessage({ type: "keydown", key: event.key })
|
|
||||||
})
|
|
||||||
|
|
||||||
window.parent.postMessage({ type: "ready" })
|
window.parent.postMessage({ type: "ready" })
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -13,6 +13,12 @@
|
||||||
$: noChildrenAllowed = !component || !definition?.hasChildren
|
$: noChildrenAllowed = !component || !definition?.hasChildren
|
||||||
$: noPaste = !$store.componentToPaste
|
$: noPaste = !$store.componentToPaste
|
||||||
|
|
||||||
|
// "editable" has been repurposed for inline text editing.
|
||||||
|
// It remains here for legacy compatibility.
|
||||||
|
// Future components should define "static": true for indicate they should
|
||||||
|
// not show a context menu.
|
||||||
|
$: showMenu = definition?.editable !== false && definition?.static !== true
|
||||||
|
|
||||||
const moveUpComponent = () => {
|
const moveUpComponent = () => {
|
||||||
const asset = get(currentAsset)
|
const asset = get(currentAsset)
|
||||||
const parent = findComponentParent(asset.props, component._id)
|
const parent = findComponentParent(asset.props, component._id)
|
||||||
|
@ -69,7 +75,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if definition?.editable !== false}
|
{#if showMenu}
|
||||||
<ActionMenu>
|
<ActionMenu>
|
||||||
<div slot="control" class="icon">
|
<div slot="control" class="icon">
|
||||||
<Icon size="S" hoverable name="MoreSmallList" />
|
<Icon size="S" hoverable name="MoreSmallList" />
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
$: setupComplete =
|
$: setupComplete =
|
||||||
$datasources.list.find(x => (x._id = "bb_internal")).entities.length > 1 ||
|
$datasources.list.find(x => (x._id = "bb_internal"))?.entities.length > 1 ||
|
||||||
$datasources.list.length > 1
|
$datasources.list.length > 1
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
|
|
@ -240,13 +240,15 @@
|
||||||
"name": "Screenslot",
|
"name": "Screenslot",
|
||||||
"icon": "WebPage",
|
"icon": "WebPage",
|
||||||
"description": "Contains your app screens",
|
"description": "Contains your app screens",
|
||||||
"editable": false
|
"static": true
|
||||||
},
|
},
|
||||||
"button": {
|
"button": {
|
||||||
"name": "Button",
|
"name": "Button",
|
||||||
"description": "A basic html button that is ready for styling",
|
"description": "A basic html button that is ready for styling",
|
||||||
"icon": "Button",
|
"icon": "Button",
|
||||||
|
"editable": true,
|
||||||
"illegalChildren": ["section"],
|
"illegalChildren": ["section"],
|
||||||
|
"showSettingsBar": true,
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
@ -255,6 +257,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "select",
|
"type": "select",
|
||||||
|
"showInBar": true,
|
||||||
"label": "Variant",
|
"label": "Variant",
|
||||||
"key": "type",
|
"key": "type",
|
||||||
"options": [
|
"options": [
|
||||||
|
@ -283,6 +286,7 @@
|
||||||
{
|
{
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"label": "Size",
|
"label": "Size",
|
||||||
|
"showInBar": true,
|
||||||
"key": "size",
|
"key": "size",
|
||||||
"options": [
|
"options": [
|
||||||
{
|
{
|
||||||
|
@ -307,11 +311,18 @@
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Quiet",
|
"label": "Quiet",
|
||||||
"key": "quiet"
|
"key": "quiet",
|
||||||
|
"showInBar": true,
|
||||||
|
"barIcon": "VisibilityOff",
|
||||||
|
"barTitle": "Quiet variant",
|
||||||
|
"barSeparator": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Disabled",
|
"label": "Disabled",
|
||||||
|
"showInBar": true,
|
||||||
|
"barIcon": "NoEdit",
|
||||||
|
"barTitle": "Disable button",
|
||||||
"key": "disabled"
|
"key": "disabled"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -590,6 +601,7 @@
|
||||||
"icon": "TextParagraph",
|
"icon": "TextParagraph",
|
||||||
"illegalChildren": ["section"],
|
"illegalChildren": ["section"],
|
||||||
"showSettingsBar": true,
|
"showSettingsBar": true,
|
||||||
|
"editable": true,
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
@ -696,6 +708,7 @@
|
||||||
"description": "A component for displaying heading text",
|
"description": "A component for displaying heading text",
|
||||||
"illegalChildren": ["section"],
|
"illegalChildren": ["section"],
|
||||||
"showSettingsBar": true,
|
"showSettingsBar": true,
|
||||||
|
"editable": true,
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
@ -940,6 +953,7 @@
|
||||||
"description": "A basic link component for internal and external links",
|
"description": "A basic link component for internal and external links",
|
||||||
"icon": "Link",
|
"icon": "Link",
|
||||||
"showSettingsBar": true,
|
"showSettingsBar": true,
|
||||||
|
"editable": true,
|
||||||
"illegalChildren": ["section"],
|
"illegalChildren": ["section"],
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
|
@ -1831,6 +1845,7 @@
|
||||||
"icon": "Text",
|
"icon": "Text",
|
||||||
"illegalChildren": ["section"],
|
"illegalChildren": ["section"],
|
||||||
"styles": ["size"],
|
"styles": ["size"],
|
||||||
|
"editable": true,
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "field/string",
|
"type": "field/string",
|
||||||
|
@ -1869,6 +1884,7 @@
|
||||||
"name": "Number Field",
|
"name": "Number Field",
|
||||||
"icon": "123",
|
"icon": "123",
|
||||||
"styles": ["size"],
|
"styles": ["size"],
|
||||||
|
"editable": true,
|
||||||
"illegalChildren": ["section"],
|
"illegalChildren": ["section"],
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
|
@ -1908,6 +1924,7 @@
|
||||||
"name": "Password Field",
|
"name": "Password Field",
|
||||||
"icon": "LockClosed",
|
"icon": "LockClosed",
|
||||||
"styles": ["size"],
|
"styles": ["size"],
|
||||||
|
"editable": true,
|
||||||
"illegalChildren": ["section"],
|
"illegalChildren": ["section"],
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
|
@ -1947,6 +1964,7 @@
|
||||||
"name": "Options Picker",
|
"name": "Options Picker",
|
||||||
"icon": "ViewList",
|
"icon": "ViewList",
|
||||||
"styles": ["size"],
|
"styles": ["size"],
|
||||||
|
"editable": true,
|
||||||
"illegalChildren": ["section"],
|
"illegalChildren": ["section"],
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
|
@ -2070,6 +2088,7 @@
|
||||||
"name": "Multi-select Picker",
|
"name": "Multi-select Picker",
|
||||||
"icon": "ViewList",
|
"icon": "ViewList",
|
||||||
"styles": ["size"],
|
"styles": ["size"],
|
||||||
|
"editable": true,
|
||||||
"illegalChildren": ["section"],
|
"illegalChildren": ["section"],
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
|
@ -2171,6 +2190,7 @@
|
||||||
"booleanfield": {
|
"booleanfield": {
|
||||||
"name": "Checkbox",
|
"name": "Checkbox",
|
||||||
"icon": "Checkmark",
|
"icon": "Checkmark",
|
||||||
|
"editable": true,
|
||||||
"illegalChildren": ["section"],
|
"illegalChildren": ["section"],
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
|
@ -2234,6 +2254,7 @@
|
||||||
"name": "Rich Text",
|
"name": "Rich Text",
|
||||||
"icon": "TextParagraph",
|
"icon": "TextParagraph",
|
||||||
"styles": ["size"],
|
"styles": ["size"],
|
||||||
|
"editable": true,
|
||||||
"illegalChildren": ["section"],
|
"illegalChildren": ["section"],
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
|
@ -2274,6 +2295,7 @@
|
||||||
"name": "Date Picker",
|
"name": "Date Picker",
|
||||||
"icon": "DateInput",
|
"icon": "DateInput",
|
||||||
"styles": ["size"],
|
"styles": ["size"],
|
||||||
|
"editable": true,
|
||||||
"illegalChildren": ["section"],
|
"illegalChildren": ["section"],
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
|
@ -2319,6 +2341,7 @@
|
||||||
"name": "Attachment",
|
"name": "Attachment",
|
||||||
"icon": "Attach",
|
"icon": "Attach",
|
||||||
"styles": ["size"],
|
"styles": ["size"],
|
||||||
|
"editable": true,
|
||||||
"illegalChildren": ["section"],
|
"illegalChildren": ["section"],
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
|
@ -2348,6 +2371,7 @@
|
||||||
"name": "Relationship Picker",
|
"name": "Relationship Picker",
|
||||||
"icon": "TaskList",
|
"icon": "TaskList",
|
||||||
"styles": ["size"],
|
"styles": ["size"],
|
||||||
|
"editable": true,
|
||||||
"illegalChildren": ["section"],
|
"illegalChildren": ["section"],
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { writable } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
import { setContext, onMount } from "svelte"
|
import { setContext, onMount } from "svelte"
|
||||||
import { Layout, Heading, Body } from "@budibase/bbui"
|
import { Layout, Heading, Body } from "@budibase/bbui"
|
||||||
import Component from "./Component.svelte"
|
import Component from "./Component.svelte"
|
||||||
|
@ -25,6 +25,7 @@
|
||||||
import CustomThemeWrapper from "./CustomThemeWrapper.svelte"
|
import CustomThemeWrapper from "./CustomThemeWrapper.svelte"
|
||||||
import DNDHandler from "components/preview/DNDHandler.svelte"
|
import DNDHandler from "components/preview/DNDHandler.svelte"
|
||||||
import ErrorSVG from "builder/assets/error.svg"
|
import ErrorSVG from "builder/assets/error.svg"
|
||||||
|
import KeyboardManager from "components/preview/KeyboardManager.svelte"
|
||||||
|
|
||||||
// Provide contexts
|
// Provide contexts
|
||||||
setContext("sdk", SDK)
|
setContext("sdk", SDK)
|
||||||
|
@ -39,7 +40,7 @@
|
||||||
await initialise()
|
await initialise()
|
||||||
await authStore.actions.fetchUser()
|
await authStore.actions.fetchUser()
|
||||||
dataLoaded = true
|
dataLoaded = true
|
||||||
if ($builderStore.inBuilder) {
|
if (get(builderStore).inBuilder) {
|
||||||
builderStore.actions.notifyLoaded()
|
builderStore.actions.notifyLoaded()
|
||||||
} else {
|
} else {
|
||||||
builderStore.actions.pingEndUser()
|
builderStore.actions.pingEndUser()
|
||||||
|
@ -143,6 +144,7 @@
|
||||||
</UserBindingsProvider>
|
</UserBindingsProvider>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
<KeyboardManager />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getContext, setContext } from "svelte"
|
import { getContext, setContext } from "svelte"
|
||||||
import { writable, get } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
import * as AppComponents from "components/app"
|
import * as AppComponents from "components/app"
|
||||||
import Router from "./Router.svelte"
|
import Router from "./Router.svelte"
|
||||||
import { enrichProps, propsAreSame } from "utils/componentProps"
|
import { enrichProps, propsAreSame } from "utils/componentProps"
|
||||||
|
@ -19,15 +19,22 @@
|
||||||
export let isScreen = false
|
export let isScreen = false
|
||||||
export let isBlock = false
|
export let isBlock = false
|
||||||
|
|
||||||
|
// Component settings are the un-enriched settings for this component that
|
||||||
|
// need to be enriched at this level.
|
||||||
|
// Nested settings are the un-enriched block settings that are to be passed on
|
||||||
|
// and enriched at a deeper level.
|
||||||
|
let componentSettings
|
||||||
|
let nestedSettings
|
||||||
|
|
||||||
// The enriched component settings
|
// The enriched component settings
|
||||||
let enrichedSettings
|
let enrichedSettings
|
||||||
|
|
||||||
// Any prop overrides that need to be applied due to conditional UI
|
// Any setting overrides that need to be applied due to conditional UI
|
||||||
let conditionalSettings
|
let conditionalSettings
|
||||||
|
|
||||||
// Settings are hashed when inside the builder preview and used as a key,
|
// Resultant cached settings which will be passed to the component instance.
|
||||||
// so that components fully remount whenever any settings change
|
// These are a combination of the enriched, nested and conditional settings.
|
||||||
let hash = 0
|
let cachedSettings
|
||||||
|
|
||||||
// Latest timestamp that we started a props update.
|
// Latest timestamp that we started a props update.
|
||||||
// Due to enrichment now being async, we need to avoid overwriting newer
|
// Due to enrichment now being async, we need to avoid overwriting newer
|
||||||
|
@ -63,6 +70,7 @@
|
||||||
$: selected =
|
$: selected =
|
||||||
$builderStore.inBuilder && $builderStore.selectedComponentId === id
|
$builderStore.inBuilder && $builderStore.selectedComponentId === id
|
||||||
$: inSelectedPath = $builderStore.selectedComponentPath?.includes(id)
|
$: inSelectedPath = $builderStore.selectedComponentPath?.includes(id)
|
||||||
|
$: inDragPath = inSelectedPath && $builderStore.editMode
|
||||||
|
|
||||||
// Interactive components can be selected, dragged and highlighted inside
|
// Interactive components can be selected, dragged and highlighted inside
|
||||||
// the builder preview
|
// the builder preview
|
||||||
|
@ -70,7 +78,9 @@
|
||||||
$builderStore.inBuilder &&
|
$builderStore.inBuilder &&
|
||||||
($builderStore.previewType === "layout" || insideScreenslot) &&
|
($builderStore.previewType === "layout" || insideScreenslot) &&
|
||||||
!isBlock
|
!isBlock
|
||||||
$: draggable = interactive && !isLayout && !isScreen
|
$: editable = definition?.editable
|
||||||
|
$: editing = editable && selected && $builderStore.editMode
|
||||||
|
$: draggable = !inDragPath && interactive && !isLayout && !isScreen
|
||||||
$: droppable = interactive && !isLayout && !isScreen
|
$: droppable = interactive && !isLayout && !isScreen
|
||||||
|
|
||||||
// Empty components are those which accept children but do not have any.
|
// Empty components are those which accept children but do not have any.
|
||||||
|
@ -79,44 +89,39 @@
|
||||||
$: empty = interactive && !children.length && definition?.hasChildren
|
$: empty = interactive && !children.length && definition?.hasChildren
|
||||||
$: emptyState = empty && definition?.showEmptyState !== false
|
$: emptyState = empty && definition?.showEmptyState !== false
|
||||||
|
|
||||||
// Raw props are all props excluding internal props and children
|
// Raw settings are all settings excluding internal props and children
|
||||||
$: rawSettings = getRawSettings(instance)
|
$: rawSettings = getRawSettings(instance)
|
||||||
$: instanceKey = hashString(JSON.stringify(rawSettings))
|
$: instanceKey = hashString(JSON.stringify(rawSettings))
|
||||||
|
|
||||||
// Component settings are those which are intended for this component and
|
// Update and enrich component settings
|
||||||
// which need to be enriched
|
$: updateSettings(rawSettings, instanceKey, settingsDefinition, $context)
|
||||||
$: componentSettings = getComponentSettings(rawSettings, settingsDefinition)
|
|
||||||
$: enrichComponentSettings(rawSettings, instanceKey, $context)
|
|
||||||
|
|
||||||
// Nested settings are those which are intended for child components inside
|
|
||||||
// blocks and which should not be enriched at this level
|
|
||||||
$: nestedSettings = getNestedSettings(rawSettings, settingsDefinition)
|
|
||||||
|
|
||||||
// Evaluate conditional UI settings and store any component setting changes
|
// Evaluate conditional UI settings and store any component setting changes
|
||||||
// which need to be made
|
// which need to be made
|
||||||
$: evaluateConditions(enrichedSettings?._conditions)
|
$: evaluateConditions(enrichedSettings?._conditions)
|
||||||
|
|
||||||
// Build up the final settings object to be passed to the component
|
// Build up the final settings object to be passed to the component
|
||||||
$: settings = {
|
$: cacheSettings(enrichedSettings, nestedSettings, conditionalSettings)
|
||||||
...enrichedSettings,
|
|
||||||
...nestedSettings,
|
|
||||||
...conditionalSettings,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render key is used when in the builder preview to fully remount
|
|
||||||
// components when settings are changed
|
|
||||||
$: renderKey = `${hash}-${emptyState}`
|
|
||||||
|
|
||||||
// Update component context
|
// Update component context
|
||||||
$: componentStore.set({
|
$: componentStore.set({
|
||||||
id,
|
id,
|
||||||
children: children.length,
|
children: children.length,
|
||||||
styles: { ...instance._styles, id, empty: emptyState, interactive },
|
styles: {
|
||||||
|
...instance._styles,
|
||||||
|
id,
|
||||||
|
empty: emptyState,
|
||||||
|
interactive,
|
||||||
|
draggable,
|
||||||
|
editable,
|
||||||
|
},
|
||||||
empty: emptyState,
|
empty: emptyState,
|
||||||
selected,
|
selected,
|
||||||
name,
|
name,
|
||||||
|
editing,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Extracts all settings from the component instance
|
||||||
const getRawSettings = instance => {
|
const getRawSettings = instance => {
|
||||||
let validSettings = {}
|
let validSettings = {}
|
||||||
Object.entries(instance)
|
Object.entries(instance)
|
||||||
|
@ -137,12 +142,14 @@
|
||||||
return AppComponents[name]
|
return AppComponents[name]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gets this component's definition from the manifest
|
||||||
const getComponentDefinition = component => {
|
const getComponentDefinition = component => {
|
||||||
const prefix = "@budibase/standard-components/"
|
const prefix = "@budibase/standard-components/"
|
||||||
const type = component?.replace(prefix, "")
|
const type = component?.replace(prefix, "")
|
||||||
return type ? Manifest[type] : null
|
return type ? Manifest[type] : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gets the definition of this component's settings from the manifest
|
||||||
const getSettingsDefinition = definition => {
|
const getSettingsDefinition = definition => {
|
||||||
if (!definition) {
|
if (!definition) {
|
||||||
return []
|
return []
|
||||||
|
@ -162,35 +169,50 @@
|
||||||
return settings
|
return settings
|
||||||
}
|
}
|
||||||
|
|
||||||
const getComponentSettings = (rawSettings, settingsDefinition) => {
|
// Updates and enriches component settings when raw settings change
|
||||||
let clone = { ...rawSettings }
|
const updateSettings = (settings, key, settingsDefinition, context) => {
|
||||||
settingsDefinition?.forEach(setting => {
|
const instanceChanged = key !== lastInstanceKey
|
||||||
if (setting.nested) {
|
|
||||||
delete clone[setting.key]
|
// Derive component and nested settings if the instance changed
|
||||||
}
|
if (instanceChanged) {
|
||||||
})
|
splitRawSettings(settings, settingsDefinition)
|
||||||
return clone
|
}
|
||||||
|
|
||||||
|
// Enrich component settings
|
||||||
|
enrichComponentSettings(componentSettings, context, instanceChanged)
|
||||||
|
|
||||||
|
// Update instance key
|
||||||
|
if (instanceChanged) {
|
||||||
|
lastInstanceKey = key
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getNestedSettings = (rawSettings, settingsDefinition) => {
|
// Splits the raw settings into those destined for the component itself
|
||||||
let clone = { ...rawSettings }
|
// and nexted settings for child components inside blocks
|
||||||
|
const splitRawSettings = (rawSettings, settingsDefinition) => {
|
||||||
|
let newComponentSettings = { ...rawSettings }
|
||||||
|
let newNestedSettings = { ...rawSettings }
|
||||||
settingsDefinition?.forEach(setting => {
|
settingsDefinition?.forEach(setting => {
|
||||||
if (!setting.nested) {
|
if (setting.nested) {
|
||||||
delete clone[setting.key]
|
delete newComponentSettings[setting.key]
|
||||||
|
} else {
|
||||||
|
delete newNestedSettings[setting.key]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return clone
|
componentSettings = newComponentSettings
|
||||||
|
nestedSettings = newNestedSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enriches any string component props using handlebars
|
// Enriches any string component props using handlebars
|
||||||
const enrichComponentSettings = (rawSettings, instanceKey, context) => {
|
const enrichComponentSettings = (rawSettings, context, instanceChanged) => {
|
||||||
const instanceSame = instanceKey === lastInstanceKey
|
const contextChanged = context.key !== lastContextKey
|
||||||
const contextSame = context.key === lastContextKey
|
|
||||||
|
|
||||||
if (instanceSame && contextSame) {
|
// Skip enrichment if the context and instance are unchanged
|
||||||
return
|
if (!contextChanged) {
|
||||||
|
if (!instanceChanged) {
|
||||||
|
return
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
lastInstanceKey = instanceKey
|
|
||||||
lastContextKey = context.key
|
lastContextKey = context.key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,31 +228,11 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the component props.
|
enrichedSettings = newEnrichedSettings
|
||||||
// Most props are deeply compared so that svelte will only trigger reactive
|
|
||||||
// statements on props that have actually changed.
|
|
||||||
if (!newEnrichedSettings) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let propsChanged = false
|
|
||||||
if (!enrichedSettings) {
|
|
||||||
enrichedSettings = {}
|
|
||||||
propsChanged = true
|
|
||||||
}
|
|
||||||
Object.keys(newEnrichedSettings).forEach(key => {
|
|
||||||
if (!propsAreSame(newEnrichedSettings[key], enrichedSettings[key])) {
|
|
||||||
propsChanged = true
|
|
||||||
enrichedSettings[key] = newEnrichedSettings[key]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update the hash if we're in the builder so we can fully remount this
|
|
||||||
// component
|
|
||||||
if (get(builderStore).inBuilder && propsChanged) {
|
|
||||||
hash = hashString(JSON.stringify(enrichedSettings))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Evaluates the list of conditional UI conditions and determines any setting
|
||||||
|
// or visibility changes required
|
||||||
const evaluateConditions = conditions => {
|
const evaluateConditions = conditions => {
|
||||||
if (!conditions?.length) {
|
if (!conditions?.length) {
|
||||||
return
|
return
|
||||||
|
@ -250,36 +252,51 @@
|
||||||
conditionalSettings = result.settingUpdates
|
conditionalSettings = result.settingUpdates
|
||||||
visible = nextVisible
|
visible = nextVisible
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Combines and caches all settings which will be passed to the component
|
||||||
|
// instance. Settings are aggressively memoized to avoid triggering svelte
|
||||||
|
// reactive statements as much as possible.
|
||||||
|
const cacheSettings = (enriched, nested, conditional) => {
|
||||||
|
const allSettings = { ...enriched, ...nested, ...conditional }
|
||||||
|
if (!cachedSettings) {
|
||||||
|
cachedSettings = allSettings
|
||||||
|
} else {
|
||||||
|
Object.keys(allSettings).forEach(key => {
|
||||||
|
if (!propsAreSame(allSettings[key], cachedSettings[key])) {
|
||||||
|
cachedSettings[key] = allSettings[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#key renderKey}
|
{#if constructor && cachedSettings && (visible || inSelectedPath)}
|
||||||
{#if constructor && settings && (visible || inSelectedPath)}
|
<!-- The ID is used as a class because getElementsByClassName is O(1) -->
|
||||||
<!-- The ID is used as a class because getElementsByClassName is O(1) -->
|
<!-- and the performance matters for the selection indicators -->
|
||||||
<!-- and the performance matters for the selection indicators -->
|
<div
|
||||||
<div
|
class={`component ${id}`}
|
||||||
class={`component ${id}`}
|
class:draggable
|
||||||
class:draggable
|
class:droppable
|
||||||
class:droppable
|
class:empty
|
||||||
class:empty
|
class:interactive
|
||||||
class:interactive
|
class:editing
|
||||||
class:block={isBlock}
|
class:block={isBlock}
|
||||||
data-id={id}
|
data-id={id}
|
||||||
data-name={name}
|
data-name={name}
|
||||||
>
|
>
|
||||||
<svelte:component this={constructor} {...settings}>
|
<svelte:component this={constructor} {...cachedSettings}>
|
||||||
{#if children.length}
|
{#if children.length}
|
||||||
{#each children as child (child._id)}
|
{#each children as child (child._id)}
|
||||||
<svelte:self instance={child} />
|
<svelte:self instance={child} />
|
||||||
{/each}
|
{/each}
|
||||||
{:else if emptyState}
|
{:else if emptyState}
|
||||||
<Placeholder />
|
<Placeholder />
|
||||||
{:else if isBlock}
|
{:else if isBlock}
|
||||||
<slot />
|
<slot />
|
||||||
{/if}
|
{/if}
|
||||||
</svelte:component>
|
</svelte:component>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/key}
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.component {
|
.component {
|
||||||
|
@ -291,4 +308,7 @@
|
||||||
.draggable :global(*:hover) {
|
.draggable :global(*:hover) {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
}
|
}
|
||||||
|
.editing :global(*:hover) {
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -79,4 +79,9 @@
|
||||||
scrollbar-color: var(--spectrum-global-color-gray-400)
|
scrollbar-color: var(--spectrum-global-color-gray-400)
|
||||||
var(--spectrum-alias-background-color-default);
|
var(--spectrum-alias-background-color-default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Remove border when editing contenteditable components */
|
||||||
|
:global(*[contenteditable="true"]:focus) {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import "@spectrum-css/button/dist/index-vars.css"
|
import "@spectrum-css/button/dist/index-vars.css"
|
||||||
|
|
||||||
const { styleable } = getContext("sdk")
|
const { styleable, builderStore } = getContext("sdk")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
|
@ -11,16 +11,35 @@
|
||||||
export let size = "M"
|
export let size = "M"
|
||||||
export let type = "primary"
|
export let type = "primary"
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
|
|
||||||
|
let node
|
||||||
|
|
||||||
|
$: $component.editing && node?.focus()
|
||||||
|
$: componentText = getComponentText(text, $builderStore, $component)
|
||||||
|
|
||||||
|
const getComponentText = (text, builderState, componentState) => {
|
||||||
|
if (!builderState.inBuilder || componentState.editing) {
|
||||||
|
return text || " "
|
||||||
|
}
|
||||||
|
return text || componentState.name || "Placeholder text"
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateText = e => {
|
||||||
|
builderStore.actions.updateProp("text", e.target.textContent)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class={`spectrum-Button spectrum-Button--size${size} spectrum-Button--${type}`}
|
class={`spectrum-Button spectrum-Button--size${size} spectrum-Button--${type}`}
|
||||||
class:spectrum-Button--quiet={quiet}
|
class:spectrum-Button--quiet={quiet}
|
||||||
disabled={disabled || false}
|
{disabled}
|
||||||
use:styleable={$component.styles}
|
use:styleable={$component.styles}
|
||||||
on:click={onClick}
|
on:click={onClick}
|
||||||
|
contenteditable={$component.editing}
|
||||||
|
on:blur={$component.editing ? updateText : null}
|
||||||
|
bind:this={node}
|
||||||
>
|
>
|
||||||
{text || ""}
|
{componentText}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -34,12 +34,18 @@
|
||||||
let bookmarks = [null]
|
let bookmarks = [null]
|
||||||
let pageNumber = 0
|
let pageNumber = 0
|
||||||
let query = null
|
let query = null
|
||||||
|
let queryExtensions = {}
|
||||||
|
|
||||||
// Sorting can be overridden at run time, so we can't use the prop directly
|
// Sorting can be overridden at run time, so we can't use the prop directly
|
||||||
let currentSortColumn = sortColumn
|
let currentSortColumn = sortColumn
|
||||||
let currentSortOrder = sortOrder
|
let currentSortOrder = sortOrder
|
||||||
|
|
||||||
$: query = buildLuceneQuery(filter)
|
// Reset the current sort state to props if props change
|
||||||
|
$: currentSortColumn = sortColumn
|
||||||
|
$: currentSortOrder = sortOrder
|
||||||
|
|
||||||
|
$: defaultQuery = buildLuceneQuery(filter)
|
||||||
|
$: extendQuery(defaultQuery, queryExtensions)
|
||||||
$: internalTable = dataSource?.type === "table"
|
$: internalTable = dataSource?.type === "table"
|
||||||
$: nestedProvider = dataSource?.type === "provider"
|
$: nestedProvider = dataSource?.type === "provider"
|
||||||
$: hasNextPage = bookmarks[pageNumber + 1] != null
|
$: hasNextPage = bookmarks[pageNumber + 1] != null
|
||||||
|
@ -91,8 +97,12 @@
|
||||||
metadata: { dataSource },
|
metadata: { dataSource },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: ActionTypes.SetDataProviderQuery,
|
type: ActionTypes.AddDataProviderQueryExtension,
|
||||||
callback: newQuery => (query = newQuery),
|
callback: addQueryExtension,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ActionTypes.RemoveDataProviderQueryExtension,
|
||||||
|
callback: removeQueryExtension,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: ActionTypes.SetDataProviderSorting,
|
type: ActionTypes.SetDataProviderSorting,
|
||||||
|
@ -264,6 +274,38 @@
|
||||||
pageNumber--
|
pageNumber--
|
||||||
allRows = res.rows
|
allRows = res.rows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addQueryExtension = (key, operator, field, value) => {
|
||||||
|
if (!key || !operator || !field) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const extension = { operator, field, value }
|
||||||
|
queryExtensions = { ...queryExtensions, [key]: extension }
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeQueryExtension = key => {
|
||||||
|
if (!key) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const newQueryExtensions = { ...queryExtensions }
|
||||||
|
delete newQueryExtensions[key]
|
||||||
|
queryExtensions = newQueryExtensions
|
||||||
|
}
|
||||||
|
|
||||||
|
const extendQuery = (defaultQuery, extensions) => {
|
||||||
|
const extensionValues = Object.values(extensions || {})
|
||||||
|
let extendedQuery = { ...defaultQuery }
|
||||||
|
extensionValues.forEach(({ operator, field, value }) => {
|
||||||
|
extendedQuery[operator] = {
|
||||||
|
...extendedQuery[operator],
|
||||||
|
[field]: value,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (JSON.stringify(query) !== JSON.stringify(extendedQuery)) {
|
||||||
|
query = extendedQuery
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div use:styleable={$component.styles} class="container">
|
<div use:styleable={$component.styles} class="container">
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import utc from "dayjs/plugin/utc"
|
import utc from "dayjs/plugin/utc"
|
||||||
import { onMount } from "svelte"
|
import { onDestroy } from "svelte"
|
||||||
|
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
|
|
||||||
|
@ -14,7 +14,14 @@
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const { styleable, ActionTypes, getAction } = getContext("sdk")
|
const { styleable, ActionTypes, getAction } = getContext("sdk")
|
||||||
|
|
||||||
const setQuery = getAction(dataProvider?.id, ActionTypes.SetDataProviderQuery)
|
$: addExtension = getAction(
|
||||||
|
dataProvider?.id,
|
||||||
|
ActionTypes.AddDataProviderQueryExtension
|
||||||
|
)
|
||||||
|
$: removeExtension = getAction(
|
||||||
|
dataProvider?.id,
|
||||||
|
ActionTypes.RemoveDataProviderQueryExtension
|
||||||
|
)
|
||||||
const options = [
|
const options = [
|
||||||
"Last 1 day",
|
"Last 1 day",
|
||||||
"Last 7 days",
|
"Last 7 days",
|
||||||
|
@ -25,44 +32,30 @@
|
||||||
]
|
]
|
||||||
let value = options.includes(defaultValue) ? defaultValue : "Last 30 days"
|
let value = options.includes(defaultValue) ? defaultValue : "Last 30 days"
|
||||||
|
|
||||||
const updateDateRange = option => {
|
$: queryExtension = getQueryExtension(value)
|
||||||
const query = dataProvider?.state?.query
|
$: addExtension?.($component.id, "range", field, queryExtension)
|
||||||
if (!query || !setQuery) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
value = option
|
const getQueryExtension = value => {
|
||||||
let low = dayjs.utc().subtract(1, "year")
|
let low = dayjs.utc().subtract(1, "year")
|
||||||
let high = dayjs.utc().add(1, "day")
|
let high = dayjs.utc().add(1, "day")
|
||||||
|
|
||||||
if (option === "Last 1 day") {
|
if (value === "Last 1 day") {
|
||||||
low = dayjs.utc().subtract(1, "day")
|
low = dayjs.utc().subtract(1, "day")
|
||||||
} else if (option === "Last 7 days") {
|
} else if (value === "Last 7 days") {
|
||||||
low = dayjs.utc().subtract(7, "days")
|
low = dayjs.utc().subtract(7, "days")
|
||||||
} else if (option === "Last 30 days") {
|
} else if (value === "Last 30 days") {
|
||||||
low = dayjs.utc().subtract(30, "days")
|
low = dayjs.utc().subtract(30, "days")
|
||||||
} else if (option === "Last 3 months") {
|
} else if (value === "Last 3 months") {
|
||||||
low = dayjs.utc().subtract(3, "months")
|
low = dayjs.utc().subtract(3, "months")
|
||||||
} else if (option === "Last 6 months") {
|
} else if (value === "Last 6 months") {
|
||||||
low = dayjs.utc().subtract(6, "months")
|
low = dayjs.utc().subtract(6, "months")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update data provider query with the new filter
|
return { low: low.format(), high: high.format() }
|
||||||
setQuery({
|
|
||||||
...query,
|
|
||||||
range: {
|
|
||||||
...query.range,
|
|
||||||
[field]: {
|
|
||||||
high: high.format(),
|
|
||||||
low: low.format(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the range on mount to the initial value
|
onDestroy(() => {
|
||||||
onMount(() => {
|
removeExtension?.($component.id)
|
||||||
updateDateRange(value)
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -71,6 +64,6 @@
|
||||||
placeholder={null}
|
placeholder={null}
|
||||||
{options}
|
{options}
|
||||||
{value}
|
{value}
|
||||||
on:change={e => updateDateRange(e.detail)}
|
on:change={e => (value = e.detail)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,10 +13,11 @@
|
||||||
export let underline
|
export let underline
|
||||||
export let size
|
export let size
|
||||||
|
|
||||||
$: placeholder = $builderStore.inBuilder && !text
|
let node
|
||||||
$: componentText = $builderStore.inBuilder
|
|
||||||
? text || $component.name || "Placeholder text"
|
$: $component.editing && node?.focus()
|
||||||
: text || ""
|
$: placeholder = $builderStore.inBuilder && !text && !$component.editing
|
||||||
|
$: componentText = getComponentText(text, $builderStore, $component)
|
||||||
$: sizeClass = `spectrum-Heading--size${size || "M"}`
|
$: sizeClass = `spectrum-Heading--size${size || "M"}`
|
||||||
$: alignClass = `align--${align || "left"}`
|
$: alignClass = `align--${align || "left"}`
|
||||||
|
|
||||||
|
@ -24,6 +25,13 @@
|
||||||
// overrides the color when it's passed as inline style.
|
// overrides the color when it's passed as inline style.
|
||||||
$: styles = enrichStyles($component.styles, color)
|
$: styles = enrichStyles($component.styles, color)
|
||||||
|
|
||||||
|
const getComponentText = (text, builderState, componentState) => {
|
||||||
|
if (!builderState.inBuilder || componentState.editing) {
|
||||||
|
return text || ""
|
||||||
|
}
|
||||||
|
return text || componentState.name || "Placeholder text"
|
||||||
|
}
|
||||||
|
|
||||||
const enrichStyles = (styles, color) => {
|
const enrichStyles = (styles, color) => {
|
||||||
if (!color) {
|
if (!color) {
|
||||||
return styles
|
return styles
|
||||||
|
@ -36,15 +44,24 @@
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert contenteditable HTML to text and save
|
||||||
|
const updateText = e => {
|
||||||
|
const sanitized = e.target.innerHTML.replace(/<br>/gi, "\n")
|
||||||
|
builderStore.actions.updateProp("text", sanitized)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1
|
<h1
|
||||||
|
bind:this={node}
|
||||||
|
contenteditable={$component.editing}
|
||||||
use:styleable={styles}
|
use:styleable={styles}
|
||||||
class:placeholder
|
class:placeholder
|
||||||
class:bold
|
class:bold
|
||||||
class:italic
|
class:italic
|
||||||
class:underline
|
class:underline
|
||||||
class="spectrum-Heading {sizeClass} {alignClass}"
|
class="spectrum-Heading {sizeClass} {alignClass}"
|
||||||
|
on:blur={$component.editing ? updateText : null}
|
||||||
>
|
>
|
||||||
{componentText}
|
{componentText}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
|
@ -14,19 +14,25 @@
|
||||||
export let underline
|
export let underline
|
||||||
export let size
|
export let size
|
||||||
|
|
||||||
$: external = url && typeof url === "string" && !url.startsWith("/")
|
let node
|
||||||
|
|
||||||
|
$: $component.editing && node?.focus()
|
||||||
|
$: externalLink = url && typeof url === "string" && !url.startsWith("/")
|
||||||
$: target = openInNewTab ? "_blank" : "_self"
|
$: target = openInNewTab ? "_blank" : "_self"
|
||||||
$: placeholder = $builderStore.inBuilder && !text
|
$: placeholder = $builderStore.inBuilder && !text
|
||||||
$: componentText = $builderStore.inBuilder
|
$: componentText = getComponentText(text, $builderStore, $component)
|
||||||
? text || "Placeholder link"
|
|
||||||
: text || ""
|
|
||||||
|
|
||||||
// Add color styles to main styles object, otherwise the styleable helper
|
|
||||||
// overrides the color when it's passed as inline style.
|
|
||||||
// Add color styles to main styles object, otherwise the styleable helper
|
// Add color styles to main styles object, otherwise the styleable helper
|
||||||
// overrides the color when it's passed as inline style.
|
// overrides the color when it's passed as inline style.
|
||||||
$: styles = enrichStyles($component.styles, color)
|
$: styles = enrichStyles($component.styles, color)
|
||||||
|
|
||||||
|
const getComponentText = (text, builderState, componentState) => {
|
||||||
|
if (!builderState.inBuilder || componentState.editing) {
|
||||||
|
return text || ""
|
||||||
|
}
|
||||||
|
return text || componentState.name || "Placeholder text"
|
||||||
|
}
|
||||||
|
|
||||||
const enrichStyles = (styles, color) => {
|
const enrichStyles = (styles, color) => {
|
||||||
if (!color) {
|
if (!color) {
|
||||||
return styles
|
return styles
|
||||||
|
@ -39,10 +45,27 @@
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateText = e => {
|
||||||
|
builderStore.actions.updateProp("text", e.target.textContent)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $builderStore.inBuilder || componentText}
|
{#if $component.editing}
|
||||||
{#if external}
|
<div
|
||||||
|
bind:this={node}
|
||||||
|
contenteditable
|
||||||
|
use:styleable={styles}
|
||||||
|
class:bold
|
||||||
|
class:italic
|
||||||
|
class:underline
|
||||||
|
class="align--{align || 'left'} size--{size || 'M'}"
|
||||||
|
on:blur={$component.editing ? updateText : null}
|
||||||
|
>
|
||||||
|
{componentText}
|
||||||
|
</div>
|
||||||
|
{:else if $builderStore.inBuilder || componentText}
|
||||||
|
{#if externalLink}
|
||||||
<a
|
<a
|
||||||
{target}
|
{target}
|
||||||
href={url || "/"}
|
href={url || "/"}
|
||||||
|
@ -72,12 +95,12 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
a {
|
a,
|
||||||
|
div {
|
||||||
color: var(--spectrum-alias-text-color);
|
color: var(--spectrum-alias-text-color);
|
||||||
white-space: nowrap;
|
|
||||||
transition: color 130ms ease-in-out;
|
transition: color 130ms ease-in-out;
|
||||||
}
|
}
|
||||||
a:hover {
|
a:not(.placeholder):hover {
|
||||||
color: var(--spectrum-link-primary-m-text-color-hover) !important;
|
color: var(--spectrum-link-primary-m-text-color-hover) !important;
|
||||||
}
|
}
|
||||||
.placeholder {
|
.placeholder {
|
||||||
|
|
|
@ -12,10 +12,11 @@
|
||||||
export let underline
|
export let underline
|
||||||
export let size
|
export let size
|
||||||
|
|
||||||
$: placeholder = $builderStore.inBuilder && !text
|
let node
|
||||||
$: componentText = $builderStore.inBuilder
|
|
||||||
? text || $component.name || "Placeholder text"
|
$: $component.editing && node?.focus()
|
||||||
: text || ""
|
$: placeholder = $builderStore.inBuilder && !text && !$component.editing
|
||||||
|
$: componentText = getComponentText(text, $builderStore, $component)
|
||||||
$: sizeClass = `spectrum-Body--size${size || "M"}`
|
$: sizeClass = `spectrum-Body--size${size || "M"}`
|
||||||
$: alignClass = `align--${align || "left"}`
|
$: alignClass = `align--${align || "left"}`
|
||||||
|
|
||||||
|
@ -23,6 +24,13 @@
|
||||||
// overrides the color when it's passed as inline style.
|
// overrides the color when it's passed as inline style.
|
||||||
$: styles = enrichStyles($component.styles, color)
|
$: styles = enrichStyles($component.styles, color)
|
||||||
|
|
||||||
|
const getComponentText = (text, builderState, componentState) => {
|
||||||
|
if (!builderState.inBuilder || componentState.editing) {
|
||||||
|
return text || ""
|
||||||
|
}
|
||||||
|
return text || componentState.name || "Placeholder text"
|
||||||
|
}
|
||||||
|
|
||||||
const enrichStyles = (styles, color) => {
|
const enrichStyles = (styles, color) => {
|
||||||
if (!color) {
|
if (!color) {
|
||||||
return styles
|
return styles
|
||||||
|
@ -35,15 +43,24 @@
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert contenteditable HTML to text and save
|
||||||
|
const updateText = e => {
|
||||||
|
const sanitized = e.target.innerHTML.replace(/<br>/gi, "\n")
|
||||||
|
builderStore.actions.updateProp("text", sanitized)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
|
bind:this={node}
|
||||||
|
contenteditable={$component.editing}
|
||||||
use:styleable={styles}
|
use:styleable={styles}
|
||||||
class:placeholder
|
class:placeholder
|
||||||
class:bold
|
class:bold
|
||||||
class:italic
|
class:italic
|
||||||
class:underline
|
class:underline
|
||||||
class="spectrum-Body {sizeClass} {alignClass}"
|
class="spectrum-Body {sizeClass} {alignClass}"
|
||||||
|
on:blur={$component.editing ? updateText : null}
|
||||||
>
|
>
|
||||||
{componentText}
|
{componentText}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount, 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 { Heading } from "@budibase/bbui"
|
||||||
|
@ -46,6 +46,7 @@
|
||||||
let repeaterId
|
let repeaterId
|
||||||
let schema
|
let schema
|
||||||
|
|
||||||
|
$: fetchSchema(dataSource)
|
||||||
$: enrichedSearchColumns = enrichSearchColumns(searchColumns, schema)
|
$: enrichedSearchColumns = enrichSearchColumns(searchColumns, schema)
|
||||||
$: enrichedFilter = enrichFilter(filter, enrichedSearchColumns, formId)
|
$: enrichedFilter = enrichFilter(filter, enrichedSearchColumns, formId)
|
||||||
$: cardWidth = cardHorizontal ? 420 : 300
|
$: cardWidth = cardHorizontal ? 420 : 300
|
||||||
|
@ -107,12 +108,12 @@
|
||||||
return `${split[0]}/{{ ${safe(repeaterId)}.${safe(col)} }}`
|
return `${split[0]}/{{ ${safe(repeaterId)}.${safe(col)} }}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the datasource schema on mount so we can determine column types
|
// Load the datasource schema so we can determine column types
|
||||||
onMount(async () => {
|
const fetchSchema = async dataSource => {
|
||||||
if (dataSource) {
|
if (dataSource) {
|
||||||
schema = await API.fetchDatasourceSchema(dataSource)
|
schema = await API.fetchDatasourceSchema(dataSource)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Block>
|
<Block>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount, 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 { Heading } from "@budibase/bbui"
|
||||||
|
@ -41,6 +41,7 @@
|
||||||
let dataProviderId
|
let dataProviderId
|
||||||
let schema
|
let schema
|
||||||
|
|
||||||
|
$: fetchSchema(dataSource)
|
||||||
$: enrichedSearchColumns = enrichSearchColumns(searchColumns, schema)
|
$: enrichedSearchColumns = enrichSearchColumns(searchColumns, schema)
|
||||||
$: enrichedFilter = enrichFilter(filter, enrichedSearchColumns, formId)
|
$: enrichedFilter = enrichFilter(filter, enrichedSearchColumns, formId)
|
||||||
$: titleButtonAction = [
|
$: titleButtonAction = [
|
||||||
|
@ -85,12 +86,12 @@
|
||||||
return enrichedColumns.slice(0, 3)
|
return enrichedColumns.slice(0, 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the datasource schema on mount so we can determine column types
|
// Load the datasource schema so we can determine column types
|
||||||
onMount(async () => {
|
const fetchSchema = async dataSource => {
|
||||||
if (dataSource) {
|
if (dataSource) {
|
||||||
schema = await API.fetchDatasourceSchema(dataSource)
|
schema = await API.fetchDatasourceSchema(dataSource)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Block>
|
<Block>
|
||||||
|
|
|
@ -17,54 +17,53 @@
|
||||||
const formContext = getContext("form")
|
const formContext = getContext("form")
|
||||||
const formStepContext = getContext("form-step")
|
const formStepContext = getContext("form-step")
|
||||||
const fieldGroupContext = getContext("field-group")
|
const fieldGroupContext = getContext("field-group")
|
||||||
const { styleable } = getContext("sdk")
|
const { styleable, builderStore } = getContext("sdk")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
|
||||||
// Register field with form
|
// Register field with form
|
||||||
const formApi = formContext?.formApi
|
const formApi = formContext?.formApi
|
||||||
const labelPos = fieldGroupContext?.labelPosition || "above"
|
const labelPos = fieldGroupContext?.labelPosition || "above"
|
||||||
const formField = formApi?.registerField(
|
$: formStep = formStepContext ? $formStepContext || 1 : 1
|
||||||
|
$: formField = formApi?.registerField(
|
||||||
field,
|
field,
|
||||||
type,
|
type,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
disabled,
|
disabled,
|
||||||
validation,
|
validation,
|
||||||
formStepContext || 1
|
formStep
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Focus label when editing
|
||||||
|
let labelNode
|
||||||
|
$: $component.editing && labelNode?.focus()
|
||||||
|
|
||||||
// Update form properties in parent component on every store change
|
// Update form properties in parent component on every store change
|
||||||
const unsubscribe = formField?.subscribe(value => {
|
$: unsubscribe = formField?.subscribe(value => {
|
||||||
fieldState = value?.fieldState
|
fieldState = value?.fieldState
|
||||||
fieldApi = value?.fieldApi
|
fieldApi = value?.fieldApi
|
||||||
fieldSchema = value?.fieldSchema
|
fieldSchema = value?.fieldSchema
|
||||||
})
|
})
|
||||||
onDestroy(() => unsubscribe?.())
|
onDestroy(() => unsubscribe?.())
|
||||||
|
|
||||||
// Keep field state up to date with props which might change due to
|
|
||||||
// conditional UI
|
|
||||||
$: updateValidation(validation)
|
|
||||||
$: updateDisabled(disabled)
|
|
||||||
|
|
||||||
// Determine label class from position
|
// Determine label class from position
|
||||||
$: labelClass = labelPos === "above" ? "" : `spectrum-FieldLabel--${labelPos}`
|
$: labelClass = labelPos === "above" ? "" : `spectrum-FieldLabel--${labelPos}`
|
||||||
|
|
||||||
const updateValidation = validation => {
|
const updateLabel = e => {
|
||||||
fieldApi?.updateValidation(validation)
|
builderStore.actions.updateProp("label", e.target.textContent)
|
||||||
}
|
|
||||||
|
|
||||||
const updateDisabled = disabled => {
|
|
||||||
fieldApi?.setDisabled(disabled)
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FieldGroupFallback>
|
<FieldGroupFallback>
|
||||||
<div class="spectrum-Form-item" use:styleable={$component.styles}>
|
<div class="spectrum-Form-item" use:styleable={$component.styles}>
|
||||||
<label
|
<label
|
||||||
|
bind:this={labelNode}
|
||||||
|
contenteditable={$component.editing}
|
||||||
|
on:blur={$component.editing ? updateLabel : null}
|
||||||
class:hidden={!label}
|
class:hidden={!label}
|
||||||
for={fieldState?.fieldId}
|
for={fieldState?.fieldId}
|
||||||
class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${labelClass}`}
|
class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${labelClass}`}
|
||||||
>
|
>
|
||||||
{label || ""}
|
{label || " "}
|
||||||
</label>
|
</label>
|
||||||
<div class="spectrum-Form-itemField">
|
<div class="spectrum-Form-itemField">
|
||||||
{#if !formContext}
|
{#if !formContext}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext, onMount } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import InnerForm from "./InnerForm.svelte"
|
import InnerForm from "./InnerForm.svelte"
|
||||||
|
import { hashString } from "utils/helpers"
|
||||||
|
|
||||||
export let dataSource
|
export let dataSource
|
||||||
export let theme
|
export let theme
|
||||||
|
@ -15,6 +16,8 @@
|
||||||
let schema
|
let schema
|
||||||
let table
|
let table
|
||||||
|
|
||||||
|
$: fetchSchema(dataSource)
|
||||||
|
|
||||||
// Returns the closes data context which isn't a built in context
|
// Returns the closes data context which isn't a built in context
|
||||||
const getInitialValues = (type, dataSource, context) => {
|
const getInitialValues = (type, dataSource, context) => {
|
||||||
// Only inherit values for update forms
|
// Only inherit values for update forms
|
||||||
|
@ -35,7 +38,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetches the form schema from this form's dataSource
|
// Fetches the form schema from this form's dataSource
|
||||||
const fetchSchema = async () => {
|
const fetchSchema = async dataSource => {
|
||||||
if (!dataSource) {
|
if (!dataSource) {
|
||||||
schema = {}
|
schema = {}
|
||||||
}
|
}
|
||||||
|
@ -62,14 +65,15 @@
|
||||||
schema = dataSourceSchema || {}
|
schema = dataSourceSchema || {}
|
||||||
}
|
}
|
||||||
|
|
||||||
loaded = true
|
if (!loaded) {
|
||||||
|
loaded = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: initialValues = getInitialValues(actionType, dataSource, $context)
|
$: initialValues = getInitialValues(actionType, dataSource, $context)
|
||||||
$: resetKey = JSON.stringify(initialValues)
|
$: resetKey = hashString(
|
||||||
|
JSON.stringify(initialValues) + JSON.stringify(schema)
|
||||||
// Load the form schema on mount
|
)
|
||||||
onMount(fetchSchema)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext, setContext } from "svelte"
|
import { getContext, setContext } from "svelte"
|
||||||
|
import { writable } from "svelte/store"
|
||||||
import Placeholder from "../Placeholder.svelte"
|
import Placeholder from "../Placeholder.svelte"
|
||||||
|
|
||||||
export let step = 1
|
export let step = 1
|
||||||
|
@ -9,7 +10,9 @@
|
||||||
const formContext = getContext("form")
|
const formContext = getContext("form")
|
||||||
|
|
||||||
// Set form step context so fields know what step they are within
|
// Set form step context so fields know what step they are within
|
||||||
setContext("form-step", step || 1)
|
const stepStore = writable(step || 1)
|
||||||
|
$: stepStore.set(step || 1)
|
||||||
|
setContext("form-step", stepStore)
|
||||||
|
|
||||||
$: formState = formContext?.formState
|
$: formState = formContext?.formState
|
||||||
$: currentStep = $formState?.currentStep
|
$: currentStep = $formState?.currentStep
|
||||||
|
|
|
@ -96,15 +96,14 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we've already registered this field then wipe any errors and
|
// If we've already registered this field then keep some existing state
|
||||||
// return the existing field
|
let initialValue = initialValues[field] ?? defaultValue
|
||||||
|
let fieldId = `id-${generateID()}`
|
||||||
const existingField = getField(field)
|
const existingField = getField(field)
|
||||||
if (existingField) {
|
if (existingField) {
|
||||||
existingField.update(state => {
|
const { fieldState } = get(existingField)
|
||||||
state.fieldState.error = null
|
initialValue = fieldState.value ?? initialValue
|
||||||
return state
|
fieldId = fieldState.fieldId
|
||||||
})
|
|
||||||
return existingField
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto columns are always disabled
|
// Auto columns are always disabled
|
||||||
|
@ -125,8 +124,8 @@
|
||||||
type,
|
type,
|
||||||
step: step || 1,
|
step: step || 1,
|
||||||
fieldState: {
|
fieldState: {
|
||||||
fieldId: `id-${generateID()}`,
|
fieldId,
|
||||||
value: initialValues[field] ?? defaultValue,
|
value: initialValue,
|
||||||
error: null,
|
error: null,
|
||||||
disabled: disabled || fieldDisabled || isAutoColumn,
|
disabled: disabled || fieldDisabled || isAutoColumn,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
|
@ -137,7 +136,12 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add this field
|
// Add this field
|
||||||
fields = [...fields, fieldInfo]
|
if (existingField) {
|
||||||
|
const otherFields = fields.filter(info => get(info).name !== field)
|
||||||
|
fields = [...otherFields, fieldInfo]
|
||||||
|
} else {
|
||||||
|
fields = [...fields, fieldInfo]
|
||||||
|
}
|
||||||
|
|
||||||
return fieldInfo
|
return fieldInfo
|
||||||
},
|
},
|
||||||
|
@ -287,7 +291,7 @@
|
||||||
|
|
||||||
// Provide form step context so that forms without any step components
|
// Provide form step context so that forms without any step components
|
||||||
// register their fields to step 1
|
// register their fields to step 1
|
||||||
setContext("form-step", 1)
|
setContext("form-step", writable(1))
|
||||||
|
|
||||||
// Action context to pass to children
|
// Action context to pass to children
|
||||||
const actions = [
|
const actions = [
|
||||||
|
|
|
@ -17,10 +17,6 @@
|
||||||
|
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const { styleable, getAction, ActionTypes, routeStore } = getContext("sdk")
|
const { styleable, getAction, ActionTypes, routeStore } = getContext("sdk")
|
||||||
const setSorting = getAction(
|
|
||||||
dataProvider?.id,
|
|
||||||
ActionTypes.SetDataProviderSorting
|
|
||||||
)
|
|
||||||
const customColumnKey = `custom-${Math.random()}`
|
const customColumnKey = `custom-${Math.random()}`
|
||||||
const customRenderers = [
|
const customRenderers = [
|
||||||
{
|
{
|
||||||
|
@ -29,13 +25,16 @@
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
// Table state
|
|
||||||
$: hasChildren = $component.children
|
$: hasChildren = $component.children
|
||||||
$: loading = dataProvider?.loading ?? false
|
$: loading = dataProvider?.loading ?? false
|
||||||
$: data = dataProvider?.rows || []
|
$: data = dataProvider?.rows || []
|
||||||
$: fullSchema = dataProvider?.schema ?? {}
|
$: fullSchema = dataProvider?.schema ?? {}
|
||||||
$: fields = getFields(fullSchema, columns, showAutoColumns)
|
$: fields = getFields(fullSchema, columns, showAutoColumns)
|
||||||
$: schema = getFilteredSchema(fullSchema, fields, hasChildren)
|
$: schema = getFilteredSchema(fullSchema, fields, hasChildren)
|
||||||
|
$: setSorting = getAction(
|
||||||
|
dataProvider?.id,
|
||||||
|
ActionTypes.SetDataProviderSorting
|
||||||
|
)
|
||||||
|
|
||||||
const getFields = (schema, customColumns, showAutoColumns) => {
|
const getFields = (schema, customColumns, showAutoColumns) => {
|
||||||
// Check for an invalid column selection
|
// Check for an invalid column selection
|
||||||
|
|
|
@ -17,10 +17,9 @@
|
||||||
|
|
||||||
<div
|
<div
|
||||||
in:fade={{
|
in:fade={{
|
||||||
delay: transition ? 50 : 0,
|
delay: transition ? 130 : 0,
|
||||||
duration: transition ? 130 : 0,
|
duration: transition ? 130 : 0,
|
||||||
}}
|
}}
|
||||||
out:fade={{ duration: transition ? 130 : 0 }}
|
|
||||||
class="indicator"
|
class="indicator"
|
||||||
class:flipped
|
class:flipped
|
||||||
class:line
|
class:line
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
<script>
|
||||||
|
import { onMount, onDestroy } from "svelte"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import { builderStore } from "stores"
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (get(builderStore).inBuilder) {
|
||||||
|
document.addEventListener("keydown", onKeyDown)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (get(builderStore).inBuilder) {
|
||||||
|
document.removeEventListener("keydown", onKeyDown)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const onKeyDown = e => {
|
||||||
|
if (e.key === "Delete" || e.key === "Backspace") {
|
||||||
|
deleteSelectedComponent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteSelectedComponent = () => {
|
||||||
|
const state = get(builderStore)
|
||||||
|
if (!state.inBuilder || !state.selectedComponentId || state.editMode) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const activeTag = document.activeElement?.tagName.toLowerCase()
|
||||||
|
if (["input", "textarea"].indexOf(activeTag) !== -1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
builderStore.actions.deleteComponent(state.selectedComponentId)
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,11 +1,15 @@
|
||||||
<script>
|
<script>
|
||||||
import { builderStore } from "stores"
|
import { builderStore } from "stores"
|
||||||
import IndicatorSet from "./IndicatorSet.svelte"
|
import IndicatorSet from "./IndicatorSet.svelte"
|
||||||
|
|
||||||
|
$: color = $builderStore.editMode
|
||||||
|
? "var(--spectrum-global-color-static-green-500)"
|
||||||
|
: "var(--spectrum-global-color-static-blue-600)"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<IndicatorSet
|
<IndicatorSet
|
||||||
componentId={$builderStore.selectedComponentId}
|
componentId={$builderStore.selectedComponentId}
|
||||||
color="var(--spectrum-global-color-static-blue-600)"
|
{color}
|
||||||
zIndex="910"
|
zIndex="910"
|
||||||
transition
|
transition
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -122,7 +122,7 @@
|
||||||
prop={setting.key}
|
prop={setting.key}
|
||||||
value={option.value}
|
value={option.value}
|
||||||
icon={option.barIcon}
|
icon={option.barIcon}
|
||||||
title={option.barTitle}
|
title={option.barTitle || option.label}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{:else}
|
||||||
|
@ -136,7 +136,7 @@
|
||||||
<SettingsButton
|
<SettingsButton
|
||||||
prop={setting.key}
|
prop={setting.key}
|
||||||
icon={setting.barIcon}
|
icon={setting.barIcon}
|
||||||
title={setting.barTitle}
|
title={setting.barTitle || setting.label}
|
||||||
bool
|
bool
|
||||||
/>
|
/>
|
||||||
{:else if setting.type === "color"}
|
{:else if setting.type === "color"}
|
||||||
|
|
|
@ -25,7 +25,8 @@ export const UnsortableTypes = [
|
||||||
export const ActionTypes = {
|
export const ActionTypes = {
|
||||||
ValidateForm: "ValidateForm",
|
ValidateForm: "ValidateForm",
|
||||||
RefreshDatasource: "RefreshDatasource",
|
RefreshDatasource: "RefreshDatasource",
|
||||||
SetDataProviderQuery: "SetDataProviderQuery",
|
AddDataProviderQueryExtension: "AddDataProviderQueryExtension",
|
||||||
|
RemoveDataProviderQueryExtension: "RemoveDataProviderQueryExtension",
|
||||||
SetDataProviderSorting: "SetDataProviderSorting",
|
SetDataProviderSorting: "SetDataProviderSorting",
|
||||||
ClearForm: "ClearForm",
|
ClearForm: "ClearForm",
|
||||||
ChangeFormStep: "ChangeFormStep",
|
ChangeFormStep: "ChangeFormStep",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { writable, derived } from "svelte/store"
|
import { writable, derived, get } from "svelte/store"
|
||||||
import Manifest from "manifest.json"
|
import Manifest from "manifest.json"
|
||||||
import { findComponentById, findComponentPathById } from "../utils/components"
|
import { findComponentById, findComponentPathById } from "../utils/components"
|
||||||
import { pingEndUser } from "../api"
|
import { pingEndUser } from "../api"
|
||||||
|
@ -14,6 +14,7 @@ const createBuilderStore = () => {
|
||||||
layout: null,
|
layout: null,
|
||||||
screen: null,
|
screen: null,
|
||||||
selectedComponentId: null,
|
selectedComponentId: null,
|
||||||
|
editMode: false,
|
||||||
previewId: null,
|
previewId: null,
|
||||||
previewType: null,
|
previewType: null,
|
||||||
selectedPath: [],
|
selectedPath: [],
|
||||||
|
@ -50,6 +51,10 @@ const createBuilderStore = () => {
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
selectComponent: id => {
|
selectComponent: id => {
|
||||||
|
if (id === get(writableStore).selectedComponentId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writableStore.update(state => ({ ...state, editMode: false }))
|
||||||
dispatchEvent("select-component", { id })
|
dispatchEvent("select-component", { id })
|
||||||
},
|
},
|
||||||
updateProp: (prop, value) => {
|
updateProp: (prop, value) => {
|
||||||
|
@ -65,10 +70,7 @@ const createBuilderStore = () => {
|
||||||
pingEndUser()
|
pingEndUser()
|
||||||
},
|
},
|
||||||
setSelectedPath: path => {
|
setSelectedPath: path => {
|
||||||
writableStore.update(state => {
|
writableStore.update(state => ({ ...state, selectedPath: path }))
|
||||||
state.selectedPath = path
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
moveComponent: (componentId, destinationComponentId, mode) => {
|
moveComponent: (componentId, destinationComponentId, mode) => {
|
||||||
dispatchEvent("move-component", {
|
dispatchEvent("move-component", {
|
||||||
|
@ -78,10 +80,16 @@ const createBuilderStore = () => {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
setDragging: dragging => {
|
setDragging: dragging => {
|
||||||
writableStore.update(state => {
|
if (dragging === get(writableStore).isDragging) {
|
||||||
state.isDragging = dragging
|
return
|
||||||
return state
|
}
|
||||||
})
|
writableStore.update(state => ({ ...state, isDragging: dragging }))
|
||||||
|
},
|
||||||
|
setEditMode: enabled => {
|
||||||
|
if (enabled === get(writableStore).editMode) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writableStore.update(state => ({ ...state, editMode: enabled }))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -21,12 +21,7 @@ export const styleable = (node, styles = {}) => {
|
||||||
let applyNormalStyles
|
let applyNormalStyles
|
||||||
let applyHoverStyles
|
let applyHoverStyles
|
||||||
let selectComponent
|
let selectComponent
|
||||||
|
let editComponent
|
||||||
// Allow dragging if required
|
|
||||||
const parent = node.closest(".component")
|
|
||||||
if (parent && parent.classList.contains("draggable")) {
|
|
||||||
node.setAttribute("draggable", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates event listeners and applies initial styles
|
// Creates event listeners and applies initial styles
|
||||||
const setupStyles = (newStyles = {}) => {
|
const setupStyles = (newStyles = {}) => {
|
||||||
|
@ -45,6 +40,9 @@ export const styleable = (node, styles = {}) => {
|
||||||
...(newStyles.hover || {}),
|
...(newStyles.hover || {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow dragging if required
|
||||||
|
node.setAttribute("draggable", !!newStyles.draggable)
|
||||||
|
|
||||||
// Applies a style string to a DOM node
|
// Applies a style string to a DOM node
|
||||||
const applyStyles = styleString => {
|
const applyStyles = styleString => {
|
||||||
node.style = styleString
|
node.style = styleString
|
||||||
|
@ -69,6 +67,17 @@ export const styleable = (node, styles = {}) => {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handler to start editing a component (if applicable) when double
|
||||||
|
// clicking in the builder preview
|
||||||
|
editComponent = event => {
|
||||||
|
if (newStyles.interactive && newStyles.editable) {
|
||||||
|
builderStore.actions.setEditMode(true)
|
||||||
|
}
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Add listeners to toggle hover styles
|
// Add listeners to toggle hover styles
|
||||||
node.addEventListener("mouseover", applyHoverStyles)
|
node.addEventListener("mouseover", applyHoverStyles)
|
||||||
node.addEventListener("mouseout", applyNormalStyles)
|
node.addEventListener("mouseout", applyNormalStyles)
|
||||||
|
@ -76,6 +85,7 @@ export const styleable = (node, styles = {}) => {
|
||||||
// Add builder preview click listener
|
// Add builder preview click listener
|
||||||
if (newStyles.interactive) {
|
if (newStyles.interactive) {
|
||||||
node.addEventListener("click", selectComponent, false)
|
node.addEventListener("click", selectComponent, false)
|
||||||
|
node.addEventListener("dblclick", editComponent, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply initial normal styles
|
// Apply initial normal styles
|
||||||
|
@ -87,6 +97,7 @@ export const styleable = (node, styles = {}) => {
|
||||||
node.removeEventListener("mouseover", applyHoverStyles)
|
node.removeEventListener("mouseover", applyHoverStyles)
|
||||||
node.removeEventListener("mouseout", applyNormalStyles)
|
node.removeEventListener("mouseout", applyNormalStyles)
|
||||||
node.removeEventListener("click", selectComponent)
|
node.removeEventListener("click", selectComponent)
|
||||||
|
node.removeEventListener("dblclick", editComponent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply initial styles
|
// Apply initial styles
|
||||||
|
|
Loading…
Reference in New Issue