From 8a31cc2ff743042f27dcc3faa02d0ed2cd29fec0 Mon Sep 17 00:00:00 2001
From: Dean <deanhannigan@gmail.com>
Date: Tue, 13 Aug 2024 11:07:00 +0100
Subject: [PATCH] Bug fixes for bindings panel and code editor

---
 .../SetupPanel/RowSelectorTypes.svelte        |  17 ++-
 .../common/CodeEditor/CodeEditor.svelte       | 109 +++++++++++-------
 .../common/bindings/BindingPanel.svelte       |  13 +++
 .../common/bindings/DrawerBindableSlot.svelte |  12 +-
 4 files changed, 96 insertions(+), 55 deletions(-)

diff --git a/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte b/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte
index ee9fd12c0c..7717054a92 100644
--- a/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte
+++ b/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte
@@ -13,6 +13,10 @@
   import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
   import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
   import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
+  import {
+    readableToRuntimeBinding,
+    runtimeToReadableBinding,
+  } from "dataBinding"
 
   export let onChange
   export let field
@@ -30,6 +34,8 @@
     return clone
   })
 
+  $: readableValue = runtimeToReadableBinding(parsedBindings, fieldData)
+
   let attachmentTypes = [
     FieldType.ATTACHMENTS,
     FieldType.ATTACHMENT_SINGLE,
@@ -132,11 +138,12 @@
   />
 {:else if schema.type === "longform"}
   <TextArea
-    value={fieldData}
+    value={readableValue}
+    bindings={parsedBindings}
     on:change={e =>
       onChange({
         row: {
-          [field]: e.detail,
+          [field]: readableToRuntimeBinding(parsedBindings, e.detail),
         },
       })}
   />
@@ -144,11 +151,11 @@
   <span>
     <div class="field-wrap json-field">
       <CodeEditor
-        value={fieldData}
-        on:change={e => {
+        value={readableValue}
+        on:blur={e => {
           onChange({
             row: {
-              [field]: e.detail,
+              [field]: readableToRuntimeBinding(parsedBindings, e.detail),
             },
           })
         }}
diff --git a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte
index 0102d8f7a9..c3e4835f96 100644
--- a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte
+++ b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte
@@ -1,6 +1,6 @@
 <script>
   import { Label } from "@budibase/bbui"
-  import { onMount, createEventDispatcher } from "svelte"
+  import { onMount, createEventDispatcher, onDestroy } from "svelte"
   import { FIND_ANY_HBS_REGEX } from "@budibase/string-templates"
 
   import {
@@ -58,6 +58,64 @@
 
   const dispatch = createEventDispatcher()
 
+  let textarea
+  let editor
+  let mounted = false
+  let isEditorInitialised = false
+  let queuedRefresh = false
+
+  // Theming!
+  let currentTheme = $themeStore?.theme
+  let isDark = !currentTheme.includes("light")
+  let themeConfig = new Compartment()
+
+  $: {
+    if (autofocus && isEditorInitialised) {
+      editor.focus()
+    }
+  }
+
+  // Init when all elements are ready
+  $: if (mounted && !isEditorInitialised) {
+    isEditorInitialised = true
+    initEditor()
+  }
+
+  // Theme change
+  $: if (mounted && isEditorInitialised && $themeStore?.theme) {
+    if (currentTheme != $themeStore?.theme) {
+      currentTheme = $themeStore?.theme
+      isDark = !currentTheme.includes("light")
+
+      // Issue theme compartment update
+      editor.dispatch({
+        effects: themeConfig.reconfigure([...(isDark ? [oneDark] : [])]),
+      })
+    }
+  }
+
+  // Wait to try and gracefully replace
+  $: refresh(value, isEditorInitialised, mounted)
+
+  /**
+   * Will refresh the editor contents only after
+   * it has been fully initialised
+   * @param value {string} the editor value
+   */
+  const refresh = (value, initialised, mounted) => {
+    if (!initialised || !mounted) {
+      queuedRefresh = true
+      return
+    }
+
+    if (editor.state.doc.toString() !== value || queuedRefresh) {
+      editor.dispatch({
+        changes: { from: 0, to: editor.state.doc.length, insert: value },
+      })
+      queuedRefresh = false
+    }
+  }
+
   // Export a function to expose caret position
   export const getCaretPosition = () => {
     const selection_range = editor.state.selection.ranges[0]
@@ -132,11 +190,6 @@
     }
   )
 
-  // Theming!
-  let currentTheme = $themeStore?.theme
-  let isDark = !currentTheme.includes("light")
-  let themeConfig = new Compartment()
-
   const indentWithTabCustom = {
     key: "Tab",
     run: view => {
@@ -253,6 +306,11 @@
         lineNumbers(),
         foldGutter(),
         keymap.of(buildKeymap()),
+        EditorView.domEventHandlers({
+          blur: () => {
+            dispatch("blur", editor.state.doc.toString())
+          },
+        }),
         EditorView.updateListener.of(v => {
           const docStr = v.state.doc?.toString()
           if (docStr === value) {
@@ -266,11 +324,6 @@
     return complete
   }
 
-  let textarea
-  let editor
-  let mounted = false
-  let isEditorInitialised = false
-
   const initEditor = () => {
     const baseExtensions = buildBaseExtensions()
 
@@ -281,37 +334,13 @@
     })
   }
 
-  $: {
-    if (autofocus && isEditorInitialised) {
-      editor.focus()
-    }
-  }
-
-  // Init when all elements are ready
-  $: if (mounted && !isEditorInitialised) {
-    isEditorInitialised = true
-    initEditor()
-  }
-
-  // Theme change
-  $: if (mounted && isEditorInitialised && $themeStore?.theme) {
-    if (currentTheme != $themeStore?.theme) {
-      currentTheme = $themeStore?.theme
-      isDark = !currentTheme.includes("light")
-
-      // Issue theme compartment update
-      editor.dispatch({
-        effects: themeConfig.reconfigure([...(isDark ? [oneDark] : [])]),
-      })
-    }
-  }
-
   onMount(async () => {
     mounted = true
-    return () => {
-      if (editor) {
-        editor.destroy()
-      }
+  })
+
+  onDestroy(() => {
+    if (editor) {
+      editor.destroy()
     }
   })
 </script>
diff --git a/packages/builder/src/components/common/bindings/BindingPanel.svelte b/packages/builder/src/components/common/bindings/BindingPanel.svelte
index d8edf0cbb1..2a656dec6f 100644
--- a/packages/builder/src/components/common/bindings/BindingPanel.svelte
+++ b/packages/builder/src/components/common/bindings/BindingPanel.svelte
@@ -67,6 +67,10 @@
   let targetMode = null
   let expressionResult
   let evaluating = false
+  let mounted = false
+
+  // Value updates must be reprocessed to avoid timing issues
+  $: processUpdate(value, mounted)
 
   $: useSnippets = allowSnippets && !$licensing.isFreePlan
   $: editorModeOptions = getModeOptions(allowHBS, allowJS)
@@ -94,6 +98,13 @@
     }
   }
 
+  const processUpdate = value => {
+    if (!mounted) return
+    let isJS = value?.startsWith?.("{{ js ")
+    jsValue = isJS ? value : null
+    hbsValue = isJS ? null : value
+  }
+
   const getHBSCompletions = bindingCompletions => {
     return [
       hbAutocomplete([
@@ -266,6 +277,8 @@
 
     // Set the initial side panel
     sidePanel = sidePanelOptions[0]
+
+    mounted = true
   })
 </script>
 
diff --git a/packages/builder/src/components/common/bindings/DrawerBindableSlot.svelte b/packages/builder/src/components/common/bindings/DrawerBindableSlot.svelte
index 81c3bca3d3..e41a740229 100644
--- a/packages/builder/src/components/common/bindings/DrawerBindableSlot.svelte
+++ b/packages/builder/src/components/common/bindings/DrawerBindableSlot.svelte
@@ -20,7 +20,6 @@
   export let allowJS = true
   export let allowHelpers = true
   export let updateOnChange = true
-  export let drawerLeft
   export let type
   export let schema
 
@@ -170,14 +169,7 @@
       <Icon disabled={isJS} size="S" name="Close" />
     </div>
   {:else}
-    <slot
-      {label}
-      {disabled}
-      readonly={isJS}
-      value={isJS ? "(JavaScript function)" : readableValue}
-      {placeholder}
-      {updateOnChange}
-    />
+    <slot />
   {/if}
   {#if !disabled && type !== "formula" && !disabled && !attachmentTypes.includes(type)}
     <div
@@ -195,7 +187,7 @@
   on:drawerShow
   bind:this={bindingDrawer}
   title={title ?? placeholder ?? "Bindings"}
-  left={drawerLeft}
+  forceModal={true}
 >
   <Button cta slot="buttons" on:click={saveBinding}>Save</Button>
   <svelte:component