{
if (prop) {
@@ -49,7 +50,4 @@
background-color: rgba(13, 102, 208, 0.1);
color: var(--spectrum-global-color-blue-600);
}
- .rotate {
- transform: rotate(90deg);
- }
diff --git a/packages/client/src/components/preview/SettingsColorPicker.svelte b/packages/client/src/components/preview/SettingsColorPicker.svelte
index b078d048d2..a292d7d838 100644
--- a/packages/client/src/components/preview/SettingsColorPicker.svelte
+++ b/packages/client/src/components/preview/SettingsColorPicker.svelte
@@ -1,10 +1,11 @@
diff --git a/packages/client/src/components/preview/SettingsPicker.svelte b/packages/client/src/components/preview/SettingsPicker.svelte
index 8b83729fde..3900d065e8 100644
--- a/packages/client/src/components/preview/SettingsPicker.svelte
+++ b/packages/client/src/components/preview/SettingsPicker.svelte
@@ -1,12 +1,13 @@
diff --git a/packages/client/src/constants.js b/packages/client/src/constants.js
index d3f1ab8be9..f7e3e86d40 100644
--- a/packages/client/src/constants.js
+++ b/packages/client/src/constants.js
@@ -15,3 +15,6 @@ export const ActionTypes = {
export const DNDPlaceholderID = "dnd-placeholder"
export const ScreenslotType = "screenslot"
+export const GridRowHeight = 24
+export const GridColumns = 12
+export const GridSpacing = 4
diff --git a/packages/client/src/stores/builder.js b/packages/client/src/stores/builder.js
index 5440fc3a79..faa37eddca 100644
--- a/packages/client/src/stores/builder.js
+++ b/packages/client/src/stores/builder.js
@@ -41,13 +41,20 @@ const createBuilderStore = () => {
eventStore.actions.dispatchEvent("update-prop", { prop, value })
},
updateStyles: async (styles, id) => {
- await eventStore.actions.dispatchEvent("update-styles", { styles, id })
+ await eventStore.actions.dispatchEvent("update-styles", {
+ styles,
+ id,
+ })
},
keyDown: (key, ctrlKey) => {
eventStore.actions.dispatchEvent("key-down", { key, ctrlKey })
},
- duplicateComponent: id => {
- eventStore.actions.dispatchEvent("duplicate-component", { id })
+ duplicateComponent: (id, mode = "below", selectComponent = true) => {
+ eventStore.actions.dispatchEvent("duplicate-component", {
+ id,
+ mode,
+ selectComponent,
+ })
},
deleteComponent: id => {
eventStore.actions.dispatchEvent("delete-component", { id })
diff --git a/packages/client/src/stores/components.js b/packages/client/src/stores/components.js
index b7f7d98197..d4afa6c7f1 100644
--- a/packages/client/src/stores/components.js
+++ b/packages/client/src/stores/components.js
@@ -142,9 +142,6 @@ const createComponentStore = () => {
}
const getComponentInstance = id => {
- if (!id) {
- return null
- }
return derived(store, $store => $store.mountedComponents[id])
}
diff --git a/packages/client/src/stores/screens.js b/packages/client/src/stores/screens.js
index 9cb0d536de..3c5ece0a6c 100644
--- a/packages/client/src/stores/screens.js
+++ b/packages/client/src/stores/screens.js
@@ -129,29 +129,30 @@ const createScreenStore = () => {
// If we don't have a legacy custom layout, build a layout structure
// from the screen navigation settings
if (!activeLayout) {
- let navigationSettings = {
+ let layoutSettings = {
navigation: "None",
pageWidth: activeScreen?.width || "Large",
+ embedded: $appStore.embedded,
}
if (activeScreen?.showNavigation) {
- navigationSettings = {
- ...navigationSettings,
+ layoutSettings = {
+ ...layoutSettings,
...($builderStore.navigation || $appStore.application?.navigation),
}
// Default navigation to top
- if (!navigationSettings.navigation) {
- navigationSettings.navigation = "Top"
+ if (!layoutSettings.navigation) {
+ layoutSettings.navigation = "Top"
}
// Default title to app name
- if (!navigationSettings.title && !navigationSettings.hideTitle) {
- navigationSettings.title = $appStore.application?.name
+ if (!layoutSettings.title && !layoutSettings.hideTitle) {
+ layoutSettings.title = $appStore.application?.name
}
// Default to the org logo
- if (!navigationSettings.logoUrl) {
- navigationSettings.logoUrl = $orgStore?.logoUrl
+ if (!layoutSettings.logoUrl) {
+ layoutSettings.logoUrl = $orgStore?.logoUrl
}
}
activeLayout = {
@@ -173,8 +174,7 @@ const createScreenStore = () => {
},
},
],
- ...navigationSettings,
- embedded: $appStore.embedded,
+ ...layoutSettings,
},
}
}
diff --git a/packages/client/src/utils/domDebounce.js b/packages/client/src/utils/domDebounce.js
deleted file mode 100644
index b15d2698b4..0000000000
--- a/packages/client/src/utils/domDebounce.js
+++ /dev/null
@@ -1,14 +0,0 @@
-export const domDebounce = (callback, extractParams = x => x) => {
- let active = false
- let lastParams
- return (...params) => {
- lastParams = extractParams(...params)
- if (!active) {
- active = true
- requestAnimationFrame(() => {
- callback(lastParams)
- active = false
- })
- }
- }
-}
diff --git a/packages/client/src/utils/grid.js b/packages/client/src/utils/grid.js
new file mode 100644
index 0000000000..1727b904ca
--- /dev/null
+++ b/packages/client/src/utils/grid.js
@@ -0,0 +1,183 @@
+import { GridSpacing, GridRowHeight } from "constants"
+import { builderStore } from "stores"
+import { buildStyleString } from "utils/styleable.js"
+
+/**
+ * We use CSS variables on components to control positioning and layout of
+ * components inside grids.
+ * --grid-[mobile/desktop]-[row/col]-[start-end]: for positioning
+ * --grid-[mobile/desktop]-[h/v]-align: for layout of inner components within
+ * the components grid bounds
+ *
+ * Component definitions define their default layout preference via the
+ * `grid.hAlign` and `grid.vAlign` keys in the manifest.
+ *
+ * We also apply grid-[mobile/desktop]-grow CSS classes to component wrapper
+ * DOM nodes to use later in selectors, to control the sizing of children.
+ */
+
+// Enum representing the different CSS variables we use for grid metadata
+export const GridParams = {
+ HAlign: "h-align",
+ VAlign: "v-align",
+ ColStart: "col-start",
+ ColEnd: "col-end",
+ RowStart: "row-start",
+ RowEnd: "row-end",
+}
+
+// Classes used in selectors inside grid containers to control child styles
+export const GridClasses = {
+ DesktopFill: "grid-desktop-grow",
+ MobileFill: "grid-mobile-grow",
+}
+
+// Enum for device preview type, included in grid CSS variables
+export const Devices = {
+ Desktop: "desktop",
+ Mobile: "mobile",
+}
+
+export const GridDragModes = {
+ Resize: "resize",
+ Move: "move",
+}
+
+// Builds a CSS variable name for a certain piece of grid metadata
+export const getGridVar = (device, param) => `--grid-${device}-${param}`
+
+// Determines whether a JS event originated from immediately within a grid
+export const isGridEvent = e => {
+ return (
+ e.target.dataset?.indicator === "true" ||
+ e.target
+ .closest?.(".component")
+ ?.parentNode.closest(".component")
+ ?.childNodes[0]?.classList?.contains("grid")
+ )
+}
+
+// Svelte action to apply required class names and styles to our component
+// wrappers
+export const gridLayout = (node, metadata) => {
+ let selectComponent
+
+ // Applies the required listeners, CSS and classes to a component DOM node
+ const applyMetadata = metadata => {
+ const {
+ id,
+ styles,
+ interactive,
+ errored,
+ definition,
+ draggable,
+ insideGrid,
+ ignoresLayout,
+ } = metadata
+ if (!insideGrid) {
+ return
+ }
+
+ // If this component ignores layout, flag it as such so that we can avoid
+ // selecting it later
+ if (ignoresLayout) {
+ node.classList.add("ignores-layout")
+ return
+ }
+
+ // Callback to select the component when clicking on the wrapper
+ selectComponent = e => {
+ e.stopPropagation()
+ builderStore.actions.selectComponent(id)
+ }
+
+ // Determine default width and height of component
+ let width = errored ? 500 : definition?.size?.width || 200
+ let height = errored ? 60 : definition?.size?.height || 200
+ width += 2 * GridSpacing
+ height += 2 * GridSpacing
+ let vars = {
+ "--default-width": width,
+ "--default-height": height,
+ }
+
+ // Generate defaults for all grid params
+ const defaults = {
+ [GridParams.HAlign]: definition?.grid?.hAlign || "stretch",
+ [GridParams.VAlign]: definition?.grid?.vAlign || "center",
+ [GridParams.ColStart]: 1,
+ [GridParams.ColEnd]:
+ "round(up, calc((var(--grid-spacing) * 2 + var(--default-width)) / var(--col-size) + 1))",
+ [GridParams.RowStart]: 1,
+ [GridParams.RowEnd]: Math.max(2, Math.ceil(height / GridRowHeight) + 1),
+ }
+
+ // Specify values for all grid params for all devices, and strip these CSS
+ // variables from the styles being applied to the inner component, as we
+ // want to apply these to the wrapper instead
+ for (let param of Object.values(GridParams)) {
+ let dVar = getGridVar(Devices.Desktop, param)
+ let mVar = getGridVar(Devices.Mobile, param)
+ vars[dVar] = styles[dVar] ?? styles[mVar] ?? defaults[param]
+ vars[mVar] = styles[mVar] ?? styles[dVar] ?? defaults[param]
+ }
+
+ // Apply some overrides depending on component state
+ if (errored) {
+ vars[getGridVar(Devices.Desktop, GridParams.HAlign)] = "stretch"
+ vars[getGridVar(Devices.Mobile, GridParams.HAlign)] = "stretch"
+ vars[getGridVar(Devices.Desktop, GridParams.VAlign)] = "stretch"
+ vars[getGridVar(Devices.Mobile, GridParams.VAlign)] = "stretch"
+ }
+
+ // Apply some metadata to data attributes to speed up lookups
+ const addDataTag = (tagName, device, param) => {
+ const val = `${vars[getGridVar(device, param)]}`
+ if (node.dataset[tagName] !== val) {
+ node.dataset[tagName] = val
+ }
+ }
+ addDataTag("gridDesktopRowEnd", Devices.Desktop, GridParams.RowEnd)
+ addDataTag("gridMobileRowEnd", Devices.Mobile, GridParams.RowEnd)
+ addDataTag("gridDesktopHAlign", Devices.Desktop, GridParams.HAlign)
+ addDataTag("gridMobileHAlign", Devices.Mobile, GridParams.HAlign)
+ addDataTag("gridDesktopVAlign", Devices.Desktop, GridParams.VAlign)
+ addDataTag("gridMobileVAlign", Devices.Mobile, GridParams.VAlign)
+ if (node.dataset.insideGrid !== true) {
+ node.dataset.insideGrid = true
+ }
+
+ // Apply all CSS variables to the wrapper
+ node.style = buildStyleString(vars)
+
+ // Add a listener to select this node on click
+ if (interactive) {
+ node.addEventListener("click", selectComponent, false)
+ }
+
+ // Add draggable attribute
+ node.setAttribute("draggable", !!draggable)
+ }
+
+ // Removes the previously set up listeners
+ const removeListeners = () => {
+ // By checking if this is defined we can avoid trying to remove event
+ // listeners on every component
+ if (selectComponent) {
+ node.removeEventListener("click", selectComponent, false)
+ selectComponent = null
+ }
+ }
+
+ applyMetadata(metadata)
+
+ return {
+ update(newMetadata) {
+ removeListeners()
+ applyMetadata(newMetadata)
+ },
+ destroy() {
+ removeListeners()
+ },
+ }
+}
diff --git a/packages/client/src/utils/styleable.js b/packages/client/src/utils/styleable.js
index 3fccae0be5..0f484a9ab9 100644
--- a/packages/client/src/utils/styleable.js
+++ b/packages/client/src/utils/styleable.js
@@ -3,13 +3,13 @@ import { builderStore } from "stores"
/**
* Helper to build a CSS string from a style object.
*/
-const buildStyleString = (styleObject, customStyles) => {
+export const buildStyleString = (styleObject, customStyles) => {
let str = ""
- Object.entries(styleObject || {}).forEach(([style, value]) => {
- if (style && value != null) {
- str += `${style}: ${value}; `
+ for (let key of Object.keys(styleObject || {})) {
+ if (styleObject[key] != null) {
+ str += `${key}:${styleObject[key]};`
}
- })
+ }
return str + (customStyles || "")
}
diff --git a/packages/frontend-core/src/components/ClientAppSkeleton.svelte b/packages/frontend-core/src/components/ClientAppSkeleton.svelte
index a1c90d2db7..f867fccddb 100644
--- a/packages/frontend-core/src/components/ClientAppSkeleton.svelte
+++ b/packages/frontend-core/src/components/ClientAppSkeleton.svelte
@@ -58,7 +58,6 @@
height: 100%;
display: flex;
flex-direction: column;
- border-radius: 4px;
overflow: hidden;
background-color: var(--spectrum-global-color-gray-200);
}
diff --git a/packages/frontend-core/src/utils/index.js b/packages/frontend-core/src/utils/index.js
index 9eb7206012..5f21e7db99 100644
--- a/packages/frontend-core/src/utils/index.js
+++ b/packages/frontend-core/src/utils/index.js
@@ -9,3 +9,4 @@ export { memo, derivedMemo } from "./memo"
export { createWebsocket } from "./websocket"
export * from "./download"
export * from "./theme"
+export * from "./settings"
diff --git a/packages/frontend-core/src/utils/memo.js b/packages/frontend-core/src/utils/memo.js
index ba0e3f3490..b99af15c2c 100644
--- a/packages/frontend-core/src/utils/memo.js
+++ b/packages/frontend-core/src/utils/memo.js
@@ -4,32 +4,23 @@ import { writable, get, derived } from "svelte/store"
// subscribed children will only fire when a new value is actually set
export const memo = initialValue => {
const store = writable(initialValue)
+ let currentJSON = null
- const tryUpdateValue = (newValue, currentValue) => {
- // Sanity check for primitive equality
- if (currentValue === newValue) {
- return
- }
-
- // Otherwise deep compare via JSON stringify
- const currentString = JSON.stringify(currentValue)
- const newString = JSON.stringify(newValue)
- if (currentString !== newString) {
+ const tryUpdateValue = newValue => {
+ const newJSON = JSON.stringify(newValue)
+ if (newJSON !== currentJSON) {
store.set(newValue)
+ currentJSON = newJSON
}
}
return {
subscribe: store.subscribe,
- set: newValue => {
- const currentValue = get(store)
- tryUpdateValue(newValue, currentValue)
- },
+ set: tryUpdateValue,
update: updateFn => {
- const currentValue = get(store)
- let mutableCurrentValue = JSON.parse(JSON.stringify(currentValue))
+ let mutableCurrentValue = JSON.parse(currentJSON)
const newValue = updateFn(mutableCurrentValue)
- tryUpdateValue(newValue, currentValue)
+ tryUpdateValue(newValue)
},
}
}
diff --git a/packages/frontend-core/src/utils/settings.js b/packages/frontend-core/src/utils/settings.js
new file mode 100644
index 0000000000..0e312c70e6
--- /dev/null
+++ b/packages/frontend-core/src/utils/settings.js
@@ -0,0 +1,43 @@
+import { helpers } from "@budibase/shared-core"
+
+// Util to check if a setting can be rendered for a certain instance, based on
+// the "dependsOn" metadata in the manifest
+export const shouldDisplaySetting = (instance, setting) => {
+ let dependsOn = setting.dependsOn
+ if (dependsOn && !Array.isArray(dependsOn)) {
+ dependsOn = [dependsOn]
+ }
+ if (!dependsOn?.length) {
+ return true
+ }
+
+ // Ensure all conditions are met
+ return dependsOn.every(condition => {
+ let dependantSetting = condition
+ let dependantValues = null
+ let invert = !!condition.invert
+ if (typeof condition === "object") {
+ dependantSetting = condition.setting
+ dependantValues = condition.value
+ }
+ if (!dependantSetting) {
+ return false
+ }
+
+ // Ensure values is an array
+ if (!Array.isArray(dependantValues)) {
+ dependantValues = [dependantValues]
+ }
+
+ // If inverting, we want to ensure that we don't have any matches.
+ // If not inverting, we want to ensure that we do have any matches.
+ const currentVal = helpers.deepGet(instance, dependantSetting)
+ const anyMatches = dependantValues.some(dependantVal => {
+ if (dependantVal == null) {
+ return currentVal != null && currentVal !== false && currentVal !== ""
+ }
+ return dependantVal === currentVal
+ })
+ return anyMatches !== invert
+ })
+}
diff --git a/yarn.lock b/yarn.lock
index a636a62c87..6d71f587c0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5127,6 +5127,11 @@
resolved "https://registry.yarnpkg.com/@spectrum-css/tabs/-/tabs-3.2.12.tgz#9b08f23d5aa881b3441af7757800c7173e5685ff"
integrity sha512-rPFUW9SSW4+3/UJ3UrtY2/l3sQvlqB1fqxHLPDjgykvbfrnMejcCTNV4ZrFNHXpE/6+kGnk+yVViSPtWGwJzkA==
+"@spectrum-css/tag@3.0.0":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@spectrum-css/tag/-/tag-3.0.0.tgz#b2e335dc526713b83f3e995e8d1d4fc84a3fc4df"
+ integrity sha512-a9z7ZTAWPonkWRNY5kxVaO6bxu9de3qUZWJ9Bl1YBlwWc8Fy1L7XqT4Wq3pW+4sktUbUUqqPYPIXK9xEFDofEw==
+
"@spectrum-css/tags@3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@spectrum-css/tags/-/tags-3.0.2.tgz#5bf35fb79c97cd9344de485bd4626ad5b9f07757"