diff --git a/packages/bbui/src/Actions/click_outside.js b/packages/bbui/src/Actions/click_outside.js index 1961dca47c..62416ae88d 100644 --- a/packages/bbui/src/Actions/click_outside.js +++ b/packages/bbui/src/Actions/click_outside.js @@ -32,6 +32,13 @@ const handleClick = event => { return } + // Ignore clicks for drawers, unless the handler is registered from a drawer + const sourceInDrawer = handler.anchor.closest(".drawer-container") != null + const clickInDrawer = event.target.closest(".drawer-container") != null + if (clickInDrawer && !sourceInDrawer) { + return + } + handler.callback?.(event) }) } diff --git a/packages/bbui/src/Actions/position_dropdown.js b/packages/bbui/src/Actions/position_dropdown.js index d259b9197a..770d1bd507 100644 --- a/packages/bbui/src/Actions/position_dropdown.js +++ b/packages/bbui/src/Actions/position_dropdown.js @@ -15,6 +15,7 @@ export default function positionDropdown(element, opts) { align, maxHeight, maxWidth, + minWidth, useAnchorWidth, offset = 5, customUpdate, @@ -28,7 +29,7 @@ export default function positionDropdown(element, opts) { const elementBounds = element.getBoundingClientRect() let styles = { maxHeight: null, - minWidth: null, + minWidth, maxWidth, left: null, top: null, @@ -41,8 +42,13 @@ export default function positionDropdown(element, opts) { }) } else { // Determine vertical styles - if (align === "right-outside") { - styles.top = anchorBounds.top + if (align === "right-outside" || align === "left-outside") { + styles.top = + anchorBounds.top + anchorBounds.height / 2 - elementBounds.height / 2 + styles.maxHeight = maxHeight + if (styles.top + elementBounds.height > window.innerHeight) { + styles.top = window.innerHeight - elementBounds.height + } } else if ( window.innerHeight - anchorBounds.bottom < (maxHeight || 100) diff --git a/packages/bbui/src/Drawer/Drawer.svelte b/packages/bbui/src/Drawer/Drawer.svelte index 8976bfb81e..757f86b471 100644 --- a/packages/bbui/src/Drawer/Drawer.svelte +++ b/packages/bbui/src/Drawer/Drawer.svelte @@ -1,28 +1,109 @@ + + {#if visible} - -
- {#if !headless} + +
+
+
0} + class:modal={$modal} + transition:drawerSlide|local + {style} + >
-
- {title} - - - -
+
{title || "Bindings"}
- {/if} - -
+ +
+
+
{/if} diff --git a/packages/builder/src/components/common/CodeEditor/index.js b/packages/builder/src/components/common/CodeEditor/index.js index 0d71a475f0..c104267aa4 100644 --- a/packages/builder/src/components/common/CodeEditor/index.js +++ b/packages/builder/src/components/common/CodeEditor/index.js @@ -1,4 +1,3 @@ -import { EditorView } from "@codemirror/view" import { getManifest } from "@budibase/string-templates" import sanitizeHtml from "sanitize-html" import { groupBy } from "lodash" @@ -27,123 +26,33 @@ export const SECTIONS = { }, } -export const getDefaultTheme = opts => { - const { height, resize, dark } = opts - return EditorView.theme( - { - "&.cm-focused .cm-cursor": { - borderLeftColor: "var(--spectrum-alias-text-color)", - }, - "&": { - height: height ? `${height}` : "", - lineHeight: "1.3", - border: - "var(--spectrum-alias-border-size-thin) solid var(--spectrum-alias-border-color)", - borderRadius: "var(--border-radius-s)", - backgroundColor: - "var( --spectrum-textfield-m-background-color, var(--spectrum-global-color-gray-50) )", - resize: resize ? `${resize}` : "", - overflow: "hidden", - color: "var(--spectrum-alias-text-color)", - }, - "& .cm-tooltip.cm-tooltip-autocomplete > ul": { - fontFamily: - "var(--spectrum-alias-body-text-font-family, var(--spectrum-global-font-family-base))", - maxHeight: "16em", - }, - "& .cm-placeholder": { - color: "var(--spectrum-alias-text-color)", - fontStyle: "italic", - }, - "&.cm-focused": { - outline: "none", - borderColor: "var(--spectrum-alias-border-color-mouse-focus)", - }, - // AUTO COMPLETE - "& .cm-completionDetail": { - fontStyle: "unset", - textTransform: "uppercase", - fontSize: "10px", - backgroundColor: "var(--spectrum-global-color-gray-100)", - color: "var(--spectrum-global-color-gray-600)", - }, - "& .cm-completionLabel": { - marginLeft: - "calc(var(--spectrum-alias-workflow-icon-size-m) + var(--spacing-m))", - }, - "& .info-bubble": { - fontSize: "var(--font-size-s)", - display: "grid", - gridGap: "var(--spacing-s)", - gridTemplateColumns: "1fr", - color: "var(--spectrum-global-color-gray-800)", - }, - "& .cm-tooltip": { - marginLeft: "var(--spacing-s)", - border: "1px solid var(--spectrum-global-color-gray-300)", - borderRadius: - "var( --spectrum-popover-border-radius, var(--spectrum-alias-border-radius-regular) )", - backgroundColor: "var(--spectrum-global-color-gray-50)", - }, - // Section header - "& .info-section": { - display: "flex", - padding: "var(--spacing-s)", - gap: "var(--spacing-m)", - borderBottom: "1px solid var(--spectrum-global-color-gray-200)", - color: "var(--spectrum-global-color-gray-800)", - fontWeight: "bold", - }, - "& .info-section .spectrum-Icon": { - color: "var(--spectrum-global-color-gray-600)", - }, - // Autocomplete Option - "& .cm-tooltip.cm-tooltip-autocomplete .autocomplete-option": { - display: "flex", - justifyContent: "space-between", - alignItems: "center", - fontSize: "var(--spectrum-alias-font-size-default)", - padding: "var(--spacing-s)", - color: "var(--spectrum-global-color-gray-800)", - }, - "& .cm-tooltip-autocomplete ul li[aria-selected].autocomplete-option": { - backgroundColor: "var(--spectrum-global-color-gray-200)", - }, - "& .binding-wrap": { - color: "var(--spectrum-global-color-blue-700)", - fontFamily: "monospace", - }, - }, - { dark } - ) -} - export const buildHelperInfoNode = (completion, helper) => { const ele = document.createElement("div") ele.classList.add("info-bubble") const exampleNodeHtml = helper.example - ? `
${helper.example}
` + ? `
${helper.example}
` : "" const descriptionMarkup = sanitizeHtml(helper.description, { allowedTags: [], allowedAttributes: {}, }) - const descriptionNodeHtml = `
${descriptionMarkup}
` + const descriptionNodeHtml = `
${descriptionMarkup}
` ele.innerHTML = ` - ${exampleNodeHtml} ${descriptionNodeHtml} + ${exampleNodeHtml} ` return ele } const toSpectrumIcon = name => { return ` ` @@ -152,7 +61,9 @@ const toSpectrumIcon = name => { export const buildSectionHeader = (type, sectionName, icon, rank) => { const ele = document.createElement("div") ele.classList.add("info-section") - ele.classList.add(type) + if (type) { + ele.classList.add(type) + } ele.innerHTML = `${toSpectrumIcon(icon)}${sectionName}` return { name: sectionName, @@ -174,7 +85,7 @@ export const helpersToCompletion = (helpers, mode) => { }, type: "helper", section: helperSection, - detail: "FUNCTION", + detail: "Function", apply: (view, completion, from, to) => { insertBinding(view, from, to, key, mode) }, @@ -252,21 +163,12 @@ export const jsAutocomplete = baseCompletions => { } export const buildBindingInfoNode = (completion, binding) => { + if (!binding.valueHTML || binding.value == null) { + return null + } const ele = document.createElement("div") ele.classList.add("info-bubble") - - const exampleNodeHtml = binding.readableBinding - ? `
{{ ${binding.readableBinding} }}
` - : "" - - const descriptionNodeHtml = binding.description - ? `
${binding.description}
` - : "" - - ele.innerHTML = ` - ${exampleNodeHtml} - ${descriptionNodeHtml} - ` + ele.innerHTML = `
${binding.valueHTML}
` return ele } diff --git a/packages/builder/src/components/common/bindings/BindingPanel.svelte b/packages/builder/src/components/common/bindings/BindingPanel.svelte index 7b567e052b..aa765c03f6 100644 --- a/packages/builder/src/components/common/bindings/BindingPanel.svelte +++ b/packages/builder/src/components/common/bindings/BindingPanel.svelte @@ -1,27 +1,19 @@ - - + +
- { - if (selectedMode == mode) { - return true - } - - //Get the current mode value - const editorValue = usingJS ? decodeJSBinding(jsValue) : hbsValue - - if (editorValue) { - targetMode = selectedMode - return false - } - return true - }} - > - -
-
-
- {#if targetMode} -
-
- - {`Switch to ${targetMode}?`} - - This will discard anything in your binding -
- - -
-
-
- {/if} - -
- -
- - {#if sidebar} -
- -
- {/if} -
-
- {#if allowJS} - -
-
-
- {#if targetMode} -
-
- - {`Switch to ${targetMode}?`} - - This will discard anything in your binding -
- - -
-
-
- {/if} - - -
- -
- - {#if sidebar} -
- -
- {/if} -
-
- {/if} -
- {#if typeof drawerActions?.hide === "function" && drawerActions?.headless} - - {/if} - {#if typeof bindingDrawerActions?.save === "function" && drawerActions?.headless} -
+
+ {#each sideTabs as tab} + changeSidePanel(tab)} > - Save - + + + {/each} + {#if drawerContext && get(drawerContext.resizable)} + drawerContext.modal.set(!drawerIsModal)} + > + + {/if}
-
+
+
+ {#if mode === Modes.Text} + {#key hbsCompletions} + + {/key} + {:else if mode === Modes.JavaScript} + {#key jsCompletions} + + {/key} + {/if} + {#if targetMode} +
+
+ + Switch to {targetMode}? + + This will discard anything in your binding +
+ + +
+
+
+ {/if} +
-
-
+
+ {#if sidePanel === SidePanels.Bindings} + + {:else if sidePanel === SidePanels.Evaluation} + + {/if} +
+ + diff --git a/packages/builder/src/components/common/bindings/BindingPicker.svelte b/packages/builder/src/components/common/bindings/BindingPicker.svelte deleted file mode 100644 index 42057b382b..0000000000 --- a/packages/builder/src/components/common/bindings/BindingPicker.svelte +++ /dev/null @@ -1,399 +0,0 @@ - - - - - -
- {#if hoverTarget.title} -
{hoverTarget.title}
- {/if} - {#if hoverTarget.description} -
- - {@html hoverTarget.description} -
- {/if} - {#if hoverTarget.example} -
{hoverTarget.example}
- {/if} -
-
-
-
- - - - - - {#if selectedCategory} -
- { - selectedCategory = null - }} - > - Back - -
- {/if} - - {#if !selectedCategory} - - {/if} - - {#if !selectedCategory && !search} - - {/if} - - {#if selectedCategory || search} - {#each filteredCategories as category} - {#if category.bindings?.length} -
-
- {category.name} -
- -
- {/if} - {/each} - - {#if selectedCategory === "Helpers" || search} - {#if filteredHelpers?.length} -
-
Helpers
- -
- {/if} - {/if} - {/if} -
- - diff --git a/packages/builder/src/components/common/bindings/BindingSidePanel.svelte b/packages/builder/src/components/common/bindings/BindingSidePanel.svelte new file mode 100644 index 0000000000..d990451005 --- /dev/null +++ b/packages/builder/src/components/common/bindings/BindingSidePanel.svelte @@ -0,0 +1,422 @@ + + + +
+ {#if hoverTarget.description} +
+ + {@html hoverTarget.description} +
+ {/if} + {#if hoverTarget.code} + +
{@html hoverTarget.code}
+ {/if} +
+
+ + + + +
+ + {#if selectedCategory} +
+ (selectedCategory = null)} + /> + {selectedCategory} +
+ {/if} + + {#if !selectedCategory} +
+ + + + { + search = null + }} + class:searching={search} + > + + +
+ {/if} + + {#if !selectedCategory && !search} + + {/if} + + {#if selectedCategory || search} + {#each filteredCategories as category} + {#if category.bindings?.length} +
+ {#if filteredCategories.length > 1} +
+ {category.name} +
+ {/if} +
    + {#each category.bindings as binding} +
  • showBindingPopover(binding, e.target)} + on:mouseleave={hidePopover} + on:click={() => addBinding(binding)} + > + + {#if binding.display?.name} + {binding.display.name} + {:else if binding.fieldSchema?.name} + {binding.fieldSchema?.name} + {:else} + {binding.readableBinding} + {/if} + + {#if binding.display?.type || binding.fieldSchema?.type} + + + {binding.display?.type || binding.fieldSchema?.type} + + + {/if} +
  • + {/each} +
+
+ {/if} + {/each} + + {#if selectedCategory === "Helpers" || search} + {#if filteredHelpers?.length} +
+
    + {#each filteredHelpers as helper} +
  • showHelperPopover(helper, e.target)} + on:mouseleave={hidePopover} + on:click={() => addHelper(helper, mode.name === "javascript")} + > + {helper.displayText} + + function + +
  • + {/each} +
+
+ {/if} + {/if} + {/if} +
+
+ + diff --git a/packages/builder/src/components/common/bindings/ClientBindingPanel.svelte b/packages/builder/src/components/common/bindings/ClientBindingPanel.svelte index 7888f2e486..843dec8c89 100644 --- a/packages/builder/src/components/common/bindings/ClientBindingPanel.svelte +++ b/packages/builder/src/components/common/bindings/ClientBindingPanel.svelte @@ -1,8 +1,9 @@ - - - Add the objects on the left to enrich your text. - - - + + (tempValue = event.detail)} {bindings} {allowJS} diff --git a/packages/builder/src/components/common/bindings/DrawerBindableInput.svelte b/packages/builder/src/components/common/bindings/DrawerBindableInput.svelte index 80996a484d..d11ebcf87a 100644 --- a/packages/builder/src/components/common/bindings/DrawerBindableInput.svelte +++ b/packages/builder/src/components/common/bindings/DrawerBindableInput.svelte @@ -13,21 +13,21 @@ export let panel = ClientBindingPanel export let value = "" export let bindings = [] - export let title = "Bindings" + export let title export let placeholder export let label export let disabled = false - export let fillWidth export let allowJS = true export let allowHelpers = true export let updateOnChange = true - export let drawerLeft export let key export let disableBindings = false + export let forceModal = false + export let context = null const dispatch = createEventDispatcher() + let bindingDrawer - let valid = true let currentVal = value $: readableValue = runtimeToReadableBinding(bindings, value) @@ -88,27 +88,20 @@ - - Add the objects on the left to enrich your text. - - + (tempValue = event.detail)} {bindings} {allowJS} {allowHelpers} + {context} /> diff --git a/packages/builder/src/components/common/bindings/DrawerBindableSlot.svelte b/packages/builder/src/components/common/bindings/DrawerBindableSlot.svelte index 4cac24660f..8ce9dda209 100644 --- a/packages/builder/src/components/common/bindings/DrawerBindableSlot.svelte +++ b/packages/builder/src/components/common/bindings/DrawerBindableSlot.svelte @@ -16,7 +16,6 @@ export let placeholder export let label export let disabled = false - export let fillWidth export let allowJS = true export let allowHelpers = true export let updateOnChange = true @@ -26,7 +25,6 @@ const dispatch = createEventDispatcher() let bindingDrawer - let valid = true let currentVal = value $: readableValue = runtimeToReadableBinding(bindings, value) @@ -173,22 +171,14 @@ - - Add the objects on the left to enrich your text. - - + (tempValue = event.detail)} {bindings} diff --git a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte new file mode 100644 index 0000000000..82c3f80a6f --- /dev/null +++ b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte @@ -0,0 +1,133 @@ + + +
+
+
+ {#if error} + +
Error
+ {#if evaluating} +
+ +
+ {/if} + + + {:else} +
Preview
+ {#if evaluating} +
+ +
+ {/if} + + {#if !empty} + + {/if} + {/if} +
+
+
+ {#if empty} + Your expression will be evaluated here + {:else} + + {@html highlightedResult} + {/if} +
+
+ + diff --git a/packages/builder/src/components/common/bindings/ModalBindableInput.svelte b/packages/builder/src/components/common/bindings/ModalBindableInput.svelte index 41245ceaec..3261dc6b74 100644 --- a/packages/builder/src/components/common/bindings/ModalBindableInput.svelte +++ b/packages/builder/src/components/common/bindings/ModalBindableInput.svelte @@ -1,115 +1,12 @@ - - -
- onChange(event.detail)} - {placeholder} - {updateOnChange} - /> -
- -
-
- - - - Add the objects on the left to enrich your text. - -
- (tempValue = e.detail)} - {bindings} - {allowJS} - /> -
-
-
- - + diff --git a/packages/builder/src/components/common/bindings/ServerBindingPanel.svelte b/packages/builder/src/components/common/bindings/ServerBindingPanel.svelte index 213e5bbf1d..5718d080f2 100644 --- a/packages/builder/src/components/common/bindings/ServerBindingPanel.svelte +++ b/packages/builder/src/components/common/bindings/ServerBindingPanel.svelte @@ -2,9 +2,9 @@ import BindingPanel from "./BindingPanel.svelte" export let bindings = [] - export let valid export let value = "" export let allowJS = false + export let context = null $: enrichedBindings = enrichBindings(bindings) @@ -19,9 +19,9 @@ diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte index d445c98a1a..f009c975cf 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte @@ -12,6 +12,7 @@ export let bindings export let nested export let componentInstance + export let title = "Actions" let drawer let tmpValue @@ -37,7 +38,7 @@ {actionText} - + Define what actions to run. diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DeleteRow.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DeleteRow.svelte index 431368d28f..5b7844ce53 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DeleteRow.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DeleteRow.svelte @@ -31,7 +31,7 @@ (parameters.rowId = value.detail)} /> diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/FetchRow.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/FetchRow.svelte index 4478f1bb16..61b7494ab2 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/FetchRow.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/FetchRow.svelte @@ -29,7 +29,7 @@ (parameters.rowId = value.detail)} /> diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveFields.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveFields.svelte index 52bd84c453..e03af75207 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveFields.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveFields.svelte @@ -62,7 +62,7 @@ {/if} updateFieldValue(idx, event.detail)} diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ShowNotification.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ShowNotification.svelte index 55b00d215d..d95e13cb5f 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ShowNotification.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ShowNotification.svelte @@ -40,6 +40,7 @@ diff --git a/packages/builder/src/components/design/settings/controls/PropertyControl.svelte b/packages/builder/src/components/design/settings/controls/PropertyControl.svelte index 0f0276823a..4db9a03d80 100644 --- a/packages/builder/src/components/design/settings/controls/PropertyControl.svelte +++ b/packages/builder/src/components/design/settings/controls/PropertyControl.svelte @@ -105,6 +105,7 @@ onChange={handleChange} bindings={allBindings} name={key} + title={label} {nested} {key} {type} diff --git a/packages/builder/src/components/integration/KeyValueBuilder.svelte b/packages/builder/src/components/integration/KeyValueBuilder.svelte index e71090f613..74636fc50c 100644 --- a/packages/builder/src/components/integration/KeyValueBuilder.svelte +++ b/packages/builder/src/components/integration/KeyValueBuilder.svelte @@ -143,7 +143,6 @@ value={field.value} allowJS={false} {allowHelpers} - fillWidth={true} drawerLeft={bindingDrawerLeft} /> {:else} diff --git a/packages/builder/src/pages/builder/app/[application]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/_layout.svelte index 16a1adadee..45b10d3d9e 100644 --- a/packages/builder/src/pages/builder/app/[application]/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/_layout.svelte @@ -188,7 +188,7 @@ {/if} - + 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 5867021386..4617814485 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 @@ -6,6 +6,7 @@
+
@@ -32,7 +33,17 @@ flex-direction: column; justify-content: flex-start; align-items: stretch; - padding: 9px var(--spacing-m); + padding: 9px 10px 12px 10px; + position: relative; + transition: width 360ms ease-out; + } + .drawer-container { + position: absolute; + height: 100%; + width: 100%; + overflow: hidden; + top: 0; + left: 0; } .header { display: flex; 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 bf66c28a09..a910036a4a 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 @@ -196,6 +196,16 @@ } else if (type === "add-parent-component") { const { componentId, parentType } = data await componentStore.addParent(componentId, parentType) + } else if (type === "provide-context") { + let context = data?.context + if (context) { + try { + context = JSON.parse(context) + } catch (error) { + context = null + } + } + previewStore.setSelectedComponentContext(context) } else { console.warn(`Client sent unknown event type: ${type}`) } diff --git a/packages/builder/src/stores/builder/preview.js b/packages/builder/src/stores/builder/preview.js index ecdd4573de..4923185ee7 100644 --- a/packages/builder/src/stores/builder/preview.js +++ b/packages/builder/src/stores/builder/preview.js @@ -4,6 +4,7 @@ const INITIAL_PREVIEW_STATE = { previewDevice: "desktop", previewEventHandler: null, showPreview: false, + selectedComponentContext: null, } export const createPreviewStore = () => { @@ -52,6 +53,17 @@ export const createPreviewStore = () => { }) } + const setSelectedComponentContext = context => { + store.update(state => { + state.selectedComponentContext = context + return state + }) + } + + const requestComponentContext = () => { + sendEvent("request-context") + } + return { subscribe: store.subscribe, setDevice, @@ -60,6 +72,8 @@ export const createPreviewStore = () => { startDrag, stopDrag, showPreview, + setSelectedComponentContext, + requestComponentContext, } } diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index 6d6af91dcc..3d267ec623 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -575,6 +575,15 @@ } } + const getDataContext = () => { + const normalContext = get(context) + const additionalContext = ref?.getAdditionalDataContext?.() + return { + ...normalContext, + ...additionalContext, + } + } + onMount(() => { // Register this component instance for external access if ($appStore.isDevApp) { @@ -583,7 +592,7 @@ component: instance._component, getSettings: () => cachedSettings, getRawSettings: () => ({ ...staticSettings, ...dynamicSettings }), - getDataContext: () => get(context), + getDataContext, reload: () => initialise(instance, true), setEphemeralStyles: styles => (ephemeralStyles = styles), state: store, diff --git a/packages/client/src/components/app/GridBlock.svelte b/packages/client/src/components/app/GridBlock.svelte index 0a343cac51..46a507387d 100644 --- a/packages/client/src/components/app/GridBlock.svelte +++ b/packages/client/src/components/app/GridBlock.svelte @@ -30,6 +30,7 @@ ActionTypes, createContextStore, Provider, + generateGoldenSample, } = getContext("sdk") let grid @@ -48,6 +49,19 @@ }, ] + // Provide additional data context for live binding eval + export const getAdditionalDataContext = () => { + const rows = get(grid?.getContext()?.rows) + const goldenRow = generateGoldenSample(rows) + const id = get(component).id + return { + [id]: goldenRow, + eventContext: { + row: goldenRow, + }, + } + } + // Parses columns to fix older formats const getParsedColumns = columns => { // If the first element has an active key all elements should be in the new format diff --git a/packages/client/src/components/app/blocks/CardsBlock.svelte b/packages/client/src/components/app/blocks/CardsBlock.svelte index 008fa7e730..bd2b69d352 100644 --- a/packages/client/src/components/app/blocks/CardsBlock.svelte +++ b/packages/client/src/components/app/blocks/CardsBlock.svelte @@ -4,6 +4,7 @@ import BlockComponent from "components/BlockComponent.svelte" import { makePropSafe as safe } from "@budibase/string-templates" import { enrichSearchColumns, enrichFilter } from "utils/blocks.js" + import { get } from "svelte/store" export let title export let dataSource @@ -31,7 +32,9 @@ export let linkColumn export let noRowsMessage - const { fetchDatasourceSchema } = getContext("sdk") + const context = getContext("context") + const { fetchDatasourceSchema, generateGoldenSample } = getContext("sdk") + const component = getContext("component") let formId let dataProviderId @@ -62,6 +65,16 @@ }, ] + // Provide additional data context for live binding eval + export const getAdditionalDataContext = () => { + const rows = get(context)[dataProviderId]?.rows || [] + const goldenRow = generateGoldenSample(rows) + const id = get(component).id + return { + [`${id}-repeater`]: goldenRow, + } + } + // Builds a full details page URL for the card title const buildFullCardUrl = (link, url, repeaterId, linkColumn) => { if (!link || !url || !repeaterId) { diff --git a/packages/client/src/components/app/blocks/MultiStepFormblock.svelte b/packages/client/src/components/app/blocks/MultiStepFormblock.svelte index b24c2f418a..1a0fbbd50d 100644 --- a/packages/client/src/components/app/blocks/MultiStepFormblock.svelte +++ b/packages/client/src/components/app/blocks/MultiStepFormblock.svelte @@ -5,7 +5,7 @@ import { builderStore } from "stores" import { Utils } from "@budibase/frontend-core" import FormBlockWrapper from "./form/FormBlockWrapper.svelte" - import { writable } from "svelte/store" + import { get, writable } from "svelte/store" export let actionType export let rowId @@ -15,7 +15,7 @@ export let buttonPosition = "bottom" export let size - const { fetchDatasourceSchema } = getContext("sdk") + const { fetchDatasourceSchema, generateGoldenSample } = getContext("sdk") const component = getContext("component") const context = getContext("context") @@ -45,6 +45,16 @@ $: enrichedSteps = enrichSteps(steps, schema, $component.id, $currentStep) $: updateCurrentStep(enrichedSteps, $builderStore, $component) + // Provide additional data context for live binding eval + export const getAdditionalDataContext = () => { + const id = get(component).id + const rows = get(context)[`${id}-provider`]?.rows || [] + const goldenRow = generateGoldenSample(rows) + return { + [`${id}-repeater`]: goldenRow, + } + } + const updateCurrentStep = (steps, builderStore, component) => { const { componentId, step } = builderStore.metadata || {} diff --git a/packages/client/src/components/app/blocks/RepeaterBlock.svelte b/packages/client/src/components/app/blocks/RepeaterBlock.svelte index 30fbdddcdc..878b827c78 100644 --- a/packages/client/src/components/app/blocks/RepeaterBlock.svelte +++ b/packages/client/src/components/app/blocks/RepeaterBlock.svelte @@ -4,6 +4,7 @@ import Placeholder from "components/app/Placeholder.svelte" import { getContext } from "svelte" import { makePropSafe as safe } from "@budibase/string-templates" + import { get } from "svelte/store" export let dataSource export let filter @@ -18,8 +19,20 @@ export let gap const component = getContext("component") + const context = getContext("context") + const { generateGoldenSample } = getContext("sdk") let providerId + + // Provide additional data context for live binding eval + export const getAdditionalDataContext = () => { + const rows = get(context)[providerId]?.rows || [] + const goldenRow = generateGoldenSample(rows) + const id = get(component).id + return { + [`${id}-repeater`]: goldenRow, + } + } diff --git a/packages/client/src/components/app/blocks/RowExplorer.svelte b/packages/client/src/components/app/blocks/RowExplorer.svelte index 8fadcb5006..1e2357713a 100644 --- a/packages/client/src/components/app/blocks/RowExplorer.svelte +++ b/packages/client/src/components/app/blocks/RowExplorer.svelte @@ -3,25 +3,35 @@ import BlockComponent from "components/BlockComponent.svelte" import { makePropSafe as safe } from "@budibase/string-templates" import { generate } from "shortid" + import { get } from "svelte/store" + import { getContext } from "svelte" export let dataSource export let height - export let cardTitle export let cardSubtitle export let cardDescription export let cardImageURL export let cardSearchField - export let detailFields export let detailTitle - export let noRowsMessage const stateKey = generate() + const context = getContext("context") + const { generateGoldenSample } = getContext("sdk") let listDataProviderId let listRepeaterId + + // Provide additional data context for live binding eval + export const getAdditionalDataContext = () => { + const rows = get(context)[listDataProviderId]?.rows || [] + const goldenRow = generateGoldenSample(rows) + return { + [listRepeaterId]: goldenRow, + } + } diff --git a/packages/client/src/components/app/blocks/form/FormBlock.svelte b/packages/client/src/components/app/blocks/form/FormBlock.svelte index cdf1a05628..8c084a71ab 100644 --- a/packages/client/src/components/app/blocks/form/FormBlock.svelte +++ b/packages/client/src/components/app/blocks/form/FormBlock.svelte @@ -3,6 +3,7 @@ import InnerFormBlock from "./InnerFormBlock.svelte" import { Utils } from "@budibase/frontend-core" import FormBlockWrapper from "./FormBlockWrapper.svelte" + import { get } from "svelte/store" export let actionType export let dataSource @@ -11,7 +12,6 @@ export let fields export let buttons export let buttonPosition - export let title export let description export let rowId @@ -25,8 +25,56 @@ export let saveButtonLabel export let deleteButtonLabel - const { fetchDatasourceSchema } = getContext("sdk") + const { fetchDatasourceSchema, generateGoldenSample } = getContext("sdk") const component = getContext("component") + const context = getContext("context") + + let schema + + $: formattedFields = convertOldFieldFormat(fields) + $: fieldsOrDefault = getDefaultFields(formattedFields, schema) + $: fetchSchema(dataSource) + // We could simply spread $$props into the inner form and append our + // additions, but that would create svelte warnings about unused props and + // make maintenance in future more confusing as we typically always have a + // proper mapping of schema settings to component exports, without having to + // search multiple files + $: innerProps = { + dataSource, + actionUrl, + actionType, + size, + disabled, + fields: fieldsOrDefault, + title, + description, + schema, + notificationOverride, + buttons: + buttons || + Utils.buildFormBlockButtonConfig({ + _id: $component.id, + showDeleteButton, + showSaveButton, + saveButtonLabel, + deleteButtonLabel, + notificationOverride, + actionType, + actionUrl, + dataSource, + }), + buttonPosition: buttons ? buttonPosition : "top", + } + + // Provide additional data context for live binding eval + export const getAdditionalDataContext = () => { + const id = get(component).id + const rows = get(context)[`${id}-provider`]?.rows || [] + const goldenRow = generateGoldenSample(rows) + return { + [`${id}-repeater`]: goldenRow, + } + } const convertOldFieldFormat = fields => { if (!fields) { @@ -68,42 +116,6 @@ return [...fields, ...defaultFields].filter(field => field.active) } - let schema - - $: formattedFields = convertOldFieldFormat(fields) - $: fieldsOrDefault = getDefaultFields(formattedFields, schema) - $: fetchSchema(dataSource) - // We could simply spread $$props into the inner form and append our - // additions, but that would create svelte warnings about unused props and - // make maintenance in future more confusing as we typically always have a - // proper mapping of schema settings to component exports, without having to - // search multiple files - $: innerProps = { - dataSource, - actionUrl, - actionType, - size, - disabled, - fields: fieldsOrDefault, - title, - description, - schema, - notificationOverride, - buttons: - buttons || - Utils.buildFormBlockButtonConfig({ - _id: $component.id, - showDeleteButton, - showSaveButton, - saveButtonLabel, - deleteButtonLabel, - notificationOverride, - actionType, - actionUrl, - dataSource, - }), - buttonPosition: buttons ? buttonPosition : "top", - } const fetchSchema = async () => { schema = (await fetchDatasourceSchema(dataSource)) || {} } diff --git a/packages/client/src/components/app/dynamic-filter/DynamicFilter.svelte b/packages/client/src/components/app/dynamic-filter/DynamicFilter.svelte index 549574e89b..2f7af56744 100644 --- a/packages/client/src/components/app/dynamic-filter/DynamicFilter.svelte +++ b/packages/client/src/components/app/dynamic-filter/DynamicFilter.svelte @@ -58,13 +58,13 @@ } let clonedSchema = {} if (!allowedFields?.length) { - clonedSchema = schema + clonedSchema = schema || {} } else { allowedFields?.forEach(field => { - if (schema[field.name]) { + if (schema?.[field.name]) { clonedSchema[field.name] = schema[field.name] clonedSchema[field.name].displayName = field.displayName - } else if (schema[field]) { + } else if (schema?.[field]) { clonedSchema[field] = schema[field] } }) diff --git a/packages/client/src/index.js b/packages/client/src/index.js index 2c8d310619..1ce7101466 100644 --- a/packages/client/src/index.js +++ b/packages/client/src/index.js @@ -84,6 +84,18 @@ const loadBudibase = async () => { } else { dndStore.actions.reset() } + } else if (type === "request-context") { + const { selectedComponentInstance } = get(componentStore) + const context = selectedComponentInstance?.getDataContext() + let stringifiedContext = null + try { + stringifiedContext = JSON.stringify(context) + } catch (error) { + // Ignore - invalid context + } + eventStore.actions.dispatchEvent("provide-context", { + context: stringifiedContext, + }) } else if (type === "hover-component") { hoverStore.actions.hoverComponent(data) } else if (type === "builder-meta") { diff --git a/packages/client/src/sdk.js b/packages/client/src/sdk.js index 557d5ca8da..d86d635970 100644 --- a/packages/client/src/sdk.js +++ b/packages/client/src/sdk.js @@ -29,7 +29,12 @@ import { fetchDatasourceSchema } from "./utils/schema.js" import { getAPIKey } from "./utils/api.js" import { enrichButtonActions } from "./utils/buttonActions.js" import { processStringSync, makePropSafe } from "@budibase/string-templates" -import { fetchData, LuceneUtils, Constants } from "@budibase/frontend-core" +import { + fetchData, + LuceneUtils, + Constants, + RowUtils, +} from "@budibase/frontend-core" export default { API, @@ -65,6 +70,7 @@ export default { processStringSync, makePropSafe, createContextStore, + generateGoldenSample: RowUtils.generateGoldenSample, // Components Provider, diff --git a/packages/frontend-core/src/utils/index.js b/packages/frontend-core/src/utils/index.js index 3f00c00e47..98998b7f0e 100644 --- a/packages/frontend-core/src/utils/index.js +++ b/packages/frontend-core/src/utils/index.js @@ -3,6 +3,7 @@ export * as JSONUtils from "./json" export * as CookieUtils from "./cookies" export * as RoleUtils from "./roles" export * as Utils from "./utils" +export * as RowUtils from "./rows" export { memo, derivedMemo } from "./memo" export { createWebsocket } from "./websocket" export * from "./download" diff --git a/packages/frontend-core/src/utils/rows.js b/packages/frontend-core/src/utils/rows.js new file mode 100644 index 0000000000..ea43d63734 --- /dev/null +++ b/packages/frontend-core/src/utils/rows.js @@ -0,0 +1,48 @@ +/** + * Util to check is a given value is "better" than another. "Betterness" is + * defined as presence and length. + */ +const isBetterSample = (newValue, oldValue) => { + // Prefer non-null values + if (oldValue == null && newValue != null) { + return true + } + + // Don't change type + const oldType = typeof oldValue + const newType = typeof newValue + if (oldType !== newType) { + return false + } + + // Prefer longer values + if (newType === "string" && newValue.length > oldValue.length) { + return true + } + if ( + newType === "object" && + Object.keys(newValue).length > Object.keys(oldValue).length + ) { + return true + } + + return false +} + +/** + * Generates a best-case example object of the provided samples. + * The generated sample does not necessarily exist - it simply is a sample that + * contains "good" examples for every property of all the samples. + * The generate sample will have a value for all keys across all samples. + */ +export const generateGoldenSample = samples => { + let goldenSample = {} + samples?.slice(0, 100).forEach(sample => { + Object.keys(sample).forEach(key => { + if (isBetterSample(sample[key], goldenSample[key])) { + goldenSample[key] = sample[key] + } + }) + }) + return goldenSample +} diff --git a/yarn.lock b/yarn.lock index 235e28e85e..53bf1ac017 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8242,11 +8242,16 @@ codemirror-spell-checker@1.1.2: dependencies: typo-js "*" -codemirror@^5.59.0, codemirror@^5.63.1: +codemirror@^5.63.1: version "5.65.12" resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.65.12.tgz#294fdf097d10ac5b56a9e011a91eff252afc73ae" integrity sha512-z2jlHBocElRnPYysN2HAuhXbO3DNB0bcSKmNz3hcWR2Js2Dkhc1bEOxG93Z3DeUrnm+qx56XOY5wQmbP5KY0sw== +codemirror@^5.65.16: + version "5.65.16" + resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.65.16.tgz#efc0661be6bf4988a6a1c2fe6893294638cdb334" + integrity sha512-br21LjYmSlVL0vFCPWPfhzUCT34FM/pAdK7rRIZwa0rrtrIdotvP4Oh4GUHsu2E3IrQMCfRkL/fN3ytMNxVQvg== + collect-v8-coverage@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" @@ -13944,6 +13949,11 @@ json-buffer@3.0.1: resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== +json-format-highlight@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/json-format-highlight/-/json-format-highlight-1.0.4.tgz#2e44277edabcec79a3d2c84e984c62e2258037b9" + integrity sha512-RqenIjKr1I99XfXPAml9G7YlEZg/GnsH7emWyWJh2yuGXqHW8spN7qx6/ME+MoIBb35/fxrMC9Jauj6nvGe4Mg== + json-parse-better-errors@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"