diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 1b3510cf3b..0830f8ab6f 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -72,6 +72,7 @@ "@spectrum-css/switch": "1.0.2", "@spectrum-css/table": "3.0.1", "@spectrum-css/tabs": "3.2.12", + "@spectrum-css/tag": "3.0.0", "@spectrum-css/tags": "3.0.2", "@spectrum-css/textfield": "3.0.1", "@spectrum-css/toast": "3.0.1", diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte index 54241ea1cc..d5a696c6bf 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte @@ -1,5 +1,4 @@ diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPanel.svelte index cecfdba858..62fe593602 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPanel.svelte @@ -14,11 +14,90 @@ import sanitizeUrl from "helpers/sanitizeUrl" import ButtonActionEditor from "components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte" import { getBindableProperties } from "dataBinding" + import BarButtonList from "components/design/settings/controls/BarButtonList.svelte" $: bindings = getBindableProperties($selectedScreen, null) + $: screenSettings = getScreenSettings($selectedScreen) let errors = {} + const getScreenSettings = screen => { + let settings = [ + { + key: "routing.homeScreen", + control: Checkbox, + props: { + text: "Set as home screen", + }, + }, + { + key: "routing.route", + label: "Route", + control: Input, + parser: val => { + if (!val.startsWith("/")) { + val = "/" + val + } + return sanitizeUrl(val) + }, + validate: route => { + const existingRoute = screen.routing.route + if (route !== existingRoute && routeTaken(route)) { + return "That URL is already in use for this role" + } + return null + }, + }, + { + key: "routing.roleId", + label: "Access", + control: RoleSelect, + validate: role => { + const existingRole = screen.routing.roleId + if (role !== existingRole && roleTaken(role)) { + return "That role is already in use for this URL" + } + return null + }, + }, + { + key: "onLoad", + label: "On screen load", + control: ButtonActionEditor, + }, + { + key: "width", + label: "Width", + control: Select, + props: { + options: ["Extra small", "Small", "Medium", "Large", "Max"], + placeholder: "Default", + disabled: !!screen.layoutId, + }, + }, + { + key: "props.layout", + label: "Layout", + defaultValue: "flex", + control: BarButtonList, + props: { + options: [ + { + barIcon: "ModernGridView", + value: "flex", + }, + { + barIcon: "ViewGrid", + value: "grid", + }, + ], + }, + }, + ] + + return settings + } + const routeTaken = url => { const roleId = get(selectedScreen).routing.roleId || "BASIC" return get(screenStore).screens.some( @@ -71,61 +150,6 @@ } } - $: screenSettings = [ - { - key: "routing.homeScreen", - control: Checkbox, - props: { - text: "Set as home screen", - }, - }, - { - key: "routing.route", - label: "Route", - control: Input, - parser: val => { - if (!val.startsWith("/")) { - val = "/" + val - } - return sanitizeUrl(val) - }, - validate: route => { - const existingRoute = get(selectedScreen).routing.route - if (route !== existingRoute && routeTaken(route)) { - return "That URL is already in use for this role" - } - return null - }, - }, - { - key: "routing.roleId", - label: "Access", - control: RoleSelect, - validate: role => { - const existingRole = get(selectedScreen).routing.roleId - if (role !== existingRole && roleTaken(role)) { - return "That role is already in use for this URL" - } - return null - }, - }, - { - key: "onLoad", - label: "On screen load", - control: ButtonActionEditor, - }, - { - key: "width", - label: "Width", - control: Select, - props: { - options: ["Extra small", "Small", "Medium", "Large", "Max"], - placeholder: "Default", - disabled: !!$selectedScreen.layoutId, - }, - }, - ] - const removeCustomLayout = async () => { return screenStore.removeCustomLayout(get(selectedScreen)) } @@ -149,6 +173,7 @@ value={Helpers.deepGet($selectedScreen, setting.key)} onChange={val => setScreenSetting(setting, val)} props={{ ...setting.props, error: errors[setting.key] }} + defaultValue={setting.defaultValue} {bindings} /> {/each} diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/index.svelte index 8a544eac86..405b8b0c21 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/index.svelte @@ -33,7 +33,7 @@ {/each} - + {#if activeTab === "theme"} {:else} diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte index 0ce9e096f2..055ecd88e0 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte @@ -144,7 +144,12 @@ const rootComponent = get(selectedScreen).props const component = findComponent(rootComponent, data.id) componentStore.copy(component) - await componentStore.paste(component) + await componentStore.paste( + component, + data.mode, + null, + data.selectComponent + ) } else if (type === "preview-loaded") { // Wait for this event to show the client library if intelligent // loading is supported @@ -246,13 +251,13 @@ -
+
{#if loading} -
+
+
diff --git a/packages/client/src/components/Router.svelte b/packages/client/src/components/Router.svelte index 551a564094..d19585dcec 100644 --- a/packages/client/src/components/Router.svelte +++ b/packages/client/src/components/Router.svelte @@ -71,4 +71,7 @@ div { position: relative; } + div :global(> .component > *) { + flex: 1 1 auto; + } diff --git a/packages/client/src/components/Screen.svelte b/packages/client/src/components/Screen.svelte index ac0af9f3b2..2d62b2f810 100644 --- a/packages/client/src/components/Screen.svelte +++ b/packages/client/src/components/Screen.svelte @@ -13,8 +13,9 @@ const onLoadActions = memo() // Get the screen definition for the current route - $: screenDefinition = $screenStore.activeScreen?.props - $: onLoadActions.set($screenStore.activeScreen?.onLoad) + $: screen = $screenStore.activeScreen + $: screenDefinition = { ...screen?.props, addEmptyRows: true } + $: onLoadActions.set(screen?.onLoad) $: runOnLoadActions($onLoadActions, params) // Enrich and execute any on load actions. diff --git a/packages/client/src/components/app/BackgroundImage.svelte b/packages/client/src/components/app/BackgroundImage.svelte index df6459c417..5e522252e6 100644 --- a/packages/client/src/components/app/BackgroundImage.svelte +++ b/packages/client/src/components/app/BackgroundImage.svelte @@ -1,8 +1,7 @@ -{#if url} -
-
- -
+
+
+
-{:else if $builderStore.inBuilder} -
- -
-{/if} +
diff --git a/packages/client/src/components/app/ButtonGroup.svelte b/packages/client/src/components/app/ButtonGroup.svelte index 2cf6b3db7d..b3523cdd21 100644 --- a/packages/client/src/components/app/ButtonGroup.svelte +++ b/packages/client/src/components/app/ButtonGroup.svelte @@ -19,6 +19,11 @@ gap, wrap: true, }} + styles={{ + normal: { + height: "100%", + }, + }} > {#each buttons as { text, type, quiet, disabled, onClick, size, icon, gap }} - import { getContext } from "svelte" - - const component = getContext("component") - const { styleable, builderStore } = getContext("sdk") - - export let cols = 12 - export let rows = 12 - - // Deliberately non-reactive as we want this fixed whenever the grid renders - const defaultColSpan = Math.ceil((cols + 1) / 2) - const defaultRowSpan = Math.ceil((rows + 1) / 2) - - $: coords = generateCoords(rows, cols) - - const generateCoords = (rows, cols) => { - let grid = [] - for (let row = 0; row < rows; row++) { - for (let col = 0; col < cols; col++) { - grid.push({ row, col }) - } - } - return grid - } - - -
- {#if $builderStore.inBuilder} -
- {#each coords as _} -
- {/each} -
- {/if} - -
- - diff --git a/packages/client/src/components/app/GridBlock.svelte b/packages/client/src/components/app/GridBlock.svelte index e86f163dba..30a35b0713 100644 --- a/packages/client/src/components/app/GridBlock.svelte +++ b/packages/client/src/components/app/GridBlock.svelte @@ -198,7 +198,7 @@ overflow: hidden; height: 410px; } - div.in-builder :global(*) { - pointer-events: none; + div.in-builder :global(> *) { + pointer-events: none !important; } diff --git a/packages/client/src/components/app/Layout.svelte b/packages/client/src/components/app/Layout.svelte index af74e14aa0..04dbb9efda 100644 --- a/packages/client/src/components/app/Layout.svelte +++ b/packages/client/src/components/app/Layout.svelte @@ -36,7 +36,6 @@ export let logoLinkUrl export let openLogoLinkInNewTab export let textAlign - export let embedded = false const NavigationClasses = { @@ -339,6 +338,7 @@ />
+ diff --git a/packages/client/src/components/app/container/Container.svelte b/packages/client/src/components/app/container/Container.svelte new file mode 100644 index 0000000000..9cfe74cf35 --- /dev/null +++ b/packages/client/src/components/app/container/Container.svelte @@ -0,0 +1,12 @@ + + + + + diff --git a/packages/client/src/components/app/Container.svelte b/packages/client/src/components/app/container/FlexContainer.svelte similarity index 94% rename from packages/client/src/components/app/Container.svelte rename to packages/client/src/components/app/container/FlexContainer.svelte index f11636b339..34368dcc8c 100644 --- a/packages/client/src/components/app/Container.svelte +++ b/packages/client/src/components/app/container/FlexContainer.svelte @@ -12,7 +12,7 @@ export let wrap export let onClick - $: directionClass = direction ? `valid-container direction-${direction}` : "" + $: directionClass = direction ? `flex-container direction-${direction}` : "" $: hAlignClass = hAlign ? `hAlign-${hAlign}` : "" $: vAlignClass = vAlign ? `vAlign-${vAlign}` : "" $: sizeClass = size ? `size-${size}` : "" @@ -39,11 +39,11 @@
diff --git a/packages/client/src/components/app/index.js b/packages/client/src/components/app/index.js index 6d9df6e588..63cdb95ac1 100644 --- a/packages/client/src/components/app/index.js +++ b/packages/client/src/components/app/index.js @@ -13,7 +13,7 @@ import "@spectrum-css/page/dist/index-vars.css" export { default as Placeholder } from "./Placeholder.svelte" // User facing components -export { default as container } from "./Container.svelte" +export { default as container } from "./container/Container.svelte" export { default as section } from "./Section.svelte" export { default as dataprovider } from "./DataProvider.svelte" export { default as divider } from "./Divider.svelte" @@ -35,7 +35,6 @@ export { default as spectrumcard } from "./SpectrumCard.svelte" export { default as tag } from "./Tag.svelte" export { default as markdownviewer } from "./MarkdownViewer.svelte" export { default as embeddedmap } from "./embedded-map/EmbeddedMap.svelte" -export { default as grid } from "./Grid.svelte" export { default as sidepanel } from "./SidePanel.svelte" export { default as modal } from "./Modal.svelte" export { default as gridblock } from "./GridBlock.svelte" diff --git a/packages/client/src/components/preview/DNDHandler.svelte b/packages/client/src/components/preview/DNDHandler.svelte index 767efc9e3a..bdd538748b 100644 --- a/packages/client/src/components/preview/DNDHandler.svelte +++ b/packages/client/src/components/preview/DNDHandler.svelte @@ -13,6 +13,7 @@ import { Utils } from "@budibase/frontend-core" import { findComponentById } from "utils/components.js" import { DNDPlaceholderID } from "constants" + import { isGridEvent } from "utils/grid" const ThrottleRate = 130 @@ -25,15 +26,6 @@ // Local flag for whether we are awaiting an async drop event let dropping = false - // Util to check if a DND event originates from a grid (or inside a grid). - // This is important as we do not handle grid DND in this handler. - const isGridEvent = e => { - return e.target - ?.closest?.(".component") - ?.parentNode?.closest?.(".component") - ?.childNodes[0]?.classList.contains("grid") - } - // Util to get the inner DOM node by a component ID const getDOMNode = id => { return document.getElementsByClassName(`${id}-dom`)[0] @@ -267,7 +259,7 @@ // Check if we're adding a new component rather than moving one if (source.newComponentType) { dropping = true - await builderStore.actions.dropNewComponent( + builderStore.actions.dropNewComponent( source.newComponentType, drop.parent, drop.index diff --git a/packages/client/src/components/preview/DNDPlaceholderOverlay.svelte b/packages/client/src/components/preview/DNDPlaceholderOverlay.svelte index 0be7faff1b..0ad4280e07 100644 --- a/packages/client/src/components/preview/DNDPlaceholderOverlay.svelte +++ b/packages/client/src/components/preview/DNDPlaceholderOverlay.svelte @@ -1,7 +1,7 @@ diff --git a/packages/client/src/components/preview/GridStylesButton.svelte b/packages/client/src/components/preview/GridStylesButton.svelte new file mode 100644 index 0000000000..430a0659ec --- /dev/null +++ b/packages/client/src/components/preview/GridStylesButton.svelte @@ -0,0 +1,42 @@ + + + + +
{ + builderStore.actions.updateStyles({ [style]: value }, componentId) + }} +> + +
+ + diff --git a/packages/client/src/components/preview/Indicator.svelte b/packages/client/src/components/preview/Indicator.svelte index 0480ea91ce..dce7945b29 100644 --- a/packages/client/src/components/preview/Indicator.svelte +++ b/packages/client/src/components/preview/Indicator.svelte @@ -1,5 +1,6 @@ diff --git a/packages/client/src/components/preview/SettingsBar.svelte b/packages/client/src/components/preview/SettingsBar.svelte index c5109c6bca..ec861ab5b4 100644 --- a/packages/client/src/components/preview/SettingsBar.svelte +++ b/packages/client/src/components/preview/SettingsBar.svelte @@ -1,117 +1,180 @@ {#if showBar}
+ {#if showGridStyles} + + + + +
+ + + + +
+ {/if} {#each settings as setting, idx} {#if setting.type === "select"} {#if setting.barStyle === "buttons"} @@ -141,6 +273,7 @@ value={option.value} icon={option.barIcon} title={option.barTitle || option.label} + {component} /> {/each} {:else} @@ -148,6 +281,7 @@ prop={setting.key} options={setting.options} label={setting.label} + {component} /> {/if} {:else if setting.type === "boolean"} @@ -156,9 +290,10 @@ icon={setting.barIcon} title={setting.barTitle || setting.label} bool + {component} /> {:else if setting.type === "color"} - + {/if} {#if setting.barSeparator !== false && (settings.length != idx + 1 || !isRoot)}
diff --git a/packages/client/src/components/preview/SettingsButton.svelte b/packages/client/src/components/preview/SettingsButton.svelte index 36009da821..a93ffb77af 100644 --- a/packages/client/src/components/preview/SettingsButton.svelte +++ b/packages/client/src/components/preview/SettingsButton.svelte @@ -1,17 +1,19 @@ @@ -19,7 +21,6 @@
{ 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"