2021-11-08 15:35:58 +01:00
|
|
|
<script context="module">
|
|
|
|
let SettingsDefinitionCache = {}
|
|
|
|
</script>
|
|
|
|
|
2020-11-13 16:42:32 +01:00
|
|
|
<script>
|
2020-11-20 10:50:10 +01:00
|
|
|
import { getContext, setContext } from "svelte"
|
2021-11-16 17:29:31 +01:00
|
|
|
import { writable } from "svelte/store"
|
2021-09-01 12:41:48 +02:00
|
|
|
import * as AppComponents from "components/app"
|
2020-11-18 20:18:18 +01:00
|
|
|
import Router from "./Router.svelte"
|
2021-09-01 12:41:48 +02:00
|
|
|
import { enrichProps, propsAreSame } from "utils/componentProps"
|
|
|
|
import { builderStore } from "stores"
|
|
|
|
import { hashString } from "utils/helpers"
|
|
|
|
import Manifest from "manifest.json"
|
|
|
|
import { getActiveConditions, reduceConditionActions } from "utils/conditions"
|
|
|
|
import Placeholder from "components/app/Placeholder.svelte"
|
2020-11-13 16:42:32 +01:00
|
|
|
|
2021-06-11 09:05:49 +02:00
|
|
|
export let instance = {}
|
2021-09-16 08:52:49 +02:00
|
|
|
export let isLayout = false
|
|
|
|
export let isScreen = false
|
2021-11-12 16:19:25 +01:00
|
|
|
export let isBlock = false
|
2020-11-13 16:42:32 +01:00
|
|
|
|
2021-11-16 17:29:31 +01:00
|
|
|
// 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
|
|
|
|
|
2021-07-21 15:03:49 +02:00
|
|
|
// The enriched component settings
|
|
|
|
let enrichedSettings
|
|
|
|
|
2021-11-16 17:29:31 +01:00
|
|
|
// Any setting overrides that need to be applied due to conditional UI
|
2021-07-21 15:03:49 +02:00
|
|
|
let conditionalSettings
|
2020-11-13 16:42:32 +01:00
|
|
|
|
2021-11-16 17:29:31 +01:00
|
|
|
// Resultant cached settings which will be passed to the component instance.
|
|
|
|
// These are a combination of the enriched, nested and conditional settings.
|
|
|
|
let cachedSettings
|
2021-01-27 16:52:12 +01:00
|
|
|
|
2021-01-29 14:22:38 +01:00
|
|
|
// Latest timestamp that we started a props update.
|
|
|
|
// Due to enrichment now being async, we need to avoid overwriting newer
|
2021-11-08 15:35:58 +01:00
|
|
|
// settings with old ones, depending on how long enrichment takes.
|
2021-01-29 14:22:38 +01:00
|
|
|
let latestUpdateTime
|
|
|
|
|
2021-06-25 16:04:27 +02:00
|
|
|
// Keep track of stringified representations of context and instance
|
|
|
|
// to avoid enriching bindings as much as possible
|
|
|
|
let lastContextKey
|
|
|
|
let lastInstanceKey
|
|
|
|
|
2021-07-21 15:03:49 +02:00
|
|
|
// Visibility flag used by conditional UI
|
|
|
|
let visible = true
|
|
|
|
|
2021-01-06 11:13:30 +01:00
|
|
|
// Get contexts
|
2021-02-01 19:51:22 +01:00
|
|
|
const context = getContext("context")
|
2021-06-11 14:34:37 +02:00
|
|
|
const insideScreenslot = !!getContext("screenslot")
|
2020-11-18 20:18:18 +01:00
|
|
|
|
2020-11-25 10:50:51 +01:00
|
|
|
// Create component context
|
|
|
|
const componentStore = writable({})
|
|
|
|
setContext("component", componentStore)
|
2020-11-17 13:08:24 +01:00
|
|
|
|
2021-06-11 09:05:49 +02:00
|
|
|
// Extract component instance info
|
|
|
|
$: constructor = getComponentConstructor(instance._component)
|
|
|
|
$: definition = getComponentDefinition(instance._component)
|
2021-11-08 15:35:58 +01:00
|
|
|
$: settingsDefinition = getSettingsDefinition(definition)
|
2021-06-11 09:05:49 +02:00
|
|
|
$: children = instance._children || []
|
|
|
|
$: id = instance._id
|
|
|
|
$: name = instance._instanceName
|
2021-11-08 15:35:58 +01:00
|
|
|
|
|
|
|
// Determine if the component is selected or is part of the critical path
|
|
|
|
// leading to the selected component
|
2021-06-08 09:00:54 +02:00
|
|
|
$: selected =
|
2021-10-28 13:43:31 +02:00
|
|
|
$builderStore.inBuilder && $builderStore.selectedComponentId === id
|
2021-08-23 15:01:57 +02:00
|
|
|
$: inSelectedPath = $builderStore.selectedComponentPath?.includes(id)
|
2021-11-16 14:17:34 +01:00
|
|
|
$: inDragPath = inSelectedPath && $builderStore.editMode
|
2021-11-08 15:35:58 +01:00
|
|
|
|
2021-11-18 21:32:42 +01:00
|
|
|
// Derive definition properties which can all be optional, so need to be
|
|
|
|
// coerced to booleans
|
|
|
|
$: editable = !!definition?.editable
|
|
|
|
$: hasChildren = !!definition?.hasChildren
|
2021-11-18 21:38:55 +01:00
|
|
|
$: showEmptyState = definition?.showEmptyState !== false
|
2021-11-18 21:32:42 +01:00
|
|
|
|
2021-11-08 15:35:58 +01:00
|
|
|
// Interactive components can be selected, dragged and highlighted inside
|
|
|
|
// the builder preview
|
2021-09-20 16:34:51 +02:00
|
|
|
$: interactive =
|
|
|
|
$builderStore.inBuilder &&
|
2021-11-02 09:45:27 +01:00
|
|
|
($builderStore.previewType === "layout" || insideScreenslot) &&
|
2021-11-12 16:19:25 +01:00
|
|
|
!isBlock
|
2021-10-28 13:43:31 +02:00
|
|
|
$: editing = editable && selected && $builderStore.editMode
|
2021-11-16 14:17:34 +01:00
|
|
|
$: draggable = !inDragPath && interactive && !isLayout && !isScreen
|
2021-11-16 12:46:46 +01:00
|
|
|
$: droppable = interactive && !isLayout && !isScreen
|
2021-11-08 15:35:58 +01:00
|
|
|
|
|
|
|
// Empty components are those which accept children but do not have any.
|
|
|
|
// Empty states can be shown for these components, but can be disabled
|
|
|
|
// in the component manifest.
|
2021-11-18 21:32:42 +01:00
|
|
|
$: empty = interactive && !children.length && hasChildren
|
|
|
|
$: emptyState = empty && showEmptyState
|
2021-11-08 15:35:58 +01:00
|
|
|
|
2021-11-16 17:29:31 +01:00
|
|
|
// Raw settings are all settings excluding internal props and children
|
2021-11-08 15:35:58 +01:00
|
|
|
$: rawSettings = getRawSettings(instance)
|
|
|
|
$: instanceKey = hashString(JSON.stringify(rawSettings))
|
|
|
|
|
2021-11-16 17:29:31 +01:00
|
|
|
// Update and enrich component settings
|
|
|
|
$: updateSettings(rawSettings, instanceKey, settingsDefinition, $context)
|
2021-11-08 15:35:58 +01:00
|
|
|
|
|
|
|
// Evaluate conditional UI settings and store any component setting changes
|
|
|
|
// which need to be made
|
2021-07-21 15:03:49 +02:00
|
|
|
$: evaluateConditions(enrichedSettings?._conditions)
|
2021-11-08 15:35:58 +01:00
|
|
|
|
|
|
|
// Build up the final settings object to be passed to the component
|
2021-11-16 17:29:31 +01:00
|
|
|
$: cacheSettings(enrichedSettings, nestedSettings, conditionalSettings)
|
2021-01-06 11:13:30 +01:00
|
|
|
|
2021-11-18 21:32:42 +01:00
|
|
|
// Render key is used to determine when components need to fully remount
|
|
|
|
$: renderKey = getRenderKey(id, editing)
|
|
|
|
|
2020-11-24 12:02:10 +01:00
|
|
|
// Update component context
|
2021-01-22 12:08:42 +01:00
|
|
|
$: componentStore.set({
|
|
|
|
id,
|
|
|
|
children: children.length,
|
2021-10-28 13:43:31 +02:00
|
|
|
styles: {
|
|
|
|
...instance._styles,
|
|
|
|
id,
|
|
|
|
empty: emptyState,
|
|
|
|
interactive,
|
|
|
|
draggable,
|
|
|
|
editable,
|
|
|
|
},
|
2021-09-20 16:34:51 +02:00
|
|
|
empty: emptyState,
|
2021-06-08 09:00:54 +02:00
|
|
|
selected,
|
2021-06-11 09:05:49 +02:00
|
|
|
name,
|
2021-10-28 13:43:31 +02:00
|
|
|
editing,
|
2021-01-22 12:08:42 +01:00
|
|
|
})
|
2020-11-25 10:50:51 +01:00
|
|
|
|
2021-11-16 17:29:31 +01:00
|
|
|
// Extracts all settings from the component instance
|
2021-11-08 15:35:58 +01:00
|
|
|
const getRawSettings = instance => {
|
|
|
|
let validSettings = {}
|
2021-06-25 16:04:27 +02:00
|
|
|
Object.entries(instance)
|
2021-07-21 15:03:49 +02:00
|
|
|
.filter(([name]) => name === "_conditions" || !name.startsWith("_"))
|
2021-06-25 16:04:27 +02:00
|
|
|
.forEach(([key, value]) => {
|
2021-11-08 15:35:58 +01:00
|
|
|
validSettings[key] = value
|
2021-06-25 16:04:27 +02:00
|
|
|
})
|
2021-11-08 15:35:58 +01:00
|
|
|
return validSettings
|
2021-06-25 16:04:27 +02:00
|
|
|
}
|
|
|
|
|
2021-01-29 14:22:38 +01:00
|
|
|
// Gets the component constructor for the specified component
|
|
|
|
const getComponentConstructor = component => {
|
|
|
|
const split = component?.split("/")
|
|
|
|
const name = split?.[split.length - 1]
|
|
|
|
if (name === "screenslot" && $builderStore.previewType !== "layout") {
|
|
|
|
return Router
|
|
|
|
}
|
2021-09-01 12:41:48 +02:00
|
|
|
return AppComponents[name]
|
2021-01-29 14:22:38 +01:00
|
|
|
}
|
|
|
|
|
2021-11-16 17:29:31 +01:00
|
|
|
// Gets this component's definition from the manifest
|
2021-06-11 09:05:49 +02:00
|
|
|
const getComponentDefinition = component => {
|
|
|
|
const prefix = "@budibase/standard-components/"
|
|
|
|
const type = component?.replace(prefix, "")
|
|
|
|
return type ? Manifest[type] : null
|
|
|
|
}
|
|
|
|
|
2021-11-16 17:29:31 +01:00
|
|
|
// Gets the definition of this component's settings from the manifest
|
2021-11-08 15:35:58 +01:00
|
|
|
const getSettingsDefinition = definition => {
|
|
|
|
if (!definition) {
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
if (SettingsDefinitionCache[definition.name]) {
|
|
|
|
return SettingsDefinitionCache[definition.name]
|
|
|
|
}
|
|
|
|
let settings = []
|
|
|
|
definition.settings?.forEach(setting => {
|
|
|
|
if (setting.section) {
|
|
|
|
settings = settings.concat(setting.settings || [])
|
|
|
|
} else {
|
|
|
|
settings.push(setting)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
SettingsDefinitionCache[definition] = settings
|
|
|
|
return settings
|
|
|
|
}
|
|
|
|
|
2021-11-16 17:29:31 +01:00
|
|
|
// Updates and enriches component settings when raw settings change
|
|
|
|
const updateSettings = (settings, key, settingsDefinition, context) => {
|
|
|
|
const instanceChanged = key !== lastInstanceKey
|
|
|
|
|
|
|
|
// Derive component and nested settings if the instance changed
|
|
|
|
if (instanceChanged) {
|
|
|
|
splitRawSettings(settings, settingsDefinition)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Enrich component settings
|
|
|
|
enrichComponentSettings(componentSettings, context, instanceChanged)
|
|
|
|
|
|
|
|
// Update instance key
|
|
|
|
if (instanceChanged) {
|
|
|
|
lastInstanceKey = key
|
|
|
|
}
|
2021-11-08 15:35:58 +01:00
|
|
|
}
|
|
|
|
|
2021-11-16 17:29:31 +01:00
|
|
|
// Splits the raw settings into those destined for the component itself
|
|
|
|
// and nexted settings for child components inside blocks
|
|
|
|
const splitRawSettings = (rawSettings, settingsDefinition) => {
|
|
|
|
let newComponentSettings = { ...rawSettings }
|
|
|
|
let newNestedSettings = { ...rawSettings }
|
2021-11-08 15:35:58 +01:00
|
|
|
settingsDefinition?.forEach(setting => {
|
2021-11-16 17:29:31 +01:00
|
|
|
if (setting.nested) {
|
|
|
|
delete newComponentSettings[setting.key]
|
|
|
|
} else {
|
|
|
|
delete newNestedSettings[setting.key]
|
2021-11-08 15:35:58 +01:00
|
|
|
}
|
|
|
|
})
|
2021-11-16 17:29:31 +01:00
|
|
|
componentSettings = newComponentSettings
|
|
|
|
nestedSettings = newNestedSettings
|
2021-11-08 15:35:58 +01:00
|
|
|
}
|
|
|
|
|
2021-01-29 14:22:38 +01:00
|
|
|
// Enriches any string component props using handlebars
|
2021-11-16 17:29:31 +01:00
|
|
|
const enrichComponentSettings = (rawSettings, context, instanceChanged) => {
|
|
|
|
const contextChanged = context.key !== lastContextKey
|
2021-06-25 16:04:27 +02:00
|
|
|
|
2021-11-16 17:29:31 +01:00
|
|
|
// Skip enrichment if the context and instance are unchanged
|
|
|
|
if (!contextChanged) {
|
|
|
|
if (!instanceChanged) {
|
|
|
|
return
|
|
|
|
}
|
2021-06-25 16:04:27 +02:00
|
|
|
} else {
|
|
|
|
lastContextKey = context.key
|
|
|
|
}
|
|
|
|
|
2021-01-29 14:22:38 +01:00
|
|
|
// Record the timestamp so we can reference it after enrichment
|
|
|
|
latestUpdateTime = Date.now()
|
|
|
|
const enrichmentTime = latestUpdateTime
|
|
|
|
|
2021-11-08 15:35:58 +01:00
|
|
|
// Enrich settings with context
|
|
|
|
const newEnrichedSettings = enrichProps(rawSettings, context)
|
2021-01-29 14:22:38 +01:00
|
|
|
|
|
|
|
// Abandon this update if a newer update has started
|
|
|
|
if (enrichmentTime !== latestUpdateTime) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-11-16 17:29:31 +01:00
|
|
|
enrichedSettings = newEnrichedSettings
|
2021-07-21 15:03:49 +02:00
|
|
|
}
|
|
|
|
|
2021-11-16 17:29:31 +01:00
|
|
|
// Evaluates the list of conditional UI conditions and determines any setting
|
|
|
|
// or visibility changes required
|
2021-07-21 15:03:49 +02:00
|
|
|
const evaluateConditions = conditions => {
|
|
|
|
if (!conditions?.length) {
|
|
|
|
return
|
2021-01-27 16:52:12 +01:00
|
|
|
}
|
2021-07-21 15:03:49 +02:00
|
|
|
|
|
|
|
// Default visible to false if there is a show condition
|
2021-07-26 14:16:45 +02:00
|
|
|
let nextVisible = !conditions.find(condition => condition.action === "show")
|
2021-07-21 15:03:49 +02:00
|
|
|
|
2021-07-26 14:16:45 +02:00
|
|
|
// Execute conditions and determine settings and visibility changes
|
2021-07-21 15:03:49 +02:00
|
|
|
const activeConditions = getActiveConditions(conditions)
|
|
|
|
const result = reduceConditionActions(activeConditions)
|
|
|
|
if (result.visible != null) {
|
|
|
|
nextVisible = result.visible
|
|
|
|
}
|
|
|
|
|
2021-07-26 14:16:45 +02:00
|
|
|
// Update state from condition results
|
|
|
|
conditionalSettings = result.settingUpdates
|
2021-07-21 15:03:49 +02:00
|
|
|
visible = nextVisible
|
2021-01-21 11:41:30 +01:00
|
|
|
}
|
2021-11-16 17:29:31 +01:00
|
|
|
|
|
|
|
// 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]
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2021-11-18 21:32:42 +01:00
|
|
|
|
|
|
|
// Generates a key used to determine when components need to fully remount.
|
|
|
|
// Currently only toggling editing requires remounting.
|
|
|
|
const getRenderKey = (id, editing) => {
|
|
|
|
return hashString(`${id}-${editing}`)
|
|
|
|
}
|
2020-11-13 16:42:32 +01:00
|
|
|
</script>
|
|
|
|
|
2021-11-18 21:32:42 +01:00
|
|
|
{#key renderKey}
|
|
|
|
{#if constructor && cachedSettings && (visible || inSelectedPath)}
|
|
|
|
<!-- The ID is used as a class because getElementsByClassName is O(1) -->
|
|
|
|
<!-- and the performance matters for the selection indicators -->
|
|
|
|
<div
|
|
|
|
class={`component ${id}`}
|
|
|
|
class:draggable
|
|
|
|
class:droppable
|
|
|
|
class:empty
|
|
|
|
class:interactive
|
|
|
|
class:editing
|
|
|
|
class:block={isBlock}
|
|
|
|
data-id={id}
|
|
|
|
data-name={name}
|
|
|
|
>
|
|
|
|
<svelte:component this={constructor} {...cachedSettings}>
|
|
|
|
{#if children.length}
|
|
|
|
{#each children as child (child._id)}
|
|
|
|
<svelte:self instance={child} />
|
|
|
|
{/each}
|
|
|
|
{:else if emptyState}
|
|
|
|
<Placeholder />
|
|
|
|
{:else if isBlock}
|
|
|
|
<slot />
|
|
|
|
{/if}
|
|
|
|
</svelte:component>
|
|
|
|
</div>
|
|
|
|
{/if}
|
|
|
|
{/key}
|
2021-06-08 09:00:54 +02:00
|
|
|
|
|
|
|
<style>
|
2021-06-11 09:05:49 +02:00
|
|
|
.component {
|
2021-06-08 09:00:54 +02:00
|
|
|
display: contents;
|
|
|
|
}
|
2021-09-20 16:34:51 +02:00
|
|
|
.interactive :global(*:hover) {
|
|
|
|
cursor: pointer;
|
|
|
|
}
|
|
|
|
.draggable :global(*:hover) {
|
2021-09-20 09:26:44 +02:00
|
|
|
cursor: grab;
|
2021-09-16 08:28:59 +02:00
|
|
|
}
|
2021-10-28 13:43:31 +02:00
|
|
|
.editing :global(*:hover) {
|
|
|
|
cursor: auto;
|
|
|
|
}
|
2021-06-08 09:00:54 +02:00
|
|
|
</style>
|