2021-11-08 15:35:58 +01:00
|
|
|
<script context="module">
|
2022-01-31 19:54:04 +01:00
|
|
|
// Cache the definition of settings for each component type
|
2021-11-08 15:35:58 +01:00
|
|
|
let SettingsDefinitionCache = {}
|
2022-03-15 12:16:51 +01:00
|
|
|
let SettingsDefinitionMapCache = {}
|
2022-01-31 19:54:04 +01:00
|
|
|
|
|
|
|
// Cache the settings of each component ID.
|
|
|
|
// This speeds up remounting as well as repeaters.
|
|
|
|
let InstanceSettingsCache = {}
|
2021-11-08 15:35:58 +01:00
|
|
|
</script>
|
|
|
|
|
2020-11-13 16:42:32 +01:00
|
|
|
<script>
|
2024-01-22 12:10:03 +01:00
|
|
|
import { getContext, setContext, onMount } from "svelte"
|
2022-02-01 17:32:37 +01:00
|
|
|
import { writable, get } from "svelte/store"
|
2022-02-24 16:36:21 +01:00
|
|
|
import {
|
|
|
|
enrichProps,
|
|
|
|
propsAreSame,
|
|
|
|
getSettingsDefinition,
|
|
|
|
} from "utils/componentProps"
|
2022-10-14 16:45:02 +02:00
|
|
|
import {
|
|
|
|
builderStore,
|
|
|
|
devToolsStore,
|
|
|
|
componentStore,
|
|
|
|
appStore,
|
2022-10-14 19:16:19 +02:00
|
|
|
dndComponentPath,
|
2022-10-18 19:29:21 +02:00
|
|
|
dndIsDragging,
|
2022-10-14 16:45:02 +02:00
|
|
|
} from "stores"
|
2022-01-20 11:16:13 +01:00
|
|
|
import { Helpers } from "@budibase/bbui"
|
2021-09-01 12:41:48 +02:00
|
|
|
import { getActiveConditions, reduceConditionActions } from "utils/conditions"
|
2023-03-28 22:11:33 +02:00
|
|
|
import EmptyPlaceholder from "components/app/EmptyPlaceholder.svelte"
|
2022-06-07 14:41:17 +02:00
|
|
|
import ScreenPlaceholder from "components/app/ScreenPlaceholder.svelte"
|
2023-03-28 22:11:33 +02:00
|
|
|
import ComponentErrorState from "components/error-states/ComponentErrorState.svelte"
|
|
|
|
import { BudibasePrefix } from "../stores/components.js"
|
2024-01-22 12:10:03 +01:00
|
|
|
import {
|
|
|
|
decodeJSBinding,
|
|
|
|
findHBSBlocks,
|
|
|
|
isJSBinding,
|
|
|
|
} from "@budibase/string-templates"
|
|
|
|
import {
|
|
|
|
getActionContextKey,
|
|
|
|
getActionDependentContextKeys,
|
|
|
|
} from "../utils/buttonActions.js"
|
2024-08-02 17:13:49 +02:00
|
|
|
import { gridLayout } from "utils/grid.js"
|
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
|
2023-08-14 14:31:12 +02:00
|
|
|
export let isRoot = false
|
2021-11-12 16:19:25 +01:00
|
|
|
export let isBlock = false
|
2020-11-13 16:42:32 +01:00
|
|
|
|
2022-02-01 17:32:37 +01:00
|
|
|
// Get parent contexts
|
|
|
|
const context = getContext("context")
|
|
|
|
const insideScreenslot = !!getContext("screenslot")
|
2023-03-28 22:11:33 +02:00
|
|
|
const component = getContext("component")
|
2022-02-01 17:32:37 +01:00
|
|
|
|
|
|
|
// Create component context
|
2022-02-24 15:03:29 +01:00
|
|
|
const store = writable({})
|
|
|
|
setContext("component", store)
|
2022-02-01 17:32:37 +01:00
|
|
|
|
2021-12-17 09:22:40 +01:00
|
|
|
// Ref to the svelte component
|
|
|
|
let ref
|
|
|
|
|
2021-12-17 10:18:07 +01:00
|
|
|
// Initial settings are passed in on first render of the component.
|
|
|
|
// When the first instance of cachedSettings are set, this object is set to
|
|
|
|
// reference cachedSettings, so that mutations to cachedSettings also affect
|
|
|
|
// initialSettings, but it does not get caught by svelte invalidation - which
|
|
|
|
// would happen if we spread cachedSettings directly to the component.
|
|
|
|
let initialSettings
|
|
|
|
|
2022-02-01 17:32:37 +01:00
|
|
|
// Dynamic settings contain bindings and need enriched
|
2022-01-29 19:53:21 +01:00
|
|
|
let dynamicSettings
|
2022-02-01 17:32:37 +01:00
|
|
|
|
|
|
|
// Static settings do not contain any bindings and can be passed on down
|
2022-01-29 19:53:21 +01:00
|
|
|
let staticSettings
|
2021-11-16 17:29:31 +01:00
|
|
|
|
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
|
|
|
|
2023-02-22 14:22:59 +01:00
|
|
|
// Conditional UI expressions, enriched and ready to evaluate
|
|
|
|
let conditions
|
|
|
|
|
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 lastInstanceKey
|
|
|
|
|
2021-07-21 15:03:49 +02:00
|
|
|
// Visibility flag used by conditional UI
|
|
|
|
let visible = true
|
|
|
|
|
2022-02-01 17:32:37 +01:00
|
|
|
// Component information derived during initialisation
|
2022-01-31 19:54:04 +01:00
|
|
|
let constructor
|
|
|
|
let definition
|
2022-03-15 12:16:51 +01:00
|
|
|
let settingsDefinition
|
|
|
|
let settingsDefinitionMap
|
2022-06-13 13:09:29 +02:00
|
|
|
let missingRequiredSettings = false
|
2020-11-18 20:18:18 +01:00
|
|
|
|
2024-07-31 11:35:57 +02:00
|
|
|
// Temporary styles which can be added in the app preview for things like
|
2024-07-30 16:20:59 +02:00
|
|
|
// DND. We clear these whenever a new instance is received.
|
2022-10-20 09:43:33 +02:00
|
|
|
let ephemeralStyles
|
|
|
|
|
2024-01-22 12:10:03 +01:00
|
|
|
// Single string of all HBS blocks, used to check if we use a certain binding
|
|
|
|
// or not
|
|
|
|
let bindingString = ""
|
|
|
|
|
|
|
|
// List of context keys which we use inside bindings
|
|
|
|
let knownContextKeyMap = {}
|
|
|
|
|
2024-02-09 17:44:11 +01:00
|
|
|
// Cleanup function to stop observing context changes when unmounting
|
|
|
|
let unobserve
|
|
|
|
|
2022-02-01 17:32:37 +01:00
|
|
|
// Set up initial state for each new component instance
|
2022-01-31 19:54:04 +01:00
|
|
|
$: initialise(instance)
|
2020-11-17 13:08:24 +01:00
|
|
|
|
2021-06-11 09:05:49 +02:00
|
|
|
// Extract component instance info
|
|
|
|
$: children = instance._children || []
|
|
|
|
$: id = instance._id
|
2023-08-14 14:31:12 +02:00
|
|
|
$: name = isRoot ? "Screen" : instance._instanceName
|
2022-05-23 13:22:42 +02:00
|
|
|
$: icon = definition?.icon
|
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-11-26 14:25:02 +01:00
|
|
|
$: inSelectedPath = $componentStore.selectedComponentPath?.includes(id)
|
2021-11-16 14:17:34 +01:00
|
|
|
$: inDragPath = inSelectedPath && $builderStore.editMode
|
2022-10-14 19:16:19 +02:00
|
|
|
$: inDndPath = $dndComponentPath?.includes(id)
|
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
|
|
|
|
$: hasChildren = !!definition?.hasChildren
|
2021-11-18 21:38:55 +01:00
|
|
|
$: showEmptyState = definition?.showEmptyState !== false
|
2022-06-13 13:09:29 +02:00
|
|
|
$: hasMissingRequiredSettings = missingRequiredSettings?.length > 0
|
2022-06-14 16:34:33 +02:00
|
|
|
$: editable = !!definition?.editable && !hasMissingRequiredSettings
|
2023-03-28 22:11:33 +02:00
|
|
|
$: requiredAncestors = definition?.requiredAncestors || []
|
|
|
|
$: missingRequiredAncestors = requiredAncestors.filter(
|
|
|
|
ancestor => !$component.ancestors.includes(`${BudibasePrefix}${ancestor}`)
|
|
|
|
)
|
|
|
|
$: hasMissingRequiredAncestors = missingRequiredAncestors?.length > 0
|
|
|
|
$: errorState = hasMissingRequiredSettings || hasMissingRequiredAncestors
|
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-11-26 14:25:02 +01:00
|
|
|
$: builderInteractive =
|
2022-10-06 10:17:26 +02:00
|
|
|
$builderStore.inBuilder && insideScreenslot && !isBlock && !instance.static
|
2021-11-26 14:25:02 +01:00
|
|
|
$: devToolsInteractive = $devToolsStore.allowSelection && !isBlock
|
2023-08-14 14:31:12 +02:00
|
|
|
$: interactive = !isRoot && (builderInteractive || devToolsInteractive)
|
2021-10-28 13:43:31 +02:00
|
|
|
$: editing = editable && selected && $builderStore.editMode
|
2022-03-08 17:41:21 +01:00
|
|
|
$: draggable =
|
|
|
|
!inDragPath &&
|
|
|
|
interactive &&
|
|
|
|
!isLayout &&
|
2023-08-14 14:31:12 +02:00
|
|
|
!isRoot &&
|
2023-09-05 12:02:10 +02:00
|
|
|
!isBlock &&
|
2022-03-08 17:41:21 +01:00
|
|
|
definition?.draggable !== false
|
2022-10-07 09:05:44 +02:00
|
|
|
$: droppable = interactive
|
2022-07-15 11:46:37 +02:00
|
|
|
$: builderHidden =
|
|
|
|
$builderStore.inBuilder && $builderStore.hiddenComponentIds?.includes(id)
|
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.
|
2022-09-12 09:50:22 +02:00
|
|
|
$: empty =
|
2022-10-14 19:59:32 +02:00
|
|
|
!isBlock &&
|
|
|
|
((interactive && !children.length && hasChildren) ||
|
|
|
|
hasMissingRequiredSettings)
|
2021-11-18 21:32:42 +01:00
|
|
|
$: emptyState = empty && showEmptyState
|
2021-11-08 15:35:58 +01:00
|
|
|
|
|
|
|
// Evaluate conditional UI settings and store any component setting changes
|
2023-02-22 14:22:59 +01:00
|
|
|
// which need to be made
|
2022-02-01 17:32:37 +01:00
|
|
|
$: evaluateConditions(conditions)
|
2021-11-08 15:35:58 +01:00
|
|
|
|
2022-02-01 17:32:37 +01:00
|
|
|
// Determine and apply settings to the component
|
|
|
|
$: applySettings(staticSettings, enrichedSettings, conditionalSettings)
|
2021-11-18 21:32:42 +01:00
|
|
|
|
2022-09-05 17:35:25 +02:00
|
|
|
// Determine custom css.
|
|
|
|
// Broken out as a separate variable to minimize reactivity updates.
|
|
|
|
$: customCSS = cachedSettings?._css
|
|
|
|
|
2022-05-17 15:10:21 +02:00
|
|
|
// Scroll the selected element into view
|
|
|
|
$: selected && scrollIntoView()
|
|
|
|
|
2022-10-14 16:45:02 +02:00
|
|
|
// When dragging and dropping, pad components to allow dropping between
|
|
|
|
// nested layers. Only reset this when dragging stops.
|
|
|
|
let pad = false
|
|
|
|
$: pad = pad || (interactive && hasChildren && inDndPath)
|
2022-10-18 19:29:21 +02:00
|
|
|
$: $dndIsDragging, (pad = false)
|
2022-10-14 16:45:02 +02:00
|
|
|
|
2024-04-05 16:59:28 +02:00
|
|
|
$: currentTheme = $context?.device?.theme
|
|
|
|
$: darkMode = !currentTheme?.includes("light")
|
2022-10-14 16:45:02 +02:00
|
|
|
|
2024-08-02 17:13:49 +02:00
|
|
|
$: normalStyles = {
|
2024-07-31 11:35:57 +02:00
|
|
|
...instance._styles?.normal,
|
2024-07-30 16:20:59 +02:00
|
|
|
...ephemeralStyles,
|
2024-07-31 14:58:29 +02:00
|
|
|
}
|
2024-08-02 17:13:49 +02:00
|
|
|
$: gridMetadata = {
|
|
|
|
id,
|
|
|
|
interactive,
|
|
|
|
styles: normalStyles,
|
|
|
|
draggable,
|
|
|
|
definition,
|
|
|
|
errored: errorState,
|
|
|
|
}
|
2024-07-30 16:20:59 +02:00
|
|
|
|
2020-11-24 12:02:10 +01:00
|
|
|
// Update component context
|
2021-11-26 14:25:02 +01:00
|
|
|
$: store.set({
|
2021-01-22 12:08:42 +01:00
|
|
|
id,
|
|
|
|
children: children.length,
|
2022-10-24 14:28:22 +02:00
|
|
|
styles: {
|
|
|
|
...instance._styles,
|
2024-08-02 17:13:49 +02:00
|
|
|
normal: normalStyles,
|
2022-10-24 14:28:22 +02:00
|
|
|
custom: customCSS,
|
|
|
|
id,
|
|
|
|
empty: emptyState,
|
2023-03-28 22:11:33 +02:00
|
|
|
selected,
|
2022-10-24 14:28:22 +02:00
|
|
|
interactive,
|
|
|
|
draggable,
|
|
|
|
editable,
|
2023-09-05 12:01:01 +02:00
|
|
|
isBlock,
|
2022-10-24 14:28:22 +02:00
|
|
|
},
|
2021-09-20 16:34:51 +02:00
|
|
|
empty: emptyState,
|
2021-06-08 09:00:54 +02:00
|
|
|
selected,
|
2022-11-10 15:34:23 +01:00
|
|
|
inSelectedPath,
|
2021-06-11 09:05:49 +02:00
|
|
|
name,
|
2021-10-28 13:43:31 +02:00
|
|
|
editing,
|
2022-05-30 13:57:10 +02:00
|
|
|
type: instance._component,
|
2023-03-28 22:11:33 +02:00
|
|
|
errorState,
|
|
|
|
parent: id,
|
2023-07-05 19:00:50 +02:00
|
|
|
ancestors: [...($component?.ancestors ?? []), instance._component],
|
2024-01-22 12:10:03 +01:00
|
|
|
path: [...($component?.path ?? []), id],
|
2024-04-05 16:59:28 +02:00
|
|
|
darkMode,
|
2021-01-22 12:08:42 +01:00
|
|
|
})
|
2020-11-25 10:50:51 +01:00
|
|
|
|
2022-08-11 18:05:42 +02:00
|
|
|
const initialise = (instance, force = false) => {
|
2022-01-31 19:54:04 +01:00
|
|
|
if (instance == null) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure we're processing a new instance
|
2024-01-22 12:10:03 +01:00
|
|
|
const stringifiedInstance = JSON.stringify(instance)
|
|
|
|
const instanceKey = Helpers.hashString(stringifiedInstance)
|
2022-08-11 18:05:42 +02:00
|
|
|
if (instanceKey === lastInstanceKey && !force) {
|
2022-01-31 19:54:04 +01:00
|
|
|
return
|
|
|
|
} else {
|
|
|
|
lastInstanceKey = instanceKey
|
|
|
|
}
|
|
|
|
|
|
|
|
// Pull definition and constructor
|
2022-05-17 15:33:12 +02:00
|
|
|
const component = instance._component
|
2022-08-10 16:34:00 +02:00
|
|
|
constructor = componentStore.actions.getComponentConstructor(component)
|
2022-05-17 15:33:12 +02:00
|
|
|
definition = componentStore.actions.getComponentDefinition(component)
|
2022-01-31 19:54:04 +01:00
|
|
|
if (!definition) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-03-21 15:30:18 +01:00
|
|
|
const cacheId = `${definition.name}${
|
|
|
|
definition?.deprecated === true ? "_deprecated" : ""
|
|
|
|
}`
|
2022-01-31 19:54:04 +01:00
|
|
|
// Get the settings definition for this component, and cache it
|
2024-03-21 15:30:18 +01:00
|
|
|
if (SettingsDefinitionCache[cacheId]) {
|
|
|
|
settingsDefinition = SettingsDefinitionCache[cacheId]
|
|
|
|
settingsDefinitionMap = SettingsDefinitionMapCache[cacheId]
|
2022-01-31 19:54:04 +01:00
|
|
|
} else {
|
|
|
|
settingsDefinition = getSettingsDefinition(definition)
|
2022-03-15 12:16:51 +01:00
|
|
|
settingsDefinitionMap = getSettingsDefinitionMap(settingsDefinition)
|
2024-03-21 15:30:18 +01:00
|
|
|
SettingsDefinitionCache[cacheId] = settingsDefinition
|
|
|
|
SettingsDefinitionMapCache[cacheId] = settingsDefinitionMap
|
2022-01-31 19:54:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Parse the instance settings, and cache them
|
|
|
|
let instanceSettings
|
|
|
|
if (InstanceSettingsCache[instanceKey]) {
|
|
|
|
instanceSettings = InstanceSettingsCache[instanceKey]
|
|
|
|
} else {
|
|
|
|
instanceSettings = getInstanceSettings(instance, settingsDefinition)
|
|
|
|
InstanceSettingsCache[instanceKey] = instanceSettings
|
|
|
|
}
|
|
|
|
|
2022-02-01 17:32:37 +01:00
|
|
|
// Update the settings types
|
2022-01-31 19:54:04 +01:00
|
|
|
staticSettings = instanceSettings.staticSettings
|
|
|
|
dynamicSettings = instanceSettings.dynamicSettings
|
2022-02-01 17:32:37 +01:00
|
|
|
|
2022-06-13 13:09:29 +02:00
|
|
|
// Check if we have any missing required settings
|
2024-04-17 11:09:31 +02:00
|
|
|
missingRequiredSettings = settingsDefinition.filter(setting => {
|
2022-06-15 11:17:34 +02:00
|
|
|
let empty = instance[setting.key] == null || instance[setting.key] === ""
|
|
|
|
let missing = setting.required && empty
|
|
|
|
|
|
|
|
// Check if this setting depends on another, as it may not be required
|
|
|
|
if (setting.dependsOn) {
|
|
|
|
const dependsOnKey = setting.dependsOn.setting || setting.dependsOn
|
|
|
|
const dependsOnValue = setting.dependsOn.value
|
|
|
|
const realDependentValue = instance[dependsOnKey]
|
2024-04-17 11:09:31 +02:00
|
|
|
|
2024-04-22 09:50:33 +02:00
|
|
|
const sectionDependsOnKey =
|
|
|
|
setting.sectionDependsOn?.setting || setting.sectionDependsOn
|
2024-04-17 11:09:31 +02:00
|
|
|
const sectionDependsOnValue = setting.sectionDependsOn?.value
|
|
|
|
const sectionRealDependentValue = instance[sectionDependsOnKey]
|
|
|
|
|
|
|
|
if (dependsOnValue == null && realDependentValue == null) {
|
|
|
|
return false
|
2024-04-16 11:27:52 +02:00
|
|
|
}
|
2024-04-17 11:09:31 +02:00
|
|
|
if (dependsOnValue != null && dependsOnValue !== realDependentValue) {
|
|
|
|
return false
|
2024-03-27 09:41:27 +01:00
|
|
|
}
|
2024-04-17 11:09:31 +02:00
|
|
|
|
2024-04-22 09:50:33 +02:00
|
|
|
if (
|
|
|
|
sectionDependsOnValue != null &&
|
|
|
|
sectionDependsOnValue !== sectionRealDependentValue
|
|
|
|
) {
|
2022-06-15 11:17:34 +02:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return missing
|
2022-06-13 13:09:29 +02:00
|
|
|
})
|
|
|
|
|
2024-01-22 12:10:03 +01:00
|
|
|
// When considering bindings we can ignore children, so we remove that
|
|
|
|
// before storing the reference stringified version
|
|
|
|
const noChildren = JSON.stringify({ ...instance, _children: null })
|
|
|
|
const bindings = findHBSBlocks(noChildren).map(binding => {
|
|
|
|
let sanitizedBinding = binding.replace(/\\"/g, '"')
|
|
|
|
if (isJSBinding(sanitizedBinding)) {
|
|
|
|
return decodeJSBinding(sanitizedBinding)
|
|
|
|
} else {
|
|
|
|
return sanitizedBinding
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// The known context key map is built up at runtime, as changes to keys are
|
|
|
|
// encountered. We manually seed this to the required action keys as these
|
|
|
|
// are not encountered at runtime and so need computed in advance.
|
|
|
|
knownContextKeyMap = generateActionKeyMap(instance, settingsDefinition)
|
|
|
|
bindingString = bindings.join(" ")
|
|
|
|
|
2023-08-24 16:50:57 +02:00
|
|
|
// Run any migrations
|
|
|
|
runMigrations(instance, settingsDefinition)
|
|
|
|
|
2022-02-01 17:32:37 +01:00
|
|
|
// Force an initial enrichment of the new settings
|
2024-01-22 12:10:03 +01:00
|
|
|
enrichComponentSettings(get(context), settingsDefinitionMap)
|
2024-02-09 17:44:11 +01:00
|
|
|
|
|
|
|
// Start observing changes in context now that we are initialised
|
|
|
|
if (!unobserve) {
|
|
|
|
unobserve = context.actions.observeChanges(handleContextChange)
|
|
|
|
}
|
2024-01-22 12:10:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Extracts a map of all context keys which are required by action settings
|
|
|
|
// to provide the functions to evaluate at runtime. This needs done manually
|
|
|
|
// as the action definitions themselves do not specify bindings for action
|
|
|
|
// keys, meaning we cannot do this while doing the other normal bindings.
|
|
|
|
const generateActionKeyMap = (instance, settingsDefinition) => {
|
|
|
|
let map = {}
|
|
|
|
settingsDefinition.forEach(setting => {
|
|
|
|
if (setting.type === "event") {
|
|
|
|
instance[setting.key]?.forEach(action => {
|
|
|
|
// We depend on the actual action key
|
|
|
|
const actionKey = getActionContextKey(action)
|
|
|
|
if (actionKey) {
|
|
|
|
map[actionKey] = true
|
|
|
|
}
|
|
|
|
|
|
|
|
// We also depend on any manually declared context keys
|
|
|
|
getActionDependentContextKeys(action)?.forEach(key => {
|
|
|
|
map[key] = true
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
2023-09-15 12:52:35 +02:00
|
|
|
})
|
2024-01-22 12:10:03 +01:00
|
|
|
return map
|
2021-06-25 16:04:27 +02:00
|
|
|
}
|
|
|
|
|
2023-08-24 16:50:57 +02:00
|
|
|
const runMigrations = (instance, settingsDefinition) => {
|
|
|
|
settingsDefinition.forEach(setting => {
|
|
|
|
// Migrate "table" settings to ensure they have a type and resource ID
|
|
|
|
if (setting.type === "table") {
|
|
|
|
const val = instance[setting.key]
|
|
|
|
if (val) {
|
|
|
|
if (!val.type) {
|
|
|
|
val.type = "table"
|
|
|
|
}
|
|
|
|
if (!val.resourceId) {
|
|
|
|
if (val.type === "viewV2") {
|
2023-08-30 16:46:48 +02:00
|
|
|
val.resourceId = val.id
|
2023-08-24 16:50:57 +02:00
|
|
|
} else {
|
|
|
|
val.resourceId = val.tableId
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-03-15 12:16:51 +01:00
|
|
|
const getSettingsDefinitionMap = settingsDefinition => {
|
|
|
|
let map = {}
|
|
|
|
settingsDefinition?.forEach(setting => {
|
|
|
|
map[setting.key] = setting
|
|
|
|
})
|
|
|
|
return map
|
|
|
|
}
|
|
|
|
|
2022-01-31 19:54:04 +01:00
|
|
|
const getInstanceSettings = (instance, settingsDefinition) => {
|
|
|
|
// Get raw settings
|
|
|
|
let settings = {}
|
|
|
|
Object.entries(instance)
|
2022-09-05 17:35:25 +02:00
|
|
|
.filter(([name]) => !name.startsWith("_"))
|
2022-01-31 19:54:04 +01:00
|
|
|
.forEach(([key, value]) => {
|
|
|
|
settings[key] = value
|
|
|
|
})
|
2022-01-29 19:53:21 +01:00
|
|
|
let newStaticSettings = { ...settings }
|
|
|
|
let newDynamicSettings = { ...settings }
|
2022-09-05 17:35:25 +02:00
|
|
|
|
2023-02-22 14:22:59 +01:00
|
|
|
// Attach some internal properties which we assume always need enriched
|
2022-09-05 17:35:25 +02:00
|
|
|
newDynamicSettings["_conditions"] = instance._conditions
|
|
|
|
newDynamicSettings["_css"] = instance._styles?.custom
|
|
|
|
|
|
|
|
// Derive static, dynamic and nested settings if the instance changed
|
2021-11-08 15:35:58 +01:00
|
|
|
settingsDefinition?.forEach(setting => {
|
2021-11-16 17:29:31 +01:00
|
|
|
if (setting.nested) {
|
2022-01-29 19:53:21 +01:00
|
|
|
delete newDynamicSettings[setting.key]
|
2021-11-16 17:29:31 +01:00
|
|
|
} else {
|
2022-01-31 19:54:04 +01:00
|
|
|
const value = settings[setting.key]
|
2022-01-29 19:53:21 +01:00
|
|
|
if (value == null) {
|
|
|
|
delete newDynamicSettings[setting.key]
|
|
|
|
} else if (typeof value === "string" && value.includes("{{")) {
|
2022-02-01 17:32:37 +01:00
|
|
|
// Strings can be trivially checked
|
2022-01-29 19:53:21 +01:00
|
|
|
delete newStaticSettings[setting.key]
|
2022-03-15 12:16:51 +01:00
|
|
|
} else if (setting.type === "event") {
|
2022-01-31 19:54:04 +01:00
|
|
|
// Always treat button actions as dynamic
|
|
|
|
delete newStaticSettings[setting.key]
|
2022-01-29 19:53:21 +01:00
|
|
|
} else if (typeof value === "object") {
|
2022-02-01 17:32:37 +01:00
|
|
|
// Stringify and check objects
|
2022-01-29 19:53:21 +01:00
|
|
|
const stringified = JSON.stringify(value)
|
|
|
|
if (stringified.includes("{{")) {
|
|
|
|
delete newStaticSettings[setting.key]
|
|
|
|
} else {
|
|
|
|
delete newDynamicSettings[setting.key]
|
|
|
|
}
|
|
|
|
} else {
|
2022-02-01 17:32:37 +01:00
|
|
|
// For other types, we can safely assume they are static
|
2022-01-29 19:53:21 +01:00
|
|
|
delete newDynamicSettings[setting.key]
|
|
|
|
}
|
2021-11-08 15:35:58 +01:00
|
|
|
}
|
|
|
|
})
|
2022-01-29 19:53:21 +01:00
|
|
|
|
2022-01-31 19:54:04 +01:00
|
|
|
return {
|
|
|
|
staticSettings: newStaticSettings,
|
|
|
|
dynamicSettings: newDynamicSettings,
|
|
|
|
}
|
2021-11-08 15:35:58 +01:00
|
|
|
}
|
|
|
|
|
2023-02-22 14:22:59 +01:00
|
|
|
// Generates the array of conditional UI expressions, accounting for both
|
|
|
|
// nested and non-nested settings, extracting a mixture of values from both
|
|
|
|
// the un-enriched and enriched settings
|
|
|
|
const generateConditions = () => {
|
|
|
|
if (!enrichedSettings?._conditions) {
|
|
|
|
conditions = []
|
|
|
|
return
|
|
|
|
}
|
|
|
|
conditions = enrichedSettings._conditions.map(condition => {
|
|
|
|
const raw = instance._conditions?.find(x => x.id === condition.id)
|
|
|
|
if (settingsDefinitionMap[condition.setting]?.nested && raw) {
|
|
|
|
return { ...condition, settingValue: raw.settingValue }
|
|
|
|
} else {
|
|
|
|
return condition
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-01-29 14:22:38 +01:00
|
|
|
// Enriches any string component props using handlebars
|
2024-01-22 12:10:03 +01:00
|
|
|
const enrichComponentSettings = (context, settingsDefinitionMap) => {
|
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
|
2022-03-15 12:16:51 +01:00
|
|
|
const newEnrichedSettings = enrichProps(
|
|
|
|
dynamicSettings,
|
|
|
|
context,
|
|
|
|
settingsDefinitionMap
|
|
|
|
)
|
2021-01-29 14:22:38 +01:00
|
|
|
|
|
|
|
// Abandon this update if a newer update has started
|
|
|
|
if (enrichmentTime !== latestUpdateTime) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-02-22 14:22:59 +01:00
|
|
|
// Store new enriched settings
|
2021-11-16 17:29:31 +01:00
|
|
|
enrichedSettings = newEnrichedSettings
|
2023-02-22 14:22:59 +01:00
|
|
|
|
|
|
|
// Once settings have been enriched, re-evaluate conditions
|
|
|
|
generateConditions()
|
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.
|
2022-02-01 17:32:37 +01:00
|
|
|
const applySettings = (
|
|
|
|
staticSettings,
|
|
|
|
enrichedSettings,
|
|
|
|
conditionalSettings
|
|
|
|
) => {
|
2022-01-29 19:53:21 +01:00
|
|
|
const allSettings = {
|
|
|
|
...staticSettings,
|
2022-02-01 17:32:37 +01:00
|
|
|
...enrichedSettings,
|
|
|
|
...conditionalSettings,
|
2022-01-29 19:53:21 +01:00
|
|
|
}
|
2021-11-16 17:29:31 +01:00
|
|
|
if (!cachedSettings) {
|
2021-12-17 09:22:40 +01:00
|
|
|
cachedSettings = { ...allSettings }
|
2021-12-17 10:18:07 +01:00
|
|
|
initialSettings = cachedSettings
|
2021-11-16 17:29:31 +01:00
|
|
|
} else {
|
|
|
|
Object.keys(allSettings).forEach(key => {
|
2021-12-17 09:22:40 +01:00
|
|
|
const same = propsAreSame(allSettings[key], cachedSettings[key])
|
|
|
|
if (!same) {
|
2022-01-21 14:32:56 +01:00
|
|
|
// Updated cachedSettings (which is assigned by reference to
|
|
|
|
// initialSettings) so that if we remount the component then the
|
|
|
|
// initial props are up to date. By setting it this way rather than
|
|
|
|
// setting it on initialSettings directly, we avoid a double render.
|
2021-11-16 17:29:31 +01:00
|
|
|
cachedSettings[key] = allSettings[key]
|
2022-01-21 14:32:56 +01:00
|
|
|
|
2022-09-05 17:35:25 +02:00
|
|
|
// Don't update components for internal properties
|
|
|
|
if (key.startsWith("_")) {
|
|
|
|
return
|
2022-08-22 17:45:59 +02:00
|
|
|
}
|
2022-01-21 14:32:56 +01:00
|
|
|
|
2022-01-25 12:22:26 +01:00
|
|
|
if (ref?.$$set) {
|
|
|
|
// Programmatically set the prop to avoid svelte reactive statements
|
|
|
|
// firing inside components. This circumvents the problems caused by
|
|
|
|
// spreading a props object.
|
|
|
|
ref.$$set({ [key]: allSettings[key] })
|
|
|
|
} else {
|
|
|
|
// Sometimes enrichment can occur multiple times before the
|
|
|
|
// component has mounted and been assigned a ref.
|
|
|
|
// In these cases, for some reason we need to update the
|
|
|
|
// initial settings object, even though it is equivalent by
|
|
|
|
// reference to cached settings. This solves the problem of multiple
|
|
|
|
// initial enrichments, while also not causing wasted renders for
|
|
|
|
// any components not affected by this issue.
|
|
|
|
initialSettings[key] = allSettings[key]
|
|
|
|
}
|
2021-11-16 17:29:31 +01:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2021-11-18 21:32:42 +01:00
|
|
|
|
2022-05-17 15:10:21 +02:00
|
|
|
const scrollIntoView = () => {
|
2022-10-14 20:45:47 +02:00
|
|
|
// Don't scroll into view if we selected this component because we were
|
|
|
|
// starting dragging on it
|
2022-10-21 17:52:46 +02:00
|
|
|
if (get(dndIsDragging)) {
|
2022-10-14 20:45:47 +02:00
|
|
|
return
|
|
|
|
}
|
2022-07-15 14:47:39 +02:00
|
|
|
const node = document.getElementsByClassName(id)?.[0]?.children[0]
|
2022-05-17 15:10:21 +02:00
|
|
|
if (!node) {
|
|
|
|
return
|
|
|
|
}
|
2022-05-17 15:34:54 +02:00
|
|
|
node.style.scrollMargin = "100px"
|
2022-05-17 15:10:21 +02:00
|
|
|
node.scrollIntoView({
|
|
|
|
behavior: "smooth",
|
2023-08-22 19:55:36 +02:00
|
|
|
block: "nearest",
|
2022-05-17 15:10:21 +02:00
|
|
|
inline: "start",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-01-22 12:10:03 +01:00
|
|
|
const handleContextChange = key => {
|
|
|
|
// Check if we already know if this key is used
|
|
|
|
let used = knownContextKeyMap[key]
|
|
|
|
|
|
|
|
// If we don't know, check and cache
|
|
|
|
if (used == null) {
|
2024-03-14 17:16:24 +01:00
|
|
|
const searchString = key === "snippets" ? key : `[${key}]`
|
|
|
|
used = bindingString.indexOf(searchString) !== -1
|
2024-01-22 12:10:03 +01:00
|
|
|
knownContextKeyMap[key] = used
|
2022-02-24 22:48:54 +01:00
|
|
|
}
|
2021-11-26 14:25:02 +01:00
|
|
|
|
2024-01-22 12:10:03 +01:00
|
|
|
// Enrich settings if we use this key
|
|
|
|
if (used) {
|
|
|
|
enrichComponentSettings($context, settingsDefinitionMap)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-16 14:25:41 +01:00
|
|
|
const getDataContext = () => {
|
|
|
|
const normalContext = get(context)
|
|
|
|
const additionalContext = ref?.getAdditionalDataContext?.()
|
|
|
|
return {
|
|
|
|
...normalContext,
|
|
|
|
...additionalContext,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-22 12:10:03 +01:00
|
|
|
onMount(() => {
|
2024-02-09 17:44:11 +01:00
|
|
|
// Register this component instance for external access
|
2024-01-22 12:10:03 +01:00
|
|
|
if ($appStore.isDevApp) {
|
|
|
|
if (!componentStore.actions.isComponentRegistered(id)) {
|
|
|
|
componentStore.actions.registerInstance(id, {
|
|
|
|
component: instance._component,
|
|
|
|
getSettings: () => cachedSettings,
|
|
|
|
getRawSettings: () => ({ ...staticSettings, ...dynamicSettings }),
|
2024-03-11 14:32:27 +01:00
|
|
|
getDataContext,
|
2024-01-22 12:10:03 +01:00
|
|
|
reload: () => initialise(instance, true),
|
|
|
|
setEphemeralStyles: styles => (ephemeralStyles = styles),
|
|
|
|
state: store,
|
|
|
|
})
|
|
|
|
}
|
2024-02-09 17:44:11 +01:00
|
|
|
}
|
|
|
|
return () => {
|
|
|
|
// Unregister component
|
|
|
|
if (componentStore.actions.isComponentRegistered(id)) {
|
|
|
|
componentStore.actions.unregisterInstance(id)
|
2024-01-22 12:10:03 +01:00
|
|
|
}
|
2024-02-09 17:44:11 +01:00
|
|
|
|
|
|
|
// Stop observing context changes
|
|
|
|
unobserve?.()
|
2024-01-22 12:03:05 +01:00
|
|
|
}
|
|
|
|
})
|
2020-11-13 16:42:32 +01:00
|
|
|
</script>
|
|
|
|
|
2023-03-27 15:43:29 +02:00
|
|
|
{#if constructor && initialSettings && (visible || inSelectedPath) && !builderHidden}
|
2022-01-25 12:22:26 +01:00
|
|
|
<!-- The ID is used as a class because getElementsByClassName is O(1) -->
|
|
|
|
<!-- and the performance matters for the selection indicators -->
|
|
|
|
<div
|
2022-02-24 15:14:55 +01:00
|
|
|
class={`component ${id}`}
|
2022-01-25 12:22:26 +01:00
|
|
|
class:draggable
|
|
|
|
class:droppable
|
|
|
|
class:empty
|
|
|
|
class:interactive
|
|
|
|
class:editing
|
2022-10-14 16:45:02 +02:00
|
|
|
class:pad
|
|
|
|
class:parent={hasChildren}
|
2022-01-25 12:22:26 +01:00
|
|
|
class:block={isBlock}
|
2023-03-28 22:11:33 +02:00
|
|
|
class:error={errorState}
|
2022-01-25 12:22:26 +01:00
|
|
|
data-id={id}
|
|
|
|
data-name={name}
|
2022-05-23 13:22:42 +02:00
|
|
|
data-icon={icon}
|
2023-03-28 22:11:33 +02:00
|
|
|
data-parent={$component.id}
|
2024-08-02 17:13:49 +02:00
|
|
|
use:gridLayout={gridMetadata}
|
2022-01-25 12:22:26 +01:00
|
|
|
>
|
2023-03-28 22:11:33 +02:00
|
|
|
{#if errorState}
|
|
|
|
<ComponentErrorState
|
|
|
|
{missingRequiredSettings}
|
|
|
|
{missingRequiredAncestors}
|
|
|
|
/>
|
2023-03-27 15:43:29 +02:00
|
|
|
{:else}
|
|
|
|
<svelte:component this={constructor} bind:this={ref} {...initialSettings}>
|
|
|
|
{#if children.length}
|
|
|
|
{#each children as child (child._id)}
|
2023-03-28 22:11:33 +02:00
|
|
|
<svelte:self instance={child} />
|
2023-03-27 15:43:29 +02:00
|
|
|
{/each}
|
|
|
|
{:else if emptyState}
|
2023-08-14 14:31:12 +02:00
|
|
|
{#if isRoot}
|
2023-03-27 15:43:29 +02:00
|
|
|
<ScreenPlaceholder />
|
|
|
|
{:else}
|
2023-03-28 22:11:33 +02:00
|
|
|
<EmptyPlaceholder />
|
2023-03-27 15:43:29 +02:00
|
|
|
{/if}
|
|
|
|
{:else if isBlock}
|
|
|
|
<slot />
|
2022-06-07 14:41:17 +02:00
|
|
|
{/if}
|
2023-03-27 15:43:29 +02:00
|
|
|
</svelte:component>
|
|
|
|
{/if}
|
2022-01-25 12:22:26 +01:00
|
|
|
</div>
|
|
|
|
{/if}
|
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;
|
|
|
|
}
|
2022-10-14 16:45:02 +02:00
|
|
|
.component.pad :global(> *) {
|
2022-10-25 17:18:33 +02:00
|
|
|
padding: var(--spacing-m) !important;
|
|
|
|
gap: var(--spacing-m) !important;
|
2022-10-14 21:30:58 +02:00
|
|
|
border: 2px dashed var(--spectrum-global-color-gray-400) !important;
|
2022-10-07 09:05:44 +02:00
|
|
|
border-radius: 4px !important;
|
2022-10-17 10:00:55 +02:00
|
|
|
transition: padding 260ms ease-out, border 260ms ease-out;
|
2021-09-16 08:28:59 +02:00
|
|
|
}
|
2022-10-11 09:52:45 +02:00
|
|
|
.interactive :global(*) {
|
|
|
|
cursor: default;
|
2021-10-28 13:43:31 +02:00
|
|
|
}
|
2021-06-08 09:00:54 +02:00
|
|
|
</style>
|