diff --git a/packages/builder/src/components/backend/DataTable/formula.js b/packages/builder/src/components/backend/DataTable/formula.js index 8dcda83c27..65506726e6 100644 --- a/packages/builder/src/components/backend/DataTable/formula.js +++ b/packages/builder/src/components/backend/DataTable/formula.js @@ -1,4 +1,4 @@ -import { FieldType } from "@budibase/types" +import { FieldType, FormulaType } from "@budibase/types" import { FIELDS } from "@/constants/backend" import { tables } from "@/stores/builder" import { get as svelteGet } from "svelte/store" @@ -8,7 +8,6 @@ import { makeReadableKeyPropSafe } from "@/dataBinding" const MAX_DEPTH = 1 const TYPES_TO_SKIP = [ - FieldType.FORMULA, FieldType.AI, FieldType.LONGFORM, FieldType.SIGNATURE_SINGLE, @@ -17,6 +16,18 @@ const TYPES_TO_SKIP = [ FieldType.INTERNAL, ] +const shouldSkipFieldSchema = fieldSchema => { + // Skip some types always + if (TYPES_TO_SKIP.includes(fieldSchema.type)) { + return true + } + // Skip dynamic formula fields + return ( + fieldSchema.type === FieldType.FORMULA && + fieldSchema.formulaType === FormulaType.DYNAMIC + ) +} + export function getBindings({ table, path = null, @@ -32,7 +43,7 @@ export function getBindings({ // skip relationships after a certain depth and types which // can't bind to if ( - TYPES_TO_SKIP.includes(schema.type) || + shouldSkipFieldSchema(schema) || (isRelationship && depth >= MAX_DEPTH) ) { continue diff --git a/packages/builder/src/components/common/CodeEditor/AIGen.svelte b/packages/builder/src/components/common/CodeEditor/AIGen.svelte index b03aff7aba..199c7aa42a 100644 --- a/packages/builder/src/components/common/CodeEditor/AIGen.svelte +++ b/packages/builder/src/components/common/CodeEditor/AIGen.svelte @@ -40,14 +40,27 @@ let switchOnAIModal: Modal let addCreditsModal: Modal + const thresholdExpansionWidth = 350 $: accountPortalAccess = $auth?.user?.accountPortalAccess $: accountPortal = $admin.accountPortalUrl $: aiEnabled = $auth?.user?.llm - $: expanded = expandedOnly ? true : expanded + + $: expanded = + expandedOnly || + (parentWidth !== null && parentWidth > thresholdExpansionWidth) + ? true + : expanded + $: creditsExceeded = $licensing.aiCreditsExceeded $: disabled = suggestedCode !== null || !aiEnabled || creditsExceeded - $: if (expandedOnly) { + + $: if ( + expandedOnly || + (expanded && parentWidth !== null && parentWidth > thresholdExpansionWidth) + ) { containerWidth = calculateExpandedWidth() + } else if (!expanded) { + containerWidth = "auto" } async function generateJs(prompt: string) { diff --git a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte index 0448d4d048..c159804e47 100644 --- a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte +++ b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte @@ -443,7 +443,6 @@ const resizeObserver = new ResizeObserver(() => { updateEditorWidth() }) - resizeObserver.observe(editorEle) return () => { resizeObserver.disconnect() @@ -469,7 +468,6 @@ {#if aiGenEnabled} { return !invalids.find(invalid => noSpaces?.includes(invalid)) } +// If converting readable to runtime we need to ensure we don't replace words +// which are substrings of other words - e.g. a binding of `a` would turn +// `hah` into `h[a]h` which is obviously wrong. To avoid this we can remove all +// expanded versions of the binding to be replaced. +const excludeReadableExtensions = (string, binding) => { + // Escape any special chars in the binding so we can treat it as a literal + // string match in the regexes below + const escaped = binding.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + // Regex to find prefixed bindings (e.g. exclude xfoo for foo) + const regex1 = new RegExp(`[a-zA-Z0-9-_]+${escaped}[a-zA-Z0-9-_]*`, "g") + // Regex to find prefixed bindings (e.g. exclude foox for foo) + const regex2 = new RegExp(`[a-zA-Z0-9-_]*${escaped}[a-zA-Z0-9-_]+`, "g") + const matches = [...string.matchAll(regex1), ...string.matchAll(regex2)] + for (const match of matches) { + string = string.replace(match[0], new Array(match[0].length + 1).join("*")) + } + return string +} + /** * Utility function which replaces a string between given indices. */ @@ -1361,6 +1380,11 @@ const bindingReplacement = ( // in the search, working from longest to shortest so always use best match first let searchString = newBoundValue for (let from of convertFromProps) { + // If converting readable > runtime, blank out all extensions of this + // string to avoid partial matches + if (convertTo === "runtimeBinding") { + searchString = excludeReadableExtensions(searchString, from) + } const binding = bindableProperties.find(el => el[convertFrom] === from) if ( isJS || diff --git a/packages/builder/src/dataBinding.test.js b/packages/builder/src/dataBinding.test.js index 5e3b484e22..b406ef4277 100644 --- a/packages/builder/src/dataBinding.test.js +++ b/packages/builder/src/dataBinding.test.js @@ -72,6 +72,27 @@ describe("Builder dataBinding", () => { runtimeBinding: "count", type: "context", }, + { + category: "Bindings", + icon: "Brackets", + readableBinding: "location", + runtimeBinding: "[location]", + type: "context", + }, + { + category: "Bindings", + icon: "Brackets", + readableBinding: "foo.[bar]", + runtimeBinding: "[foo].[qwe]", + type: "context", + }, + { + category: "Bindings", + icon: "Brackets", + readableBinding: "foo.baz", + runtimeBinding: "[foo].[baz]", + type: "context", + }, ] it("should convert a readable binding to a runtime one", () => { const textWithBindings = `Hello {{ Current User.firstName }}! The count is {{ Binding.count }}.` @@ -83,6 +104,28 @@ describe("Builder dataBinding", () => { ) ).toEqual(`Hello {{ [user].[firstName] }}! The count is {{ count }}.`) }) + it("should not convert a partial match", () => { + const textWithBindings = `location {{ _location Zlocation location locationZ _location_ }}` + expect( + readableToRuntimeBinding( + bindableProperties, + textWithBindings, + "runtimeBinding" + ) + ).toEqual( + `location {{ _location Zlocation [location] locationZ _location_ }}` + ) + }) + it("should handle special characters in the readable binding", () => { + const textWithBindings = `{{ foo.baz }}` + expect( + readableToRuntimeBinding( + bindableProperties, + textWithBindings, + "runtimeBinding" + ) + ).toEqual(`{{ [foo].[baz] }}`) + }) }) describe("updateReferencesInObject", () => { diff --git a/packages/builder/src/pages/builder/portal/settings/ai/AIConfigTile.svelte b/packages/builder/src/pages/builder/portal/settings/ai/AIConfigTile.svelte index fb040204c2..d7688835c3 100644 --- a/packages/builder/src/pages/builder/portal/settings/ai/AIConfigTile.svelte +++ b/packages/builder/src/pages/builder/portal/settings/ai/AIConfigTile.svelte @@ -1,4 +1,4 @@ - diff --git a/packages/builder/src/pages/builder/portal/settings/ai/index.svelte b/packages/builder/src/pages/builder/portal/settings/ai/index.svelte index ae36fd5b98..b569993cd1 100644 --- a/packages/builder/src/pages/builder/portal/settings/ai/index.svelte +++ b/packages/builder/src/pages/builder/portal/settings/ai/index.svelte @@ -1,4 +1,4 @@ -