diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 2caad20bf6..6809f1ffa5 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -80,7 +80,7 @@ "dayjs": "^1.10.8", "easymde": "^2.16.1", "svelte-dnd-action": "^0.9.8", - "svelte-portal": "^1.0.0" + "svelte-portal": "^2.2.1" }, "resolutions": { "loader-utils": "1.4.1" diff --git a/packages/bbui/src/Actions/click_outside.js b/packages/bbui/src/Actions/click_outside.ts similarity index 63% rename from packages/bbui/src/Actions/click_outside.js rename to packages/bbui/src/Actions/click_outside.ts index 526659cb7a..248a03039e 100644 --- a/packages/bbui/src/Actions/click_outside.js +++ b/packages/bbui/src/Actions/click_outside.ts @@ -1,3 +1,17 @@ +type ClickOutsideCallback = (event: MouseEvent) => void | undefined + +interface ClickOutsideOpts { + callback?: ClickOutsideCallback + anchor?: HTMLElement +} + +interface Handler { + id: number + element: HTMLElement + anchor: HTMLElement + callback?: ClickOutsideCallback +} + // These class names will never trigger a callback if clicked, no matter what const ignoredClasses = [ ".download-js-link", @@ -14,18 +28,20 @@ const conditionallyIgnoredClasses = [ ".drawer-wrapper", ".spectrum-Popover", ] -let clickHandlers = [] -let candidateTarget +let clickHandlers: Handler[] = [] +let candidateTarget: HTMLElement | undefined // Processes a "click outside" event and invokes callbacks if our source element // is valid -const handleClick = event => { +const handleClick = (e: MouseEvent) => { + const target = e.target as HTMLElement + // Ignore click if this is an ignored class - if (event.target.closest('[data-ignore-click-outside="true"]')) { + if (target.closest('[data-ignore-click-outside="true"]')) { return } for (let className of ignoredClasses) { - if (event.target.closest(className)) { + if (target.closest(className)) { return } } @@ -33,41 +49,41 @@ const handleClick = event => { // Process handlers clickHandlers.forEach(handler => { // Check that the click isn't inside the target - if (handler.element.contains(event.target)) { + if (handler.element.contains(target)) { return } // Ignore clicks for certain classes unless we're nested inside them for (let className of conditionallyIgnoredClasses) { const sourceInside = handler.anchor.closest(className) != null - const clickInside = event.target.closest(className) != null + const clickInside = target.closest(className) != null if (clickInside && !sourceInside) { return } } - handler.callback?.(event) + handler.callback?.(e) }) } // On mouse up we only trigger a "click outside" callback if we targetted the // same element that we did on mouse down. This fixes all sorts of issues where // we get annoying callbacks firing when we drag to select text. -const handleMouseUp = e => { +const handleMouseUp = (e: MouseEvent) => { if (candidateTarget === e.target) { handleClick(e) } - candidateTarget = null + candidateTarget = undefined } // On mouse down we store which element was targetted for comparison later -const handleMouseDown = e => { +const handleMouseDown = (e: MouseEvent) => { // Only handle the primary mouse button here. // We handle context menu (right click) events in another handler. if (e.button !== 0) { return } - candidateTarget = e.target + candidateTarget = e.target as HTMLElement // Clear any previous listeners in case of multiple down events, and register // a single mouse up listener @@ -82,7 +98,12 @@ document.addEventListener("contextmenu", handleClick) /** * Adds or updates a click handler */ -const updateHandler = (id, element, anchor, callback) => { +const updateHandler = ( + id: number, + element: HTMLElement, + anchor: HTMLElement, + callback: ClickOutsideCallback | undefined +) => { let existingHandler = clickHandlers.find(x => x.id === id) if (!existingHandler) { clickHandlers.push({ id, element, anchor, callback }) @@ -94,27 +115,52 @@ const updateHandler = (id, element, anchor, callback) => { /** * Removes a click handler */ -const removeHandler = id => { +const removeHandler = (id: number) => { clickHandlers = clickHandlers.filter(x => x.id !== id) } /** - * Svelte action to apply a click outside handler for a certain element + * Svelte action to apply a click outside handler for a certain element. * opts.anchor is an optional param specifying the real root source of the * component being observed. This is required for things like popovers, where * the element using the clickoutside action is the popover, but the popover is * rendered at the root of the DOM somewhere, whereas the popover anchor is the * element we actually want to consider when determining the source component. */ -export default (element, opts) => { +export default ( + element: HTMLElement, + opts?: ClickOutsideOpts | ClickOutsideCallback +) => { const id = Math.random() - const update = newOpts => { - const callback = - newOpts?.callback || (typeof newOpts === "function" ? newOpts : null) - const anchor = newOpts?.anchor || element + + const isCallback = ( + opts?: ClickOutsideOpts | ClickOutsideCallback + ): opts is ClickOutsideCallback => { + return typeof opts === "function" + } + + const isOpts = ( + opts?: ClickOutsideOpts | ClickOutsideCallback + ): opts is ClickOutsideOpts => { + return opts != null && typeof opts === "object" + } + + const update = (newOpts?: ClickOutsideOpts | ClickOutsideCallback) => { + let callback: ClickOutsideCallback | undefined + let anchor = element + if (isCallback(newOpts)) { + callback = newOpts + } else if (isOpts(newOpts)) { + callback = newOpts.callback + if (newOpts.anchor) { + anchor = newOpts.anchor + } + } updateHandler(id, element, anchor, callback) } + update(opts) + return { update, destroy: () => removeHandler(id), diff --git a/packages/bbui/src/Actions/position_dropdown.ts b/packages/bbui/src/Actions/position_dropdown.ts index 303af2b53c..424baf91f3 100644 --- a/packages/bbui/src/Actions/position_dropdown.ts +++ b/packages/bbui/src/Actions/position_dropdown.ts @@ -1,13 +1,7 @@ -/** - * Valid alignment options are - * - left - * - right - * - left-outside - * - right-outside - **/ - // Strategies are defined as [Popover]To[Anchor]. // They can apply for both horizontal and vertical alignment. +import { PopoverAlignment } from "../constants" + type Strategy = | "StartToStart" | "EndToEnd" @@ -33,7 +27,7 @@ export type UpdateHandler = ( interface Opts { anchor?: HTMLElement - align: string + align: PopoverAlignment maxHeight?: number maxWidth?: number minWidth?: number @@ -174,24 +168,33 @@ export default function positionDropdown(element: HTMLElement, opts: Opts) { } // Determine X strategy - if (align === "right") { + if (align === PopoverAlignment.Right) { applyXStrategy("EndToEnd") - } else if (align === "right-outside" || align === "right-context-menu") { + } else if ( + align === PopoverAlignment.RightOutside || + align === PopoverAlignment.RightContextMenu + ) { applyXStrategy("StartToEnd") - } else if (align === "left-outside" || align === "left-context-menu") { + } else if ( + align === PopoverAlignment.LeftOutside || + align === PopoverAlignment.LeftContextMenu + ) { applyXStrategy("EndToStart") - } else if (align === "center") { + } else if (align === PopoverAlignment.Center) { applyXStrategy("MidPoint") } else { applyXStrategy("StartToStart") } // Determine Y strategy - if (align === "right-outside" || align === "left-outside") { + if ( + align === PopoverAlignment.RightOutside || + align === PopoverAlignment.LeftOutside + ) { applyYStrategy("MidPoint") } else if ( - align === "right-context-menu" || - align === "left-context-menu" + align === PopoverAlignment.RightContextMenu || + align === PopoverAlignment.LeftContextMenu ) { applyYStrategy("StartToStart") if (styles.top) { @@ -204,11 +207,11 @@ export default function positionDropdown(element: HTMLElement, opts: Opts) { // Handle screen overflow if (doesXOverflow()) { // Swap left to right - if (align === "left") { + if (align === PopoverAlignment.Left) { applyXStrategy("EndToEnd") } // Swap right-outside to left-outside - else if (align === "right-outside") { + else if (align === PopoverAlignment.RightOutside) { applyXStrategy("EndToStart") } } @@ -225,10 +228,13 @@ export default function positionDropdown(element: HTMLElement, opts: Opts) { applyXStrategy("EndToStart") } } - // Othewise invert as normal + // Otherwise invert as normal else { // If using an outside strategy then lock to the bottom of the screen - if (align === "left-outside" || align === "right-outside") { + if ( + align === PopoverAlignment.LeftOutside || + align === PopoverAlignment.RightOutside + ) { applyYStrategy("ScreenEdge") } // Otherwise flip above diff --git a/packages/bbui/src/Form/Core/Combobox.svelte b/packages/bbui/src/Form/Core/Combobox.svelte index e2e2792f54..38cc3ec091 100644 --- a/packages/bbui/src/Form/Core/Combobox.svelte +++ b/packages/bbui/src/Form/Core/Combobox.svelte @@ -11,6 +11,7 @@ import { createEventDispatcher } from "svelte" import clickOutside from "../../Actions/click_outside" import Popover from "../../Popover/Popover.svelte" + import { PopoverAlignment } from "../../constants" export let value: string | undefined = undefined export let id: string | undefined = undefined @@ -97,11 +98,16 @@ (open = false)} useAnchorWidth > -
(open = false)}> +
{ + open = false + }} + >
    {#if options && Array.isArray(options)} {#each options as option} diff --git a/packages/bbui/src/Form/Core/Picker.svelte b/packages/bbui/src/Form/Core/Picker.svelte index 88866196a7..8541858923 100644 --- a/packages/bbui/src/Form/Core/Picker.svelte +++ b/packages/bbui/src/Form/Core/Picker.svelte @@ -1,4 +1,9 @@ - + + + +
    diff --git a/packages/bbui/src/Form/Input.svelte b/packages/bbui/src/Form/Input.svelte index 47962720af..b1a8669135 100644 --- a/packages/bbui/src/Form/Input.svelte +++ b/packages/bbui/src/Form/Input.svelte @@ -1,24 +1,24 @@ - + + + - - - - -
    - -
    - -
    Add New View
    - -
    - - -
    -
    -
    - - -
    - -
    - -
    Add New Row
    -
    - - - -
    -
    - - -
    -
    -
    - - -
    - -
    - -
    Add New Filter
    -
    -

    Where

    - - - - -
    - -
    -
    diff --git a/packages/bbui/src/StatusLight/StatusLight.svelte b/packages/bbui/src/StatusLight/StatusLight.svelte index f3ef1f62de..e15bed42dd 100644 --- a/packages/bbui/src/StatusLight/StatusLight.svelte +++ b/packages/bbui/src/StatusLight/StatusLight.svelte @@ -1,25 +1,25 @@ - diff --git a/packages/bbui/src/Tooltip/AbsTooltip.svelte b/packages/bbui/src/Tooltip/AbsTooltip.svelte index a887db4102..528ea84efd 100644 --- a/packages/bbui/src/Tooltip/AbsTooltip.svelte +++ b/packages/bbui/src/Tooltip/AbsTooltip.svelte @@ -1,38 +1,24 @@ - - - + + + + + +
    + {#if label != null} +
    +
    + {#if expandable} + (expanded = !expanded)} + /> + {/if} +
    +
    (expanded = !expanded)} + on:click={() => dispatch("click-label", clickContext)} + > + {label} +
    +
    (valueExpanded = !valueExpanded)} + on:click={() => dispatch("click-value", clickContext)} + > + {displayValue} +
    + {#if showCopyIcon} +
    + dispatch("click-copy", clickContext)} + /> +
    + {/if} +
    + {/if} + {#if expandable && (expanded || label == null)} +
    + {#each keys as key} + + {/each} +
    + {/if} +
    + + diff --git a/packages/builder/src/components/common/UpdateAppTopNav.svelte b/packages/builder/src/components/common/UpdateAppTopNav.svelte index f4a76c4576..967bf1dfdf 100644 --- a/packages/builder/src/components/common/UpdateAppTopNav.svelte +++ b/packages/builder/src/components/common/UpdateAppTopNav.svelte @@ -25,7 +25,7 @@
    + diff --git a/packages/builder/src/components/design/settings/controls/PropertyControl.svelte b/packages/builder/src/components/design/settings/controls/PropertyControl.svelte index 55680160a2..2b63cdd748 100644 --- a/packages/builder/src/components/design/settings/controls/PropertyControl.svelte +++ b/packages/builder/src/components/design/settings/controls/PropertyControl.svelte @@ -5,7 +5,6 @@ runtimeToReadableBinding, } from "@/dataBinding" import { builderStore } from "@/stores/builder" - import { onDestroy } from "svelte" export let label = "" export let labelHidden = false @@ -32,7 +31,7 @@ $: safeValue = getSafeValue(value, defaultValue, allBindings) $: replaceBindings = val => readableToRuntimeBinding(allBindings, val) - $: if (!Array.isArray(value)) { + $: if (value) { highlightType = highlightedProp?.key === key ? `highlighted-${highlightedProp?.type}` : "" } @@ -75,12 +74,6 @@ ? defaultValue : enriched } - - onDestroy(() => { - if (highlightedProp) { - builderStore.highlightSetting(null) - } - })
    diff --git a/packages/builder/src/dataBinding.js b/packages/builder/src/dataBinding.js index 1272c9eb6d..739ecc9494 100644 --- a/packages/builder/src/dataBinding.js +++ b/packages/builder/src/dataBinding.js @@ -1159,10 +1159,17 @@ export const buildFormSchema = (component, asset) => { * Returns an array of the keys of any state variables which are set anywhere * in the app. */ -export const getAllStateVariables = () => { - // Find all button action settings in all components +export const getAllStateVariables = screen => { + let assets = [] + if (screen) { + // only include state variables from a specific screen + assets.push(screen) + } else { + // otherwise include state variables from all screens + assets = getAllAssets() + } let eventSettings = [] - getAllAssets().forEach(asset => { + assets.forEach(asset => { findAllMatchingComponents(asset.props, component => { const settings = componentStore.getComponentSettings(component._component) const nestedTypes = [ @@ -1214,11 +1221,17 @@ export const getAllStateVariables = () => { }) // Add on load settings from screens - get(screenStore).screens.forEach(screen => { + if (screen) { if (screen.onLoad) { eventSettings.push(screen.onLoad) } - }) + } else { + get(screenStore).screens.forEach(screen => { + if (screen.onLoad) { + eventSettings.push(screen.onLoad) + } + }) + } // Extract all state keys from any "update state" actions in each setting let bindingSet = new Set() diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsPanel.svelte index 30f11e6cab..de6993c661 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsPanel.svelte @@ -16,6 +16,7 @@ } from "@/dataBinding" import { ActionButton, notifications } from "@budibase/bbui" import { capitalise } from "@/helpers" + import { builderStore } from "@/stores/builder" import TourWrap from "@/components/portal/onboarding/TourWrap.svelte" import { TOUR_STEP_KEYS } from "@/components/portal/onboarding/tours.js" @@ -55,6 +56,17 @@ $: id = $selectedComponent?._id $: id, (section = tabs[0]) $: componentName = getComponentName(componentInstance) + + $: highlightedSetting = $builderStore.highlightedSetting + $: if (highlightedSetting) { + if (highlightedSetting.key === "_conditions") { + section = "conditions" + } else if (highlightedSetting.key === "_styles") { + section = "styles" + } else { + section = "settings" + } + } {#if $selectedComponent} @@ -98,7 +110,7 @@ {/each}
    - {#if section == "settings"} + {#if section === "settings"} {/if} - {#if section == "styles"} + {#if section === "styles"} {/if} - {#if section == "conditions"} + {#if section === "conditions"}
    onOperatorChange(condition, e.detail)} @@ -236,7 +236,7 @@ disabled={condition.noValue || condition.operator === "oneOf"} options={valueTypeOptions} bind:value={condition.valueType} - placeholder={null} + placeholder={false} on:change={e => onValueTypeChange(condition, e.detail)} /> {#if ["string", "number"].includes(condition.valueType)} diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ConditionalUISection.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ConditionalUISection.svelte index b40a6af3c6..07b9d35a64 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ConditionalUISection.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ConditionalUISection.svelte @@ -9,6 +9,7 @@ import { componentStore } from "@/stores/builder" import ConditionalUIDrawer from "./ConditionalUIDrawer.svelte" import ComponentSettingsSection from "./ComponentSettingsSection.svelte" + import { builderStore } from "@/stores/builder" export let componentInstance export let componentDefinition @@ -18,6 +19,8 @@ let tempValue let drawer + $: highlighted = $builderStore.highlightedSetting?.key === "_conditions" + const openDrawer = () => { tempValue = JSON.parse(JSON.stringify(componentInstance?._conditions ?? [])) drawer.show() @@ -52,7 +55,9 @@ /> - {conditionText} +
    + {conditionText} +
    @@ -61,3 +66,13 @@ + + diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/CustomStylesSection.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/CustomStylesSection.svelte index 30e448774b..230f6ebc60 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/CustomStylesSection.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/CustomStylesSection.svelte @@ -16,6 +16,7 @@ readableToRuntimeBinding, runtimeToReadableBinding, } from "@/dataBinding" + import { builderStore } from "@/stores/builder" export let componentInstance export let componentDefinition @@ -32,6 +33,8 @@ $: icon = componentDefinition?.icon + $: highlighted = $builderStore.highlightedSetting?.key === "_styles" + const openDrawer = () => { tempValue = runtimeToReadableBinding( bindings, @@ -55,7 +58,7 @@ name={`Custom CSS${componentInstance?._styles?.custom ? " *" : ""}`} collapsible={false} > -
    +
    Edit custom CSS
    @@ -97,4 +100,12 @@ align-items: center; gap: var(--spacing-m); } + + .highlighted { + background: var(--spectrum-global-color-gray-300); + border-left: 4px solid var(--spectrum-semantic-informative-color-background); + transition: background 130ms ease-out, border-color 130ms ease-out; + margin: -4px calc(-1 * var(--spacing-xl)); + padding: 4px var(--spacing-xl) 4px calc(var(--spacing-xl) - 4px); + } 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 d2fed41655..be7d48f88f 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]/[componentId]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_layout.svelte index 16fe7d9c2f..f5dec6371f 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_layout.svelte @@ -30,7 +30,7 @@ if (id === `${$screenStore.selectedScreenId}-screen`) return true if (id === `${$screenStore.selectedScreenId}-navigation`) return true - return !!findComponent($selectedScreen.props, id) + return !!findComponent($selectedScreen?.props, id) } // Keep URL and state in sync for selected component ID diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPanel.svelte index 29e2ce03ff..b04cd8b956 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPanel.svelte @@ -50,6 +50,9 @@ margin-bottom: 9px; } + .header-left { + display: flex; + } .header-left :global(div) { border-right: none; } diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/BindingsPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/BindingsPanel.svelte new file mode 100644 index 0000000000..fb222905bf --- /dev/null +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/BindingsPanel.svelte @@ -0,0 +1,83 @@ + + +
+ +
+ + diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/ComponentKeyHandler.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentKeyHandler.svelte similarity index 100% rename from packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/ComponentKeyHandler.svelte rename to packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentKeyHandler.svelte diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/index.svelte index eb5a57ec9c..7636d923de 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/index.svelte @@ -1,6 +1,5 @@ - -
-
- Components -
- -
-
- +
    openScreenContextMenu(e, false)} @@ -159,7 +135,6 @@
-
diff --git a/packages/builder/src/pages/builder/portal/settings/ai/AISettings.spec.js b/packages/builder/src/pages/builder/portal/settings/ai/AISettings.spec.js index bfd4311aa0..35f8859cd5 100644 --- a/packages/builder/src/pages/builder/portal/settings/ai/AISettings.spec.js +++ b/packages/builder/src/pages/builder/portal/settings/ai/AISettings.spec.js @@ -40,26 +40,33 @@ function setupEnv(hosting, features = {}, flags = {}) { describe("AISettings", () => { let instance = null + const setupDOM = () => { + instance = render(AISettings, {}) + const modalContainer = document.createElement("div") + modalContainer.classList.add("modal-container") + instance.baseElement.appendChild(modalContainer) + } + afterEach(() => { vi.restoreAllMocks() }) it("that the AISettings is rendered", () => { - instance = render(AISettings, {}) + setupDOM() expect(instance).toBeDefined() }) describe("Licensing", () => { it("should show the premium label on self host for custom configs", async () => { setupEnv(Hosting.Self) - instance = render(AISettings, {}) + setupDOM() const premiumTag = instance.queryByText("Premium") expect(premiumTag).toBeInTheDocument() }) it("should show the enterprise label on cloud for custom configs", async () => { setupEnv(Hosting.Cloud) - instance = render(AISettings, {}) + setupDOM() const enterpriseTag = instance.queryByText("Enterprise") expect(enterpriseTag).toBeInTheDocument() }) @@ -69,7 +76,7 @@ describe("AISettings", () => { let configModal setupEnv(Hosting.Cloud) - instance = render(AISettings) + setupDOM() addConfigurationButton = instance.queryByText("Add configuration") expect(addConfigurationButton).toBeInTheDocument() await fireEvent.click(addConfigurationButton) @@ -86,7 +93,7 @@ describe("AISettings", () => { { customAIConfigsEnabled: true }, { AI_CUSTOM_CONFIGS: true } ) - instance = render(AISettings) + setupDOM() addConfigurationButton = instance.queryByText("Add configuration") expect(addConfigurationButton).toBeInTheDocument() await fireEvent.click(addConfigurationButton) @@ -103,7 +110,7 @@ describe("AISettings", () => { { customAIConfigsEnabled: true }, { AI_CUSTOM_CONFIGS: true } ) - instance = render(AISettings) + setupDOM() addConfigurationButton = instance.queryByText("Add configuration") expect(addConfigurationButton).toBeInTheDocument() await fireEvent.click(addConfigurationButton) diff --git a/packages/builder/src/stores/builder/components.ts b/packages/builder/src/stores/builder/components.ts index 6693cbf4dc..5b8b455e5a 100644 --- a/packages/builder/src/stores/builder/components.ts +++ b/packages/builder/src/stores/builder/components.ts @@ -20,6 +20,7 @@ import { previewStore, tables, componentTreeNodesStore, + builderStore, screenComponents, } from "@/stores/builder" import { buildFormSchema, getSchemaForDatasource } from "@/dataBinding" @@ -33,6 +34,7 @@ import { BudiStore } from "../BudiStore" import { Utils } from "@budibase/frontend-core" import { Component as ComponentType, + ComponentCondition, FieldType, Screen, Table, @@ -68,6 +70,7 @@ export interface ComponentDefinition { export interface ComponentSetting { key: string type: string + label?: string section?: string name?: string defaultValue?: any @@ -743,14 +746,16 @@ export class ComponentStore extends BudiStore { } } - /** - * - * @param {string} componentId - */ - select(componentId: string) { + select(id: string) { this.update(state => { - state.selectedComponentId = componentId - return state + // Only clear highlights if selecting a different component + if (!id.includes(state.selectedComponentId!)) { + builderStore.highlightSetting() + } + return { + ...state, + selectedComponentId: id, + } }) } @@ -1132,7 +1137,7 @@ export class ComponentStore extends BudiStore { }) } - async updateConditions(conditions: Record) { + async updateConditions(conditions: ComponentCondition[]) { await this.patch((component: Component) => { component._conditions = conditions }) diff --git a/packages/builder/src/stores/builder/index.js b/packages/builder/src/stores/builder/index.js index 7dd7f67828..c6dc631e5c 100644 --- a/packages/builder/src/stores/builder/index.js +++ b/packages/builder/src/stores/builder/index.js @@ -16,7 +16,11 @@ import { userStore, userSelectedResourceMap, isOnlyUser } from "./users.js" import { deploymentStore } from "./deployments.js" import { contextMenuStore } from "./contextMenu.js" import { snippets } from "./snippets" -import { screenComponents, screenComponentErrors } from "./screenComponent" +import { + screenComponents, + screenComponentErrors, + findComponentsBySettingsType, +} from "./screenComponent" // Backend import { tables } from "./tables" @@ -70,6 +74,7 @@ export { appPublished, screenComponents, screenComponentErrors, + findComponentsBySettingsType, } export const reset = () => { diff --git a/packages/builder/src/stores/builder/preview.ts b/packages/builder/src/stores/builder/preview.ts index fe14c02535..87b2b9355e 100644 --- a/packages/builder/src/stores/builder/preview.ts +++ b/packages/builder/src/stores/builder/preview.ts @@ -82,6 +82,10 @@ export class PreviewStore extends BudiStore { })) } + updateState(data: Record) { + this.sendEvent("builder-state", data) + } + requestComponentContext() { this.sendEvent("request-context") } diff --git a/packages/builder/src/stores/builder/screenComponent.ts b/packages/builder/src/stores/builder/screenComponent.ts index f1a9440c02..446d0f31d6 100644 --- a/packages/builder/src/stores/builder/screenComponent.ts +++ b/packages/builder/src/stores/builder/screenComponent.ts @@ -124,7 +124,7 @@ export const screenComponentErrors = derived( } ) -function findComponentsBySettingsType( +export function findComponentsBySettingsType( screen: Screen, type: string | string[], definitions: Record diff --git a/packages/client/src/constants.ts b/packages/client/src/constants.ts index f7e3e86d40..a3d06fd48f 100644 --- a/packages/client/src/constants.ts +++ b/packages/client/src/constants.ts @@ -15,6 +15,7 @@ export const ActionTypes = { export const DNDPlaceholderID = "dnd-placeholder" export const ScreenslotType = "screenslot" +export const ScreenslotID = "screenslot" export const GridRowHeight = 24 export const GridColumns = 12 export const GridSpacing = 4 diff --git a/packages/client/src/index.js b/packages/client/src/index.js index 7cb9ed4430..8a48aa08e5 100644 --- a/packages/client/src/index.js +++ b/packages/client/src/index.js @@ -9,6 +9,7 @@ import { dndStore, eventStore, hoverStore, + stateStore, } from "./stores" import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-vite.js" import { get } from "svelte/store" @@ -87,8 +88,10 @@ const loadBudibase = async () => { dndStore.actions.reset() } } else if (type === "request-context") { - const { selectedComponentInstance } = get(componentStore) - const context = selectedComponentInstance?.getDataContext() + const { selectedComponentInstance, screenslotInstance } = + get(componentStore) + const instance = selectedComponentInstance || screenslotInstance + const context = instance?.getDataContext() let stringifiedContext = null try { stringifiedContext = JSON.stringify(context) @@ -102,6 +105,9 @@ const loadBudibase = async () => { hoverStore.actions.hoverComponent(data, false) } else if (type === "builder-meta") { builderStore.actions.setMetadata(data) + } else if (type === "builder-state") { + const [[key, value]] = Object.entries(data) + stateStore.actions.setValue(key, value) } } diff --git a/packages/client/src/stores/components.js b/packages/client/src/stores/components.js index 48ade99321..024e7b4edc 100644 --- a/packages/client/src/stores/components.js +++ b/packages/client/src/stores/components.js @@ -6,7 +6,7 @@ import { screenStore } from "./screens" import { builderStore } from "./builder" import Router from "../components/Router.svelte" import * as AppComponents from "../components/app/index.js" -import { ScreenslotType } from "../constants" +import { ScreenslotID, ScreenslotType } from "../constants" export const BudibasePrefix = "@budibase/standard-components/" @@ -43,6 +43,7 @@ const createComponentStore = () => { selectedComponentDefinition: definition, selectedComponentPath: selectedPath?.map(component => component._id), mountedComponentCount: Object.keys($store.mountedComponents).length, + screenslotInstance: $store.mountedComponents[ScreenslotID], } } ) diff --git a/packages/client/src/stores/screens.js b/packages/client/src/stores/screens.js index 491d8d4236..a7277ca2b5 100644 --- a/packages/client/src/stores/screens.js +++ b/packages/client/src/stores/screens.js @@ -7,7 +7,7 @@ import { dndIndex, dndParent, dndIsNewComponent, dndBounds } from "./dnd.js" import { RoleUtils } from "@budibase/frontend-core" import { findComponentById, findComponentParent } from "../utils/components.js" import { Helpers } from "@budibase/bbui" -import { DNDPlaceholderID } from "constants" +import { DNDPlaceholderID, ScreenslotID, ScreenslotType } from "constants" const createScreenStore = () => { const store = derived( @@ -171,8 +171,8 @@ const createScreenStore = () => { _component: "@budibase/standard-components/layout", _children: [ { - _component: "screenslot", - _id: "screenslot", + _component: ScreenslotType, + _id: ScreenslotID, _styles: { normal: { flex: "1 1 auto", diff --git a/packages/types/src/documents/app/component.ts b/packages/types/src/documents/app/component.ts index 654b2c87cb..abf9db26ff 100644 --- a/packages/types/src/documents/app/component.ts +++ b/packages/types/src/documents/app/component.ts @@ -1,9 +1,22 @@ import { Document } from "../document" +import { BasicOperator } from "../../sdk" export interface Component extends Document { _instanceName: string _styles: { [key: string]: any } _component: string _children?: Component[] + _conditions?: ComponentCondition[] [key: string]: any } + +export interface ComponentCondition { + id: string + operator: BasicOperator + action: "update" | "show" | "hide" + valueType: "string" | "number" | "datetime" | "boolean" + newValue?: any + referenceValue?: any + setting?: string + settingValue?: any +} diff --git a/packages/types/src/documents/app/screen.ts b/packages/types/src/documents/app/screen.ts index a8c32118d3..f1a119083c 100644 --- a/packages/types/src/documents/app/screen.ts +++ b/packages/types/src/documents/app/screen.ts @@ -23,6 +23,7 @@ export interface Screen extends Document { props: ScreenProps name?: string pluginAdded?: boolean + onLoad?: EventHandler[] } export interface ScreenRoutesViewOutput extends Document { @@ -36,3 +37,14 @@ export type ScreenRoutingJson = Record< subpaths: Record } > + +export interface EventHandler { + parameters: { + key: string + type: string + value: string + persist: any | null + } + "##eventHandlerType": string + id: string +} diff --git a/yarn.lock b/yarn.lock index 60294cc5ab..ea7084e53a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18993,10 +18993,10 @@ svelte-loading-spinners@^0.1.1: resolved "https://registry.yarnpkg.com/svelte-loading-spinners/-/svelte-loading-spinners-0.1.7.tgz#3fa6fa0ef67ab635773bf20b07d0b071debbadaa" integrity sha512-EKCId1DjVL2RSUVJJsvtNcqQHox03XIgh4xh/4p7r6ST7d8mut6INY9/LqK4A17PFU64+3quZmqiSfOlf480CA== -svelte-portal@1.0.0, svelte-portal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/svelte-portal/-/svelte-portal-1.0.0.tgz#36a47c5578b1a4d9b4dc60fa32a904640ec4cdd3" - integrity sha512-nHf+DS/jZ6jjnZSleBMSaZua9JlG5rZv9lOGKgJuaZStfevtjIlUJrkLc3vbV8QdBvPPVmvcjTlazAzfKu0v3Q== +svelte-portal@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/svelte-portal/-/svelte-portal-2.2.1.tgz#b1d7bed78e56318db245996beb5483d8de6b9740" + integrity sha512-uF7is5sM4aq5iN7QF/67XLnTUvQCf2iiG/B1BHTqLwYVY1dsVmTeXZ/LeEyU6dLjApOQdbEG9lkqHzxiQtOLEQ== svelte-spa-router@^4.0.1: version "4.0.1"