From 8d700655ba66aae7bc4397d1eabd06741fc7b5cf Mon Sep 17 00:00:00 2001
From: Duarte Velez Grilo <duartegrilo@gmail.com>
Date: Wed, 18 Oct 2023 16:06:51 +0100
Subject: [PATCH 01/47] Update manifest.json

Added used camera preference option.
---
 packages/client/manifest.json | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/packages/client/manifest.json b/packages/client/manifest.json
index 8d0a4e456f..f294dbbc80 100644
--- a/packages/client/manifest.json
+++ b/packages/client/manifest.json
@@ -3419,6 +3419,22 @@
           "value": "custom"
         }
       },
+      {
+        "type": "select",
+        "label": "Preferred camera",
+        "key": "preferredCamera",
+        "defaultValue": "environment",
+        "options": [
+          {
+            "label": "Front",
+            "value": "user"
+          },
+          {
+            "label": "Back",
+            "value": "environment"
+          }
+        ]
+      },
       {
         "type": "event",
         "label": "On change",

From 5e4821e6a5e22473e869bf71f2499649ae453d26 Mon Sep 17 00:00:00 2001
From: Duarte Velez Grilo <duartegrilo@gmail.com>
Date: Wed, 18 Oct 2023 16:12:12 +0100
Subject: [PATCH 02/47] Update CodeScannerField.svelte

Added preferred camera setting.
---
 .../client/src/components/app/forms/CodeScannerField.svelte     | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/packages/client/src/components/app/forms/CodeScannerField.svelte b/packages/client/src/components/app/forms/CodeScannerField.svelte
index c408f78d7c..1a57d8fbe2 100644
--- a/packages/client/src/components/app/forms/CodeScannerField.svelte
+++ b/packages/client/src/components/app/forms/CodeScannerField.svelte
@@ -14,6 +14,7 @@
   export let beepOnScan
   export let beepFrequency
   export let customFrequency
+  export let preferredCamera
 
   let fieldState
   let fieldApi
@@ -48,6 +49,7 @@
       {beepOnScan}
       {beepFrequency}
       {customFrequency}
+      {preferredCamera}
     />
   {/if}
 </Field>

From ad3a80c629e4748bc1f27bb4193f8912100b9d33 Mon Sep 17 00:00:00 2001
From: Duarte Velez Grilo <duartegrilo@gmail.com>
Date: Wed, 18 Oct 2023 16:13:50 +0100
Subject: [PATCH 03/47] Update CodeScanner.svelte

Added preferred camera setting.
---
 packages/client/src/components/app/forms/CodeScanner.svelte | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/packages/client/src/components/app/forms/CodeScanner.svelte b/packages/client/src/components/app/forms/CodeScanner.svelte
index 04d6919157..891154939f 100644
--- a/packages/client/src/components/app/forms/CodeScanner.svelte
+++ b/packages/client/src/components/app/forms/CodeScanner.svelte
@@ -11,6 +11,7 @@
   export let beepOnScan = false
   export let beepFrequency = 2637
   export let customFrequency = 1046
+  export let preferredCamera = "environment"
 
   const dispatch = createEventDispatcher()
 
@@ -20,7 +21,7 @@
   let cameraEnabled
   let cameraStarted = false
   let html5QrCode
-  let cameraSetting = { facingMode: "environment" }
+  let cameraSetting = { facingMode: "{preferredCamera}" }
   let cameraConfig = {
     fps: 25,
     qrbox: { width: 250, height: 250 },

From 92e091818b20113ecc332da17d227ca0247e1217 Mon Sep 17 00:00:00 2001
From: Duarte Velez Grilo <duartegrilo@gmail.com>
Date: Thu, 19 Oct 2023 11:11:10 +0100
Subject: [PATCH 04/47] Update CodeScanner.svelte

Removed quotes ("") from variable reference. I believe this is what is causing the test failure.
---
 packages/client/src/components/app/forms/CodeScanner.svelte | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/client/src/components/app/forms/CodeScanner.svelte b/packages/client/src/components/app/forms/CodeScanner.svelte
index 891154939f..f68f0791f6 100644
--- a/packages/client/src/components/app/forms/CodeScanner.svelte
+++ b/packages/client/src/components/app/forms/CodeScanner.svelte
@@ -21,7 +21,7 @@
   let cameraEnabled
   let cameraStarted = false
   let html5QrCode
-  let cameraSetting = { facingMode: "{preferredCamera}" }
+  let cameraSetting = { facingMode: {preferredCamera} }
   let cameraConfig = {
     fps: 25,
     qrbox: { width: 250, height: 250 },

From 02fb5f3865c9ebb453a5889cd0c459139b98140a Mon Sep 17 00:00:00 2001
From: Andrew Kingston <andrew@kingston.dev>
Date: Mon, 4 Sep 2023 08:14:31 +0100
Subject: [PATCH 05/47] Add prototype of form block field layout

---
 packages/client/manifest.json                 | 364 ++++++++++++++++++
 .../app/blocks/form/InnerFormBlock.svelte     |  35 +-
 .../app/forms/AttachmentField.svelte          |   2 +
 .../components/app/forms/DateTimeField.svelte |   2 +
 .../src/components/app/forms/Field.svelte     |  79 ++--
 .../app/forms/MultiFieldSelect.svelte         |   2 +
 .../components/app/forms/OptionsField.svelte  |   2 +
 .../components/app/forms/StringField.svelte   |   2 +
 8 files changed, 448 insertions(+), 40 deletions(-)

diff --git a/packages/client/manifest.json b/packages/client/manifest.json
index 8d0a4e456f..faf279194f 100644
--- a/packages/client/manifest.json
+++ b/packages/client/manifest.json
@@ -2597,6 +2597,34 @@
             "barTitle": "Justify text"
           }
         ]
+      },
+      {
+        "type": "select",
+        "label": "Layout",
+        "key": "span",
+        "defaultValue": 6,
+        "showInBar": true,
+        "barStyle": "buttons",
+        "options": [
+          {
+            "label": "1 column",
+            "value": 6,
+            "barIcon": "Stop",
+            "barTitle": "1 column"
+          },
+          {
+            "label": "2 columns",
+            "value": 3,
+            "barIcon": "ColumnTwoA",
+            "barTitle": "2 columns"
+          },
+          {
+            "label": "3 columns",
+            "value": 2,
+            "barIcon": "ViewColumn",
+            "barTitle": "3 columns"
+          }
+        ]
       }
     ]
   },
@@ -2654,6 +2682,34 @@
         "type": "validation/number",
         "label": "Validation",
         "key": "validation"
+      },
+      {
+        "type": "select",
+        "label": "Layout",
+        "key": "span",
+        "defaultValue": 6,
+        "showInBar": true,
+        "barStyle": "buttons",
+        "options": [
+          {
+            "label": "1 column",
+            "value": 6,
+            "barIcon": "Stop",
+            "barTitle": "1 column"
+          },
+          {
+            "label": "2 columns",
+            "value": 3,
+            "barIcon": "ColumnTwoA",
+            "barTitle": "2 columns"
+          },
+          {
+            "label": "3 columns",
+            "value": 2,
+            "barIcon": "ViewColumn",
+            "barTitle": "3 columns"
+          }
+        ]
       }
     ]
   },
@@ -2706,6 +2762,34 @@
         "label": "Disabled",
         "key": "disabled",
         "defaultValue": false
+      },
+      {
+        "type": "select",
+        "label": "Layout",
+        "key": "span",
+        "defaultValue": 6,
+        "showInBar": true,
+        "barStyle": "buttons",
+        "options": [
+          {
+            "label": "1 column",
+            "value": 6,
+            "barIcon": "Stop",
+            "barTitle": "1 column"
+          },
+          {
+            "label": "2 columns",
+            "value": 3,
+            "barIcon": "ColumnTwoA",
+            "barTitle": "2 columns"
+          },
+          {
+            "label": "3 columns",
+            "value": 2,
+            "barIcon": "ViewColumn",
+            "barTitle": "3 columns"
+          }
+        ]
       }
     ]
   },
@@ -2763,6 +2847,34 @@
         "type": "validation/string",
         "label": "Validation",
         "key": "validation"
+      },
+      {
+        "type": "select",
+        "label": "Layout",
+        "key": "span",
+        "defaultValue": 6,
+        "showInBar": true,
+        "barStyle": "buttons",
+        "options": [
+          {
+            "label": "1 column",
+            "value": 6,
+            "barIcon": "Stop",
+            "barTitle": "1 column"
+          },
+          {
+            "label": "2 columns",
+            "value": 3,
+            "barIcon": "ColumnTwoA",
+            "barTitle": "2 columns"
+          },
+          {
+            "label": "3 columns",
+            "value": 2,
+            "barIcon": "ViewColumn",
+            "barTitle": "3 columns"
+          }
+        ]
       }
     ]
   },
@@ -2931,6 +3043,34 @@
         "type": "validation/string",
         "label": "Validation",
         "key": "validation"
+      },
+      {
+        "type": "select",
+        "label": "Layout",
+        "key": "span",
+        "defaultValue": 6,
+        "showInBar": true,
+        "barStyle": "buttons",
+        "options": [
+          {
+            "label": "1 column",
+            "value": 6,
+            "barIcon": "Stop",
+            "barTitle": "1 column"
+          },
+          {
+            "label": "2 columns",
+            "value": 3,
+            "barIcon": "ColumnTwoA",
+            "barTitle": "2 columns"
+          },
+          {
+            "label": "3 columns",
+            "value": 2,
+            "barIcon": "ViewColumn",
+            "barTitle": "3 columns"
+          }
+        ]
       }
     ]
   },
@@ -3093,6 +3233,34 @@
         "type": "validation/array",
         "label": "Validation",
         "key": "validation"
+      },
+      {
+        "type": "select",
+        "label": "Layout",
+        "key": "span",
+        "defaultValue": 6,
+        "showInBar": true,
+        "barStyle": "buttons",
+        "options": [
+          {
+            "label": "1 column",
+            "value": 6,
+            "barIcon": "Stop",
+            "barTitle": "1 column"
+          },
+          {
+            "label": "2 columns",
+            "value": 3,
+            "barIcon": "ColumnTwoA",
+            "barTitle": "2 columns"
+          },
+          {
+            "label": "3 columns",
+            "value": 2,
+            "barIcon": "ViewColumn",
+            "barTitle": "3 columns"
+          }
+        ]
       }
     ]
   },
@@ -3173,6 +3341,34 @@
         "type": "validation/boolean",
         "label": "Validation",
         "key": "validation"
+      },
+      {
+        "type": "select",
+        "label": "Layout",
+        "key": "span",
+        "defaultValue": 6,
+        "showInBar": true,
+        "barStyle": "buttons",
+        "options": [
+          {
+            "label": "1 column",
+            "value": 6,
+            "barIcon": "Stop",
+            "barTitle": "1 column"
+          },
+          {
+            "label": "2 columns",
+            "value": 3,
+            "barIcon": "ColumnTwoA",
+            "barTitle": "2 columns"
+          },
+          {
+            "label": "3 columns",
+            "value": 2,
+            "barIcon": "ViewColumn",
+            "barTitle": "3 columns"
+          }
+        ]
       }
     ]
   },
@@ -3252,6 +3448,34 @@
         "type": "validation/string",
         "label": "Validation",
         "key": "validation"
+      },
+      {
+        "type": "select",
+        "label": "Layout",
+        "key": "span",
+        "defaultValue": 6,
+        "showInBar": true,
+        "barStyle": "buttons",
+        "options": [
+          {
+            "label": "1 column",
+            "value": 6,
+            "barIcon": "Stop",
+            "barTitle": "1 column"
+          },
+          {
+            "label": "2 columns",
+            "value": 3,
+            "barIcon": "ColumnTwoA",
+            "barTitle": "2 columns"
+          },
+          {
+            "label": "3 columns",
+            "value": 2,
+            "barIcon": "ViewColumn",
+            "barTitle": "3 columns"
+          }
+        ]
       }
     ]
   },
@@ -3333,6 +3557,34 @@
         "type": "validation/datetime",
         "label": "Validation",
         "key": "validation"
+      },
+      {
+        "type": "select",
+        "label": "Layout",
+        "key": "span",
+        "defaultValue": 6,
+        "showInBar": true,
+        "barStyle": "buttons",
+        "options": [
+          {
+            "label": "1 column",
+            "value": 6,
+            "barIcon": "Stop",
+            "barTitle": "1 column"
+          },
+          {
+            "label": "2 columns",
+            "value": 3,
+            "barIcon": "ColumnTwoA",
+            "barTitle": "2 columns"
+          },
+          {
+            "label": "3 columns",
+            "value": 2,
+            "barIcon": "ViewColumn",
+            "barTitle": "3 columns"
+          }
+        ]
       }
     ]
   },
@@ -3434,6 +3686,34 @@
         "type": "validation/string",
         "label": "Validation",
         "key": "validation"
+      },
+      {
+        "type": "select",
+        "label": "Layout",
+        "key": "span",
+        "defaultValue": 6,
+        "showInBar": true,
+        "barStyle": "buttons",
+        "options": [
+          {
+            "label": "1 column",
+            "value": 6,
+            "barIcon": "Stop",
+            "barTitle": "1 column"
+          },
+          {
+            "label": "2 columns",
+            "value": 3,
+            "barIcon": "ColumnTwoA",
+            "barTitle": "2 columns"
+          },
+          {
+            "label": "3 columns",
+            "value": 2,
+            "barIcon": "ViewColumn",
+            "barTitle": "3 columns"
+          }
+        ]
       }
     ]
   },
@@ -3610,6 +3890,34 @@
         "type": "validation/attachment",
         "label": "Validation",
         "key": "validation"
+      },
+      {
+        "type": "select",
+        "label": "Layout",
+        "key": "span",
+        "defaultValue": 6,
+        "showInBar": true,
+        "barStyle": "buttons",
+        "options": [
+          {
+            "label": "1 column",
+            "value": 6,
+            "barIcon": "Stop",
+            "barTitle": "1 column"
+          },
+          {
+            "label": "2 columns",
+            "value": 3,
+            "barIcon": "ColumnTwoA",
+            "barTitle": "2 columns"
+          },
+          {
+            "label": "3 columns",
+            "value": 2,
+            "barIcon": "ViewColumn",
+            "barTitle": "3 columns"
+          }
+        ]
       }
     ]
   },
@@ -3678,6 +3986,34 @@
         "label": "Disabled",
         "key": "disabled",
         "defaultValue": false
+      },
+      {
+        "type": "select",
+        "label": "Layout",
+        "key": "span",
+        "defaultValue": 6,
+        "showInBar": true,
+        "barStyle": "buttons",
+        "options": [
+          {
+            "label": "1 column",
+            "value": 6,
+            "barIcon": "Stop",
+            "barTitle": "1 column"
+          },
+          {
+            "label": "2 columns",
+            "value": 3,
+            "barIcon": "ColumnTwoA",
+            "barTitle": "2 columns"
+          },
+          {
+            "label": "3 columns",
+            "value": 2,
+            "barIcon": "ViewColumn",
+            "barTitle": "3 columns"
+          }
+        ]
       }
     ]
   },
@@ -3730,6 +4066,34 @@
         "label": "Disabled",
         "key": "disabled",
         "defaultValue": false
+      },
+      {
+        "type": "select",
+        "label": "Layout",
+        "key": "span",
+        "defaultValue": 6,
+        "showInBar": true,
+        "barStyle": "buttons",
+        "options": [
+          {
+            "label": "1 column",
+            "value": 6,
+            "barIcon": "Stop",
+            "barTitle": "1 column"
+          },
+          {
+            "label": "2 columns",
+            "value": 3,
+            "barIcon": "ColumnTwoA",
+            "barTitle": "2 columns"
+          },
+          {
+            "label": "3 columns",
+            "value": 2,
+            "barIcon": "ViewColumn",
+            "barTitle": "3 columns"
+          }
+        ]
       }
     ]
   },
diff --git a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
index ec5daa21b1..8e6984b182 100644
--- a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
+++ b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
@@ -2,6 +2,7 @@
   import BlockComponent from "components/BlockComponent.svelte"
   import Placeholder from "components/app/Placeholder.svelte"
   import { makePropSafe as safe } from "@budibase/string-templates"
+  import { getContext } from "svelte"
 
   export let dataSource
   export let actionUrl
@@ -32,6 +33,7 @@
     barcodeqr: "codescanner",
     bb_reference: "bbreferencefield",
   }
+  const context = getContext("context")
 
   let formId
 
@@ -213,16 +215,18 @@
         </BlockComponent>
       {/if}
       {#key fields}
-        <BlockComponent type="fieldgroup" props={{ labelPosition }} order={1}>
-          {#each fields as field, idx}
-            {#if getComponentForField(field) && field.active}
-              <BlockComponent
-                type={getComponentForField(field)}
-                props={getPropsForField(field)}
-                order={idx}
-              />
-            {/if}
-          {/each}
+        <BlockComponent type="container">
+          <div class="fields" class:mobile={$context.device.mobile}>
+            {#each fields as field, idx}
+              {#if getComponentForField(field) && field.active}
+                <BlockComponent
+                  type={getComponentForField(field)}
+                  props={getPropsForField(field)}
+                  order={idx}
+                />
+              {/if}
+            {/each}
+          </div>
         </BlockComponent>
       {/key}
     </BlockComponent>
@@ -232,3 +236,14 @@
     text="Choose your table and add some fields to your form to get started"
   />
 {/if}
+
+<style>
+  .fields {
+    display: grid;
+    grid-template-columns: repeat(6, 1fr);
+    gap: 8px 16px;
+  }
+  .fields.mobile :global(.spectrum-Form-item) {
+    grid-column: span 6 !important;
+  }
+</style>
diff --git a/packages/client/src/components/app/forms/AttachmentField.svelte b/packages/client/src/components/app/forms/AttachmentField.svelte
index e24115ebc0..bc788a726d 100644
--- a/packages/client/src/components/app/forms/AttachmentField.svelte
+++ b/packages/client/src/components/app/forms/AttachmentField.svelte
@@ -11,6 +11,7 @@
   export let extensions
   export let onChange
   export let maximum = undefined
+  export let span
 
   let fieldState
   let fieldApi
@@ -72,6 +73,7 @@
   {field}
   {disabled}
   {validation}
+  {span}
   type="attachment"
   bind:fieldState
   bind:fieldApi
diff --git a/packages/client/src/components/app/forms/DateTimeField.svelte b/packages/client/src/components/app/forms/DateTimeField.svelte
index 6bcd20d250..661c0c2fad 100644
--- a/packages/client/src/components/app/forms/DateTimeField.svelte
+++ b/packages/client/src/components/app/forms/DateTimeField.svelte
@@ -13,6 +13,7 @@
   export let validation
   export let defaultValue
   export let onChange
+  export let span
 
   let fieldState
   let fieldApi
@@ -31,6 +32,7 @@
   {disabled}
   {validation}
   {defaultValue}
+  {span}
   type="datetime"
   bind:fieldState
   bind:fieldApi
diff --git a/packages/client/src/components/app/forms/Field.svelte b/packages/client/src/components/app/forms/Field.svelte
index 5d4da5afef..d8b2c7a327 100644
--- a/packages/client/src/components/app/forms/Field.svelte
+++ b/packages/client/src/components/app/forms/Field.svelte
@@ -12,6 +12,7 @@
   export let type
   export let disabled = false
   export let validation
+  export let span = 6
 
   // Get contexts
   const formContext = getContext("form")
@@ -62,40 +63,58 @@
   })
 </script>
 
-<FieldGroupFallback>
-  <div class="spectrum-Form-item" use:styleable={$component.styles}>
-    {#key $component.editing}
-      <label
-        bind:this={labelNode}
-        contenteditable={$component.editing}
-        on:blur={$component.editing ? updateLabel : null}
-        class:hidden={!label}
-        for={fieldState?.fieldId}
-        class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${labelClass}`}
-      >
-        {label || " "}
-      </label>
-    {/key}
-    <div class="spectrum-Form-itemField">
-      {#if !formContext}
-        <Placeholder text="Form components need to be wrapped in a form" />
-      {:else if !fieldState}
-        <Placeholder />
-      {:else if schemaType && schemaType !== type && !["options", "longform"].includes(type)}
-        <Placeholder
-          text="This Field setting is the wrong data type for this component"
-        />
-      {:else}
-        <slot />
-        {#if fieldState.error}
-          <div class="error">{fieldState.error}</div>
-        {/if}
+<div
+  class="spectrum-Form-item"
+  class:span-2={span === 2}
+  class:span-3={span === 3}
+  class:span-6={span === 6 || !span}
+  use:styleable={$component.styles}
+  class:above={labelPos === "above"}
+>
+  {#key $component.editing}
+    <label
+      bind:this={labelNode}
+      contenteditable={$component.editing}
+      on:blur={$component.editing ? updateLabel : null}
+      class:hidden={!label}
+      for={fieldState?.fieldId}
+      class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${labelClass}`}
+    >
+      {label || " "}
+    </label>
+  {/key}
+  <div class="spectrum-Form-itemField">
+    {#if !formContext}
+      <Placeholder text="Form components need to be wrapped in a form" />
+    {:else if !fieldState}
+      <Placeholder />
+    {:else if schemaType && schemaType !== type && !["options", "longform"].includes(type)}
+      <Placeholder
+        text="This Field setting is the wrong data type for this component"
+      />
+    {:else}
+      <slot />
+      {#if fieldState.error}
+        <div class="error">{fieldState.error}</div>
       {/if}
-    </div>
+    {/if}
   </div>
-</FieldGroupFallback>
+</div>
 
 <style>
+  .spectrum-Form-item.span-2 {
+    grid-column: span 2;
+  }
+  .spectrum-Form-item.span-3 {
+    grid-column: span 3;
+  }
+  .spectrum-Form-item.span-6 {
+    grid-column: span 6;
+  }
+  .spectrum-Form-item.above {
+    display: flex;
+    flex-direction: column;
+  }
   label {
     white-space: nowrap;
   }
diff --git a/packages/client/src/components/app/forms/MultiFieldSelect.svelte b/packages/client/src/components/app/forms/MultiFieldSelect.svelte
index 88e1ec5a8e..f3983a2284 100644
--- a/packages/client/src/components/app/forms/MultiFieldSelect.svelte
+++ b/packages/client/src/components/app/forms/MultiFieldSelect.svelte
@@ -17,6 +17,7 @@
   export let onChange
   export let optionsType = "select"
   export let direction = "vertical"
+  export let span
 
   let fieldState
   let fieldApi
@@ -56,6 +57,7 @@
   {label}
   {disabled}
   {validation}
+  {span}
   defaultValue={expandedDefaultValue}
   type="array"
   bind:fieldState
diff --git a/packages/client/src/components/app/forms/OptionsField.svelte b/packages/client/src/components/app/forms/OptionsField.svelte
index 3c229c0509..bd8dc862ca 100644
--- a/packages/client/src/components/app/forms/OptionsField.svelte
+++ b/packages/client/src/components/app/forms/OptionsField.svelte
@@ -18,6 +18,7 @@
   export let direction = "vertical"
   export let onChange
   export let sort = true
+  export let span
 
   let fieldState
   let fieldApi
@@ -47,6 +48,7 @@
   {disabled}
   {validation}
   {defaultValue}
+  {span}
   type="options"
   bind:fieldState
   bind:fieldApi
diff --git a/packages/client/src/components/app/forms/StringField.svelte b/packages/client/src/components/app/forms/StringField.svelte
index 26136b5d8d..073353d500 100644
--- a/packages/client/src/components/app/forms/StringField.svelte
+++ b/packages/client/src/components/app/forms/StringField.svelte
@@ -11,6 +11,7 @@
   export let defaultValue = ""
   export let align
   export let onChange
+  export let span
 
   let fieldState
   let fieldApi
@@ -29,6 +30,7 @@
   {disabled}
   {validation}
   {defaultValue}
+  {span}
   type={type === "number" ? "number" : "string"}
   bind:fieldState
   bind:fieldApi

From 291ecd45fe993dafa2198d78b35b4d895e6ddd7a Mon Sep 17 00:00:00 2001
From: Andrew Kingston <andrew@kingston.dev>
Date: Tue, 5 Sep 2023 11:01:01 +0100
Subject: [PATCH 06/47] Allow form block fields to be clicked from within the
 preview

---
 .../builder/src/builderStore/previewEvents.js  | 13 +++++++++++++
 .../FieldConfiguration/EditFieldPopover.svelte | 18 +++++++++++++++++-
 .../[screenId]/_components/AppPreview.svelte   |  6 ++++++
 .../src/components/BlockComponent.svelte       |  7 ++++---
 .../client/src/components/Component.svelte     |  8 +++++++-
 .../app/blocks/form/InnerFormBlock.svelte      |  6 ++++++
 packages/client/src/index.js                   |  2 ++
 packages/client/src/stores/builder.js          |  4 ++++
 packages/client/src/utils/styleable.js         | 10 +++++++++-
 .../controllers/static/templates/preview.hbs   |  2 ++
 10 files changed, 70 insertions(+), 6 deletions(-)
 create mode 100644 packages/builder/src/builderStore/previewEvents.js

diff --git a/packages/builder/src/builderStore/previewEvents.js b/packages/builder/src/builderStore/previewEvents.js
new file mode 100644
index 0000000000..dfee80391f
--- /dev/null
+++ b/packages/builder/src/builderStore/previewEvents.js
@@ -0,0 +1,13 @@
+let subscribers = []
+
+export const onPreviewEvent = cb => {
+  subscribers.push(cb)
+
+  return () => {
+    subscribers = subscribers.filter(callback => callback !== cb)
+  }
+}
+
+export const emitPreviewEvent = event => {
+  subscribers.forEach(cb => cb(event))
+}
diff --git a/packages/builder/src/components/design/settings/controls/FieldConfiguration/EditFieldPopover.svelte b/packages/builder/src/components/design/settings/controls/FieldConfiguration/EditFieldPopover.svelte
index 7d2eaae478..29c1d21841 100644
--- a/packages/builder/src/components/design/settings/controls/FieldConfiguration/EditFieldPopover.svelte
+++ b/packages/builder/src/components/design/settings/controls/FieldConfiguration/EditFieldPopover.svelte
@@ -2,9 +2,10 @@
   import { Icon, Popover, Layout } from "@budibase/bbui"
   import { store } from "builderStore"
   import { cloneDeep } from "lodash/fp"
-  import { createEventDispatcher } from "svelte"
+  import { createEventDispatcher, onDestroy, onMount } from "svelte"
   import ComponentSettingsSection from "../../../../../pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte"
   import { getContext } from "svelte"
+  import { onPreviewEvent } from "builderStore/previewEvents"
 
   export let anchor
   export let field
@@ -61,6 +62,21 @@
 
     dispatch("change", update)
   }
+
+  const handlePreviewEvent = event => {
+    const { type, data } = event?.data || {}
+    if (type === "click-form-block-field") {
+      if (data.field === field.field) {
+        popover.show()
+        open = true
+      } else {
+        popover.hide()
+        open = false
+      }
+    }
+  }
+
+  onMount(() => onPreviewEvent(handlePreviewEvent))
 </script>
 
 <Icon
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 45fe005ceb..4934116104 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
@@ -14,6 +14,7 @@
   import ErrorSVG from "@budibase/frontend-core/assets/error.svg?raw"
   import { findComponent, findComponentPath } from "builderStore/componentUtils"
   import { isActive, goto } from "@roxi/routify"
+  import { emitPreviewEvent } from "builderStore/previewEvents"
 
   let iframe
   let layout
@@ -36,12 +37,14 @@
 
   // Determine selected component ID
   $: selectedComponentId = $store.selectedComponentId
+  $: selectedBlockComponentId = $store.selectedBlockComponentId
 
   $: previewData = {
     appId: $store.appId,
     layout,
     screen,
     selectedComponentId,
+    selectedBlockComponentId,
     theme: $store.theme,
     customTheme: $store.customTheme,
     previewDevice: $store.previewDevice,
@@ -93,6 +96,7 @@
     // Await the event handler
     try {
       await handleBudibaseEvent(message)
+      emitPreviewEvent(message)
     } catch (error) {
       notifications.error(error || "Error handling event from app preview")
     }
@@ -181,6 +185,8 @@
     } else if (type === "add-parent-component") {
       const { componentId, parentType } = data
       await store.actions.components.addParent(componentId, parentType)
+    } else if (type === "click-form-block-field") {
+      // Swallow and let this be handled by form block settings
     } else {
       console.warn(`Client sent unknown event type: ${type}`)
     }
diff --git a/packages/client/src/components/BlockComponent.svelte b/packages/client/src/components/BlockComponent.svelte
index c9516b0d71..b3b6a486c8 100644
--- a/packages/client/src/components/BlockComponent.svelte
+++ b/packages/client/src/components/BlockComponent.svelte
@@ -11,6 +11,7 @@
   export let name
   export let order = 0
   export let containsSlot = false
+  export let onClick = null
 
   // ID is only exposed as a prop so that it can be bound to from parent
   // block components
@@ -26,15 +27,15 @@
   $: parentId = $component?.id
   $: inBuilder = $builderStore.inBuilder
   $: instance = {
+    ...props,
     _component: getComponent(type),
     _id: id,
     _instanceName: getInstanceName(name, type),
+    _containsSlot: containsSlot,
     _styles: {
       ...styles,
       normal: styles?.normal || {},
     },
-    _containsSlot: containsSlot,
-    ...props,
   }
 
   // Register this block component if we're inside the builder so it can be
@@ -76,6 +77,6 @@
   })
 </script>
 
-<Component {instance} isBlock>
+<Component {instance} isBlock {onClick}>
   <slot />
 </Component>
diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte
index 03aab21d38..67fb06261f 100644
--- a/packages/client/src/components/Component.svelte
+++ b/packages/client/src/components/Component.svelte
@@ -35,6 +35,7 @@
   export let isLayout = false
   export let isRoot = false
   export let isBlock = false
+  export let onClick = null
 
   // Get parent contexts
   const context = getContext("context")
@@ -131,7 +132,10 @@
   // Interactive components can be selected, dragged and highlighted inside
   // the builder preview
   $: builderInteractive =
-    $builderStore.inBuilder && insideScreenslot && !isBlock && !instance.static
+    $builderStore.inBuilder &&
+    insideScreenslot &&
+    (!isBlock || onClick) &&
+    !instance.static
   $: devToolsInteractive = $devToolsStore.allowSelection && !isBlock
   $: interactive = !isRoot && (builderInteractive || devToolsInteractive)
   $: editing = editable && selected && $builderStore.editMode
@@ -194,6 +198,8 @@
       interactive,
       draggable,
       editable,
+      isBlock,
+      onClick,
     },
     empty: emptyState,
     selected,
diff --git a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
index 8e6984b182..b8abec874a 100644
--- a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
+++ b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
@@ -34,6 +34,7 @@
     bb_reference: "bbreferencefield",
   }
   const context = getContext("context")
+  const { builderStore } = getContext("sdk")
 
   let formId
 
@@ -223,6 +224,11 @@
                   type={getComponentForField(field)}
                   props={getPropsForField(field)}
                   order={idx}
+                  interactive
+                  name={field?.field}
+                  onClick={() => {
+                    builderStore.actions.clickFormBlockField(field?.field)
+                  }}
                 />
               {/if}
             {/each}
diff --git a/packages/client/src/index.js b/packages/client/src/index.js
index 1550ba4d7b..0cf6800560 100644
--- a/packages/client/src/index.js
+++ b/packages/client/src/index.js
@@ -31,6 +31,8 @@ const loadBudibase = async () => {
     layout: window["##BUDIBASE_PREVIEW_LAYOUT##"],
     screen: window["##BUDIBASE_PREVIEW_SCREEN##"],
     selectedComponentId: window["##BUDIBASE_SELECTED_COMPONENT_ID##"],
+    selectedBlockComponentId:
+      window["##BUDIBASE_SELECTED_BLOCK_COMPONENT_ID##"],
     previewId: window["##BUDIBASE_PREVIEW_ID##"],
     theme: window["##BUDIBASE_PREVIEW_THEME##"],
     customTheme: window["##BUDIBASE_PREVIEW_CUSTOM_THEME##"],
diff --git a/packages/client/src/stores/builder.js b/packages/client/src/stores/builder.js
index 036558e8b2..c1865d608f 100644
--- a/packages/client/src/stores/builder.js
+++ b/packages/client/src/stores/builder.js
@@ -8,6 +8,7 @@ const createBuilderStore = () => {
     inBuilder: false,
     screen: null,
     selectedComponentId: null,
+    selectedBlockComponentId: null,
     editMode: false,
     previewId: null,
     theme: null,
@@ -35,6 +36,9 @@ const createBuilderStore = () => {
       devToolsStore.actions.setAllowSelection(false)
       eventStore.actions.dispatchEvent("select-component", { id })
     },
+    clickFormBlockField: field => {
+      eventStore.actions.dispatchEvent("click-form-block-field", { field })
+    },
     updateProp: (prop, value) => {
       eventStore.actions.dispatchEvent("update-prop", { prop, value })
     },
diff --git a/packages/client/src/utils/styleable.js b/packages/client/src/utils/styleable.js
index f48eece89b..31c35c4483 100644
--- a/packages/client/src/utils/styleable.js
+++ b/packages/client/src/utils/styleable.js
@@ -40,6 +40,7 @@ export const styleable = (node, styles = {}) => {
 
     const componentId = newStyles.id
     const customStyles = newStyles.custom || ""
+    const { isBlock, onClick } = newStyles
     const normalStyles = { ...baseStyles, ...newStyles.normal }
     const hoverStyles = {
       ...normalStyles,
@@ -67,7 +68,11 @@ export const styleable = (node, styles = {}) => {
     // Handler to select a component in the builder when clicking it in the
     // builder preview
     selectComponent = event => {
-      builderStore.actions.selectComponent(componentId)
+      if (isBlock && onClick) {
+        onClick()
+      } else {
+        builderStore.actions.selectComponent(componentId, isBlock)
+      }
       event.preventDefault()
       event.stopPropagation()
       return false
@@ -76,6 +81,9 @@ export const styleable = (node, styles = {}) => {
     // Handler to start editing a component (if applicable) when double
     // clicking in the builder preview
     editComponent = event => {
+      if (isBlock) {
+        return
+      }
       if (newStyles.interactive && newStyles.editable) {
         builderStore.actions.setEditMode(true)
       }
diff --git a/packages/server/src/api/controllers/static/templates/preview.hbs b/packages/server/src/api/controllers/static/templates/preview.hbs
index 31bf0762e0..f28469ca22 100644
--- a/packages/server/src/api/controllers/static/templates/preview.hbs
+++ b/packages/server/src/api/controllers/static/templates/preview.hbs
@@ -63,6 +63,7 @@
       // Extract data from message
       const {
         selectedComponentId,
+        selectedBlockComponentId,
         layout,
         screen,
         appId,
@@ -81,6 +82,7 @@
       window["##BUDIBASE_PREVIEW_LAYOUT##"] = layout
       window["##BUDIBASE_PREVIEW_SCREEN##"] = screen
       window["##BUDIBASE_SELECTED_COMPONENT_ID##"] = selectedComponentId
+      window["##BUDIBASE_SELECTED_BLOCK_COMPONENT_ID##"] = selectedBlockComponentId
       window["##BUDIBASE_PREVIEW_ID##"] = Math.random()
       window["##BUDIBASE_PREVIEW_THEME##"] = theme
       window["##BUDIBASE_PREVIEW_CUSTOM_THEME##"] = customTheme

From 43663c6fbfc69fb6b09c5f5fa5a20de5ee91b099 Mon Sep 17 00:00:00 2001
From: Andrew Kingston <andrew@kingston.dev>
Date: Tue, 5 Sep 2023 11:02:10 +0100
Subject: [PATCH 07/47] Prevent dragging interactive block components

---
 packages/client/src/components/Component.svelte | 1 +
 1 file changed, 1 insertion(+)

diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte
index 67fb06261f..467fee0468 100644
--- a/packages/client/src/components/Component.svelte
+++ b/packages/client/src/components/Component.svelte
@@ -144,6 +144,7 @@
     interactive &&
     !isLayout &&
     !isRoot &&
+    !isBlock &&
     definition?.draggable !== false
   $: droppable = interactive
   $: builderHidden =

From bffa8b1f0fbc9851bd8e194b28892eadf9856809 Mon Sep 17 00:00:00 2001
From: Andrew Kingston <andrew@kingston.dev>
Date: Tue, 5 Sep 2023 11:11:43 +0100
Subject: [PATCH 08/47] Ensure only the selected block supports clicking on
 inner interactive components

---
 packages/client/src/components/BlockComponent.svelte | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/packages/client/src/components/BlockComponent.svelte b/packages/client/src/components/BlockComponent.svelte
index b3b6a486c8..8af3a8b75e 100644
--- a/packages/client/src/components/BlockComponent.svelte
+++ b/packages/client/src/components/BlockComponent.svelte
@@ -26,6 +26,7 @@
   $: id = `${block.id}-${context ?? rand}`
   $: parentId = $component?.id
   $: inBuilder = $builderStore.inBuilder
+  $: blockSelected = $builderStore.selectedComponentId === block.id
   $: instance = {
     ...props,
     _component: getComponent(type),
@@ -77,6 +78,6 @@
   })
 </script>
 
-<Component {instance} isBlock {onClick}>
+<Component {instance} isBlock onClick={blockSelected ? onClick : null}>
   <slot />
 </Component>

From 34a752dd41310ac778f83e7d47117f6f13599df4 Mon Sep 17 00:00:00 2001
From: Duarte Velez Grilo <duartegrilo@gmail.com>
Date: Fri, 20 Oct 2023 12:46:43 +0100
Subject: [PATCH 09/47] Update CodeScanner.svelte

Updated the code as suggested. Thanks!
---
 packages/client/src/components/app/forms/CodeScanner.svelte | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/client/src/components/app/forms/CodeScanner.svelte b/packages/client/src/components/app/forms/CodeScanner.svelte
index f68f0791f6..f2e32004cf 100644
--- a/packages/client/src/components/app/forms/CodeScanner.svelte
+++ b/packages/client/src/components/app/forms/CodeScanner.svelte
@@ -21,7 +21,7 @@
   let cameraEnabled
   let cameraStarted = false
   let html5QrCode
-  let cameraSetting = { facingMode: {preferredCamera} }
+  let cameraSetting = { facingMode: preferredCamera }
   let cameraConfig = {
     fps: 25,
     qrbox: { width: 250, height: 250 },

From 0e1cffa65657fc291078c4e9257473be404b778c Mon Sep 17 00:00:00 2001
From: Andrew Kingston <andrew@kingston.dev>
Date: Mon, 23 Oct 2023 09:20:05 +0100
Subject: [PATCH 10/47] Add span setting to relationship fielsd

---
 .../client/src/components/app/forms/RelationshipField.svelte    | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/packages/client/src/components/app/forms/RelationshipField.svelte b/packages/client/src/components/app/forms/RelationshipField.svelte
index 544a1a8434..cd97790e3f 100644
--- a/packages/client/src/components/app/forms/RelationshipField.svelte
+++ b/packages/client/src/components/app/forms/RelationshipField.svelte
@@ -18,6 +18,7 @@
   export let filter
   export let datasourceType = "table"
   export let primaryDisplay
+  export let span
 
   let fieldState
   let fieldApi
@@ -186,6 +187,7 @@
   {validation}
   defaultValue={expandedDefaultValue}
   {type}
+  {span}
   bind:fieldState
   bind:fieldApi
   bind:fieldSchema

From 2bf78db0a026dd7565e6ecdbba99011c6ca71d78 Mon Sep 17 00:00:00 2001
From: Andrew Kingston <andrew@kingston.dev>
Date: Fri, 27 Oct 2023 08:11:09 +0100
Subject: [PATCH 11/47] Add field layout setting to BB reference field

---
 packages/client/manifest.json | 28 ++++++++++++++++++++++++++++
 1 file changed, 28 insertions(+)

diff --git a/packages/client/manifest.json b/packages/client/manifest.json
index 1fc25c5184..798e33f58d 100644
--- a/packages/client/manifest.json
+++ b/packages/client/manifest.json
@@ -6104,6 +6104,34 @@
         "label": "Disabled",
         "key": "disabled",
         "defaultValue": false
+      },
+      {
+        "type": "select",
+        "label": "Layout",
+        "key": "span",
+        "defaultValue": 6,
+        "showInBar": true,
+        "barStyle": "buttons",
+        "options": [
+          {
+            "label": "1 column",
+            "value": 6,
+            "barIcon": "Stop",
+            "barTitle": "1 column"
+          },
+          {
+            "label": "2 columns",
+            "value": 3,
+            "barIcon": "ColumnTwoA",
+            "barTitle": "2 columns"
+          },
+          {
+            "label": "3 columns",
+            "value": 2,
+            "barIcon": "ViewColumn",
+            "barTitle": "3 columns"
+          }
+        ]
       }
     ]
   }

From 77cd7233152c6b84c6910c38045c1be5a89029d6 Mon Sep 17 00:00:00 2001
From: Andrew Kingston <andrew@kingston.dev>
Date: Fri, 27 Oct 2023 08:14:55 +0100
Subject: [PATCH 12/47] Remove label alignment option from form block to
 support custom field layouts

---
 packages/client/manifest.json                    | 16 ----------------
 .../components/app/blocks/form/FormBlock.svelte  |  2 --
 .../app/blocks/form/InnerFormBlock.svelte        |  1 -
 3 files changed, 19 deletions(-)

diff --git a/packages/client/manifest.json b/packages/client/manifest.json
index 798e33f58d..48f3f19203 100644
--- a/packages/client/manifest.json
+++ b/packages/client/manifest.json
@@ -5755,22 +5755,6 @@
         "section": true,
         "name": "Fields",
         "settings": [
-          {
-            "type": "select",
-            "label": "Align labels",
-            "key": "labelPosition",
-            "defaultValue": "left",
-            "options": [
-              {
-                "label": "Left",
-                "value": "left"
-              },
-              {
-                "label": "Above",
-                "value": "above"
-              }
-            ]
-          },
           {
             "type": "select",
             "label": "Size",
diff --git a/packages/client/src/components/app/blocks/form/FormBlock.svelte b/packages/client/src/components/app/blocks/form/FormBlock.svelte
index f905227af9..e4d3b55eff 100644
--- a/packages/client/src/components/app/blocks/form/FormBlock.svelte
+++ b/packages/client/src/components/app/blocks/form/FormBlock.svelte
@@ -10,7 +10,6 @@
   export let size
   export let disabled
   export let fields
-  export let labelPosition
   export let title
   export let description
   export let showDeleteButton
@@ -97,7 +96,6 @@
     size,
     disabled,
     fields: fieldsOrDefault,
-    labelPosition,
     title,
     description,
     saveButtonLabel: saveLabel,
diff --git a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
index eb93279397..60a3522216 100644
--- a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
+++ b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
@@ -10,7 +10,6 @@
   export let size
   export let disabled
   export let fields
-  export let labelPosition
   export let title
   export let description
   export let saveButtonLabel

From e2c2f5e2a6363a67c597e909c8e350e51b0a00cb Mon Sep 17 00:00:00 2001
From: Andrew Kingston <andrew@kingston.dev>
Date: Fri, 27 Oct 2023 08:50:26 +0100
Subject: [PATCH 13/47] Fix some issues with dropzone height and ensure field
 alignment settings are ignored unless used inside a form block

---
 packages/bbui/src/Form/Core/Dropzone.svelte   | 13 ++++---
 .../app/blocks/form/InnerFormBlock.svelte     |  2 +-
 .../app/forms/AttachmentField.svelte          | 38 ++++++++-----------
 .../src/components/app/forms/Field.svelte     |  6 +--
 4 files changed, 26 insertions(+), 33 deletions(-)

diff --git a/packages/bbui/src/Form/Core/Dropzone.svelte b/packages/bbui/src/Form/Core/Dropzone.svelte
index e9ee75bd8b..e2e25b4a04 100644
--- a/packages/bbui/src/Form/Core/Dropzone.svelte
+++ b/packages/bbui/src/Form/Core/Dropzone.svelte
@@ -384,7 +384,7 @@
   }
   .compact .placeholder,
   .compact img {
-    margin: 10px 16px;
+    margin: 8px 16px;
   }
   .compact img {
     height: 90px;
@@ -454,6 +454,12 @@
     color: var(--red);
   }
 
+  .spectrum-Dropzone {
+    height: 220px;
+  }
+  .compact .spectrum-Dropzone {
+    height: 40px;
+  }
   .spectrum-Dropzone.disabled {
     pointer-events: none;
     background-color: var(--spectrum-global-color-gray-200);
@@ -461,10 +467,6 @@
   .disabled .spectrum-Heading--sizeL {
     color: var(--spectrum-alias-text-color-disabled);
   }
-  .compact .spectrum-Dropzone {
-    padding-top: 8px;
-    padding-bottom: 8px;
-  }
   .compact .spectrum-IllustratedMessage-description {
     margin: 0;
   }
@@ -475,7 +477,6 @@
     flex-wrap: wrap;
     justify-content: center;
   }
-
   .tag {
     margin-top: 8px;
   }
diff --git a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
index 60a3522216..502b3e0569 100644
--- a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
+++ b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
@@ -233,7 +233,7 @@
       {/if}
       {#key fields}
         <BlockComponent type="container">
-          <div class="fields" class:mobile={$context.device.mobile}>
+          <div class="form-block fields" class:mobile={$context.device.mobile}>
             {#each fields as field, idx}
               {#if getComponentForField(field) && field.active}
                 <BlockComponent
diff --git a/packages/client/src/components/app/forms/AttachmentField.svelte b/packages/client/src/components/app/forms/AttachmentField.svelte
index bc788a726d..f28d4801d4 100644
--- a/packages/client/src/components/app/forms/AttachmentField.svelte
+++ b/packages/client/src/components/app/forms/AttachmentField.svelte
@@ -79,27 +79,19 @@
   bind:fieldApi
   defaultValue={[]}
 >
-  <div class="minHeightWrapper">
-    {#if fieldState}
-      <CoreDropzone
-        value={fieldState.value}
-        disabled={fieldState.disabled}
-        error={fieldState.error}
-        on:change={handleChange}
-        {processFiles}
-        {deleteAttachments}
-        {handleFileTooLarge}
-        {handleTooManyFiles}
-        {maximum}
-        {extensions}
-        {compact}
-      />
-    {/if}
-  </div>
+  {#if fieldState}
+    <CoreDropzone
+      value={fieldState.value}
+      disabled={fieldState.disabled}
+      error={fieldState.error}
+      on:change={handleChange}
+      {processFiles}
+      {deleteAttachments}
+      {handleFileTooLarge}
+      {handleTooManyFiles}
+      {maximum}
+      {extensions}
+      {compact}
+    />
+  {/if}
 </Field>
-
-<style>
-  .minHeightWrapper {
-    min-height: 80px;
-  }
-</style>
diff --git a/packages/client/src/components/app/forms/Field.svelte b/packages/client/src/components/app/forms/Field.svelte
index d8b2c7a327..83db76b473 100644
--- a/packages/client/src/components/app/forms/Field.svelte
+++ b/packages/client/src/components/app/forms/Field.svelte
@@ -102,13 +102,13 @@
 </div>
 
 <style>
-  .spectrum-Form-item.span-2 {
+  :global(.form-block .spectrum-Form-item.span-2) {
     grid-column: span 2;
   }
-  .spectrum-Form-item.span-3 {
+  :global(.form-block .spectrum-Form-item.span-3) {
     grid-column: span 3;
   }
-  .spectrum-Form-item.span-6 {
+  :global(.form-block .spectrum-Form-item.span-6) {
     grid-column: span 6;
   }
   .spectrum-Form-item.above {

From b1f233784916f4b089c9af3a708a5379a90205ac Mon Sep 17 00:00:00 2001
From: Andrew Kingston <andrew@kingston.dev>
Date: Fri, 27 Oct 2023 11:42:01 +0100
Subject: [PATCH 14/47] Hide field layout settings for normal field components,
 and only show them when editing from within a form block

---
 .../EditFieldPopover.svelte                   |  1 +
 .../Component/ComponentSettingsSection.svelte | 25 +++++++++++++++----
 packages/client/manifest.json                 | 14 +++++++++++
 3 files changed, 35 insertions(+), 5 deletions(-)

diff --git a/packages/builder/src/components/design/settings/controls/FieldConfiguration/EditFieldPopover.svelte b/packages/builder/src/components/design/settings/controls/FieldConfiguration/EditFieldPopover.svelte
index 29c1d21841..e6fb82bedd 100644
--- a/packages/builder/src/components/design/settings/controls/FieldConfiguration/EditFieldPopover.svelte
+++ b/packages/builder/src/components/design/settings/controls/FieldConfiguration/EditFieldPopover.svelte
@@ -134,6 +134,7 @@
         <span>{field.field}</span>
       </div>
       <ComponentSettingsSection
+        includeHidden
         componentInstance={pseudoComponentInstance}
         componentDefinition={parsedComponentDef}
         isScreen={false}
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 f833464d8c..f78aeaa192 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
@@ -16,10 +16,16 @@
   export let isScreen = false
   export let onUpdateSetting
   export let showSectionTitle = true
+  export let includeHidden = false
 
-  $: sections = getSections(componentInstance, componentDefinition, isScreen)
+  $: sections = getSections(
+    componentInstance,
+    componentDefinition,
+    isScreen,
+    includeHidden
+  )
 
-  const getSections = (instance, definition, isScreen) => {
+  const getSections = (instance, definition, isScreen, includeHidden) => {
     const settings = definition?.settings ?? []
     const generalSettings = settings.filter(setting => !setting.section)
     const customSections = settings.filter(setting => setting.section)
@@ -38,7 +44,12 @@
         return
       }
       section.settings.forEach(setting => {
-        setting.visible = canRenderControl(instance, setting, isScreen)
+        setting.visible = canRenderControl(
+          instance,
+          setting,
+          isScreen,
+          includeHidden
+        )
       })
       section.visible =
         section.name === "General" ||
@@ -108,16 +119,20 @@
     })
   }
 
-  const canRenderControl = (instance, setting, isScreen) => {
+  const canRenderControl = (instance, setting, isScreen, includeHidden) => {
     // Prevent rendering on click setting for screens
     if (setting?.type === "event" && isScreen) {
       return false
     }
+    // Check we have a component to render for this setting
     const control = getComponentForSetting(setting)
     if (!control) {
       return false
     }
-
+    // Check if setting is hidden
+    if (setting.hidden && !includeHidden) {
+      return false
+    }
     return shouldDisplay(instance, setting)
   }
 </script>
diff --git a/packages/client/manifest.json b/packages/client/manifest.json
index 48f3f19203..c74ec7dea8 100644
--- a/packages/client/manifest.json
+++ b/packages/client/manifest.json
@@ -2603,6 +2603,7 @@
         "label": "Layout",
         "key": "span",
         "defaultValue": 6,
+        "hidden": true,
         "showInBar": true,
         "barStyle": "buttons",
         "options": [
@@ -2688,6 +2689,7 @@
         "label": "Layout",
         "key": "span",
         "defaultValue": 6,
+        "hidden": true,
         "showInBar": true,
         "barStyle": "buttons",
         "options": [
@@ -2768,6 +2770,7 @@
         "label": "Layout",
         "key": "span",
         "defaultValue": 6,
+        "hidden": true,
         "showInBar": true,
         "barStyle": "buttons",
         "options": [
@@ -2853,6 +2856,7 @@
         "label": "Layout",
         "key": "span",
         "defaultValue": 6,
+        "hidden": true,
         "showInBar": true,
         "barStyle": "buttons",
         "options": [
@@ -3049,6 +3053,7 @@
         "label": "Layout",
         "key": "span",
         "defaultValue": 6,
+        "hidden": true,
         "showInBar": true,
         "barStyle": "buttons",
         "options": [
@@ -3239,6 +3244,7 @@
         "label": "Layout",
         "key": "span",
         "defaultValue": 6,
+        "hidden": true,
         "showInBar": true,
         "barStyle": "buttons",
         "options": [
@@ -3347,6 +3353,7 @@
         "label": "Layout",
         "key": "span",
         "defaultValue": 6,
+        "hidden": true,
         "showInBar": true,
         "barStyle": "buttons",
         "options": [
@@ -3454,6 +3461,7 @@
         "label": "Layout",
         "key": "span",
         "defaultValue": 6,
+        "hidden": true,
         "showInBar": true,
         "barStyle": "buttons",
         "options": [
@@ -3563,6 +3571,7 @@
         "label": "Layout",
         "key": "span",
         "defaultValue": 6,
+        "hidden": true,
         "showInBar": true,
         "barStyle": "buttons",
         "options": [
@@ -3692,6 +3701,7 @@
         "label": "Layout",
         "key": "span",
         "defaultValue": 6,
+        "hidden": true,
         "showInBar": true,
         "barStyle": "buttons",
         "options": [
@@ -3896,6 +3906,7 @@
         "label": "Layout",
         "key": "span",
         "defaultValue": 6,
+        "hidden": true,
         "showInBar": true,
         "barStyle": "buttons",
         "options": [
@@ -3992,6 +4003,7 @@
         "label": "Layout",
         "key": "span",
         "defaultValue": 6,
+        "hidden": true,
         "showInBar": true,
         "barStyle": "buttons",
         "options": [
@@ -4072,6 +4084,7 @@
         "label": "Layout",
         "key": "span",
         "defaultValue": 6,
+        "hidden": true,
         "showInBar": true,
         "barStyle": "buttons",
         "options": [
@@ -6094,6 +6107,7 @@
         "label": "Layout",
         "key": "span",
         "defaultValue": 6,
+        "hidden": true,
         "showInBar": true,
         "barStyle": "buttons",
         "options": [

From 77fa373a9077da9bbbcf41451ac78387ade4dd70 Mon Sep 17 00:00:00 2001
From: Andrew Kingston <andrew@kingston.dev>
Date: Fri, 27 Oct 2023 15:09:17 +0100
Subject: [PATCH 15/47] Remove ability to select fields in preview because it
 is not compatible with field configuration changes

---
 .../builder/src/builderStore/previewEvents.js | 13 ----------
 .../controls/EditComponentPopover.svelte      | 25 +++----------------
 .../[screenId]/_components/AppPreview.svelte  |  6 -----
 .../src/components/BlockComponent.svelte      |  4 +--
 .../client/src/components/Component.svelte    |  7 +-----
 packages/client/src/index.js                  |  2 --
 packages/client/src/stores/builder.js         |  4 ---
 packages/client/src/utils/styleable.js        |  8 ++----
 .../controllers/static/templates/preview.hbs  |  2 --
 9 files changed, 8 insertions(+), 63 deletions(-)
 delete mode 100644 packages/builder/src/builderStore/previewEvents.js

diff --git a/packages/builder/src/builderStore/previewEvents.js b/packages/builder/src/builderStore/previewEvents.js
deleted file mode 100644
index dfee80391f..0000000000
--- a/packages/builder/src/builderStore/previewEvents.js
+++ /dev/null
@@ -1,13 +0,0 @@
-let subscribers = []
-
-export const onPreviewEvent = cb => {
-  subscribers.push(cb)
-
-  return () => {
-    subscribers = subscribers.filter(callback => callback !== cb)
-  }
-}
-
-export const emitPreviewEvent = event => {
-  subscribers.forEach(cb => cb(event))
-}
diff --git a/packages/builder/src/components/design/settings/controls/EditComponentPopover.svelte b/packages/builder/src/components/design/settings/controls/EditComponentPopover.svelte
index d963ad04ed..26c1ced502 100644
--- a/packages/builder/src/components/design/settings/controls/EditComponentPopover.svelte
+++ b/packages/builder/src/components/design/settings/controls/EditComponentPopover.svelte
@@ -2,10 +2,9 @@
   import { Icon, Popover, Layout } from "@budibase/bbui"
   import { store } from "builderStore"
   import { cloneDeep } from "lodash/fp"
-  import { createEventDispatcher, onMount } from "svelte"
+  import { createEventDispatcher } from "svelte"
   import ComponentSettingsSection from "../../../../pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte"
   import { getContext } from "svelte"
-  import { onPreviewEvent } from "builderStore/previewEvents"
 
   export let anchor
   export let componentInstance
@@ -21,7 +20,7 @@
   let open = false
 
   // Auto hide the component when another item is selected
-  $: if (open && $draggable.selected != componentInstance._id) {
+  $: if (open && $draggable.selected !== componentInstance._id) {
     popover.hide()
   }
 
@@ -79,22 +78,6 @@
 
     return { ...cfg, left, top }
   }
-
-  const handlePreviewEvent = event => {
-    const { type, data } = event?.data || {}
-    if (type === "click-form-block-field") {
-      console.log(data.field)
-      if (data.field === "asdasd") {
-        popover.show()
-        open = true
-      } else {
-        popover.hide()
-        open = false
-      }
-    }
-  }
-
-  onMount(() => onPreviewEvent(handlePreviewEvent))
 </script>
 
 <Icon
@@ -117,13 +100,13 @@
   }}
   on:close={() => {
     open = false
-    if ($draggable.selected == componentInstance._id) {
+    if ($draggable.selected === componentInstance._id) {
       $draggable.actions.select()
     }
   }}
   {anchor}
   align="left-outside"
-  showPopover={drawers.length == 0}
+  showPopover={drawers.length === 0}
   clickOutsideOverride={drawers.length > 0}
   maxHeight={600}
   handlePostionUpdate={customPositionHandler}
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 4934116104..45fe005ceb 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
@@ -14,7 +14,6 @@
   import ErrorSVG from "@budibase/frontend-core/assets/error.svg?raw"
   import { findComponent, findComponentPath } from "builderStore/componentUtils"
   import { isActive, goto } from "@roxi/routify"
-  import { emitPreviewEvent } from "builderStore/previewEvents"
 
   let iframe
   let layout
@@ -37,14 +36,12 @@
 
   // Determine selected component ID
   $: selectedComponentId = $store.selectedComponentId
-  $: selectedBlockComponentId = $store.selectedBlockComponentId
 
   $: previewData = {
     appId: $store.appId,
     layout,
     screen,
     selectedComponentId,
-    selectedBlockComponentId,
     theme: $store.theme,
     customTheme: $store.customTheme,
     previewDevice: $store.previewDevice,
@@ -96,7 +93,6 @@
     // Await the event handler
     try {
       await handleBudibaseEvent(message)
-      emitPreviewEvent(message)
     } catch (error) {
       notifications.error(error || "Error handling event from app preview")
     }
@@ -185,8 +181,6 @@
     } else if (type === "add-parent-component") {
       const { componentId, parentType } = data
       await store.actions.components.addParent(componentId, parentType)
-    } else if (type === "click-form-block-field") {
-      // Swallow and let this be handled by form block settings
     } else {
       console.warn(`Client sent unknown event type: ${type}`)
     }
diff --git a/packages/client/src/components/BlockComponent.svelte b/packages/client/src/components/BlockComponent.svelte
index 8af3a8b75e..12555a7fb5 100644
--- a/packages/client/src/components/BlockComponent.svelte
+++ b/packages/client/src/components/BlockComponent.svelte
@@ -11,7 +11,6 @@
   export let name
   export let order = 0
   export let containsSlot = false
-  export let onClick = null
 
   // ID is only exposed as a prop so that it can be bound to from parent
   // block components
@@ -26,7 +25,6 @@
   $: id = `${block.id}-${context ?? rand}`
   $: parentId = $component?.id
   $: inBuilder = $builderStore.inBuilder
-  $: blockSelected = $builderStore.selectedComponentId === block.id
   $: instance = {
     ...props,
     _component: getComponent(type),
@@ -78,6 +76,6 @@
   })
 </script>
 
-<Component {instance} isBlock onClick={blockSelected ? onClick : null}>
+<Component {instance} isBlock>
   <slot />
 </Component>
diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte
index 467fee0468..63ce8dc152 100644
--- a/packages/client/src/components/Component.svelte
+++ b/packages/client/src/components/Component.svelte
@@ -35,7 +35,6 @@
   export let isLayout = false
   export let isRoot = false
   export let isBlock = false
-  export let onClick = null
 
   // Get parent contexts
   const context = getContext("context")
@@ -132,10 +131,7 @@
   // Interactive components can be selected, dragged and highlighted inside
   // the builder preview
   $: builderInteractive =
-    $builderStore.inBuilder &&
-    insideScreenslot &&
-    (!isBlock || onClick) &&
-    !instance.static
+    $builderStore.inBuilder && insideScreenslot && !isBlock && !instance.static
   $: devToolsInteractive = $devToolsStore.allowSelection && !isBlock
   $: interactive = !isRoot && (builderInteractive || devToolsInteractive)
   $: editing = editable && selected && $builderStore.editMode
@@ -200,7 +196,6 @@
       draggable,
       editable,
       isBlock,
-      onClick,
     },
     empty: emptyState,
     selected,
diff --git a/packages/client/src/index.js b/packages/client/src/index.js
index 0cf6800560..1550ba4d7b 100644
--- a/packages/client/src/index.js
+++ b/packages/client/src/index.js
@@ -31,8 +31,6 @@ const loadBudibase = async () => {
     layout: window["##BUDIBASE_PREVIEW_LAYOUT##"],
     screen: window["##BUDIBASE_PREVIEW_SCREEN##"],
     selectedComponentId: window["##BUDIBASE_SELECTED_COMPONENT_ID##"],
-    selectedBlockComponentId:
-      window["##BUDIBASE_SELECTED_BLOCK_COMPONENT_ID##"],
     previewId: window["##BUDIBASE_PREVIEW_ID##"],
     theme: window["##BUDIBASE_PREVIEW_THEME##"],
     customTheme: window["##BUDIBASE_PREVIEW_CUSTOM_THEME##"],
diff --git a/packages/client/src/stores/builder.js b/packages/client/src/stores/builder.js
index c1865d608f..036558e8b2 100644
--- a/packages/client/src/stores/builder.js
+++ b/packages/client/src/stores/builder.js
@@ -8,7 +8,6 @@ const createBuilderStore = () => {
     inBuilder: false,
     screen: null,
     selectedComponentId: null,
-    selectedBlockComponentId: null,
     editMode: false,
     previewId: null,
     theme: null,
@@ -36,9 +35,6 @@ const createBuilderStore = () => {
       devToolsStore.actions.setAllowSelection(false)
       eventStore.actions.dispatchEvent("select-component", { id })
     },
-    clickFormBlockField: field => {
-      eventStore.actions.dispatchEvent("click-form-block-field", { field })
-    },
     updateProp: (prop, value) => {
       eventStore.actions.dispatchEvent("update-prop", { prop, value })
     },
diff --git a/packages/client/src/utils/styleable.js b/packages/client/src/utils/styleable.js
index 31c35c4483..3fccae0be5 100644
--- a/packages/client/src/utils/styleable.js
+++ b/packages/client/src/utils/styleable.js
@@ -40,7 +40,7 @@ export const styleable = (node, styles = {}) => {
 
     const componentId = newStyles.id
     const customStyles = newStyles.custom || ""
-    const { isBlock, onClick } = newStyles
+    const { isBlock } = newStyles
     const normalStyles = { ...baseStyles, ...newStyles.normal }
     const hoverStyles = {
       ...normalStyles,
@@ -68,11 +68,7 @@ export const styleable = (node, styles = {}) => {
     // Handler to select a component in the builder when clicking it in the
     // builder preview
     selectComponent = event => {
-      if (isBlock && onClick) {
-        onClick()
-      } else {
-        builderStore.actions.selectComponent(componentId, isBlock)
-      }
+      builderStore.actions.selectComponent(componentId)
       event.preventDefault()
       event.stopPropagation()
       return false
diff --git a/packages/server/src/api/controllers/static/templates/preview.hbs b/packages/server/src/api/controllers/static/templates/preview.hbs
index f28469ca22..31bf0762e0 100644
--- a/packages/server/src/api/controllers/static/templates/preview.hbs
+++ b/packages/server/src/api/controllers/static/templates/preview.hbs
@@ -63,7 +63,6 @@
       // Extract data from message
       const {
         selectedComponentId,
-        selectedBlockComponentId,
         layout,
         screen,
         appId,
@@ -82,7 +81,6 @@
       window["##BUDIBASE_PREVIEW_LAYOUT##"] = layout
       window["##BUDIBASE_PREVIEW_SCREEN##"] = screen
       window["##BUDIBASE_SELECTED_COMPONENT_ID##"] = selectedComponentId
-      window["##BUDIBASE_SELECTED_BLOCK_COMPONENT_ID##"] = selectedBlockComponentId
       window["##BUDIBASE_PREVIEW_ID##"] = Math.random()
       window["##BUDIBASE_PREVIEW_THEME##"] = theme
       window["##BUDIBASE_PREVIEW_CUSTOM_THEME##"] = customTheme

From 4fd1c26bbde7b53347bb49e9acce9fe65edec946 Mon Sep 17 00:00:00 2001
From: Andrew Kingston <andrew@kingston.dev>
Date: Fri, 27 Oct 2023 15:13:49 +0100
Subject: [PATCH 16/47] Clean up

---
 .../src/components/app/blocks/form/InnerFormBlock.svelte      | 4 ----
 packages/client/src/components/app/forms/Field.svelte         | 1 -
 2 files changed, 5 deletions(-)

diff --git a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
index 502b3e0569..f5e0d236a3 100644
--- a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
+++ b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
@@ -34,7 +34,6 @@
     bb_reference: "bbreferencefield",
   }
   const context = getContext("context")
-  const { builderStore } = getContext("sdk")
 
   let formId
 
@@ -242,9 +241,6 @@
                   order={idx}
                   interactive
                   name={field?.field}
-                  onClick={() => {
-                    builderStore.actions.clickFormBlockField(field?.field)
-                  }}
                 />
               {/if}
             {/each}
diff --git a/packages/client/src/components/app/forms/Field.svelte b/packages/client/src/components/app/forms/Field.svelte
index 83db76b473..f6fbe37681 100644
--- a/packages/client/src/components/app/forms/Field.svelte
+++ b/packages/client/src/components/app/forms/Field.svelte
@@ -1,6 +1,5 @@
 <script>
   import Placeholder from "../Placeholder.svelte"
-  import FieldGroupFallback from "./FieldGroupFallback.svelte"
   import { getContext, onDestroy } from "svelte"
 
   export let label

From de55445168785ae2a64abebda435bdcd5b9c57a3 Mon Sep 17 00:00:00 2001
From: Andrew Kingston <andrew@kingston.dev>
Date: Fri, 27 Oct 2023 16:06:06 +0100
Subject: [PATCH 17/47] Update pro

---
 packages/pro | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/pro b/packages/pro
index d24c0dc3a3..5ed0ee2aca 160000
--- a/packages/pro
+++ b/packages/pro
@@ -1 +1 @@
-Subproject commit d24c0dc3a30014cbe61860252aa48104cad36376
+Subproject commit 5ed0ee2aca9d754d80cd46bae412b24621afa47e

From 5c049e0465ca27ed078e3ffda4c5c91998ba97cd Mon Sep 17 00:00:00 2001
From: Mel O'Hagan <mel@budibase.com>
Date: Wed, 1 Nov 2023 13:54:18 +0000
Subject: [PATCH 18/47] LongFormField readonly prop

---
 .../bbui/src/Form/Core/RichTextField.svelte   |  2 ++
 packages/bbui/src/Form/Core/TextArea.svelte   |  2 ++
 .../bbui/src/Markdown/MarkdownEditor.svelte   |  5 ++-
 packages/client/manifest.json                 | 33 +++++++++++++++++++
 .../components/app/forms/LongFormField.svelte |  3 ++
 .../components/app/forms/StringField.svelte   |  2 ++
 6 files changed, 46 insertions(+), 1 deletion(-)

diff --git a/packages/bbui/src/Form/Core/RichTextField.svelte b/packages/bbui/src/Form/Core/RichTextField.svelte
index f964405f0d..3e0b0caf4d 100644
--- a/packages/bbui/src/Form/Core/RichTextField.svelte
+++ b/packages/bbui/src/Form/Core/RichTextField.svelte
@@ -4,6 +4,7 @@
   export let value = ""
   export let placeholder = null
   export let disabled = false
+  export let readonly = false
   export let error = null
   export let height = null
   export let id = null
@@ -20,6 +21,7 @@
     {fullScreenOffset}
     {disabled}
     {easyMDEOptions}
+    {readonly}
     on:change
   />
 </div>
diff --git a/packages/bbui/src/Form/Core/TextArea.svelte b/packages/bbui/src/Form/Core/TextArea.svelte
index 465212cd44..be7eed466d 100644
--- a/packages/bbui/src/Form/Core/TextArea.svelte
+++ b/packages/bbui/src/Form/Core/TextArea.svelte
@@ -5,6 +5,7 @@
   export let value = ""
   export let placeholder = null
   export let disabled = false
+  export let readonly = false
   export let error = null
   export let id = null
   export let height = null
@@ -61,6 +62,7 @@
     class="spectrum-Textfield-input"
     style={align ? `text-align: ${align}` : ""}
     {disabled}
+    {readonly}
     {id}
     on:focus={() => (focus = true)}
     on:blur={onChange}
diff --git a/packages/bbui/src/Markdown/MarkdownEditor.svelte b/packages/bbui/src/Markdown/MarkdownEditor.svelte
index 7fb6414ad8..27035d8033 100644
--- a/packages/bbui/src/Markdown/MarkdownEditor.svelte
+++ b/packages/bbui/src/Markdown/MarkdownEditor.svelte
@@ -8,6 +8,7 @@
   export let id = null
   export let fullScreenOffset = 0
   export let disabled = false
+  export let readonly = false
   export let easyMDEOptions
 
   const dispatch = createEventDispatcher()
@@ -19,6 +20,7 @@
   // control
   $: checkValue(value)
   $: mde?.codemirror.on("change", debouncedUpdate)
+  $: mde?.codemirror.setOption("readOnly", readonly)
 
   const checkValue = val => {
     if (mde && val !== latestValue) {
@@ -43,7 +45,7 @@
   const debouncedUpdate = debounce(update, 250)
 </script>
 
-{#key height}
+{#key (height, readonly)}
   <SpectrumMDE
     bind:mde
     scroll={true}
@@ -54,6 +56,7 @@
     easyMDEOptions={{
       initialValue: value,
       placeholder,
+      toolbar: readonly ? false : undefined,
       ...easyMDEOptions,
     }}
   />
diff --git a/packages/client/manifest.json b/packages/client/manifest.json
index eef1e50b7c..6e175c44de 100644
--- a/packages/client/manifest.json
+++ b/packages/client/manifest.json
@@ -2589,6 +2589,17 @@
         "key": "disabled",
         "defaultValue": false
       },
+      {
+        "type": "boolean",
+        "label": "Read only",
+        "key": "readonly",
+        "defaultValue": false,
+        "dependsOn": {
+          "setting": "disabled",
+          "value": true,
+          "invert": true
+        }
+      },
       {
         "type": "text",
         "label": "Initial form step",
@@ -2738,6 +2749,17 @@
         "key": "disabled",
         "defaultValue": false
       },
+      {
+        "type": "boolean",
+        "label": "Read only",
+        "key": "readonly",
+        "defaultValue": false,
+        "dependsOn": {
+          "setting": "disabled",
+          "value": true,
+          "invert": true
+        }
+      },
       {
         "type": "validation/string",
         "label": "Validation",
@@ -3427,6 +3449,17 @@
         "key": "disabled",
         "defaultValue": false
       },
+      {
+        "type": "boolean",
+        "label": "Read only",
+        "key": "readonly",
+        "defaultValue": false,
+        "dependsOn": {
+          "setting": "disabled",
+          "value": true,
+          "invert": true
+        }
+      },
       {
         "type": "validation/string",
         "label": "Validation",
diff --git a/packages/client/src/components/app/forms/LongFormField.svelte b/packages/client/src/components/app/forms/LongFormField.svelte
index 8d94f83319..8482a6a68e 100644
--- a/packages/client/src/components/app/forms/LongFormField.svelte
+++ b/packages/client/src/components/app/forms/LongFormField.svelte
@@ -8,6 +8,7 @@
   export let label
   export let placeholder
   export let disabled = false
+  export let readonly = false
   export let validation
   export let defaultValue = ""
   export let format = "auto"
@@ -71,6 +72,7 @@
         value={fieldState.value}
         on:change={handleChange}
         disabled={fieldState.disabled}
+        {readonly}
         error={fieldState.error}
         id={fieldState.fieldId}
         {placeholder}
@@ -88,6 +90,7 @@
         value={fieldState.value}
         on:change={handleChange}
         disabled={fieldState.disabled}
+        {readonly}
         error={fieldState.error}
         id={fieldState.fieldId}
         {placeholder}
diff --git a/packages/client/src/components/app/forms/StringField.svelte b/packages/client/src/components/app/forms/StringField.svelte
index 26136b5d8d..624611c733 100644
--- a/packages/client/src/components/app/forms/StringField.svelte
+++ b/packages/client/src/components/app/forms/StringField.svelte
@@ -11,6 +11,7 @@
   export let defaultValue = ""
   export let align
   export let onChange
+  export let readonly = false
 
   let fieldState
   let fieldApi
@@ -44,6 +45,7 @@
       {placeholder}
       {type}
       {align}
+      {readonly}
     />
   {/if}
 </Field>

From 5c36d70a0116624acadc20865a7bba6f76d46d1d Mon Sep 17 00:00:00 2001
From: Mel O'Hagan <mel@budibase.com>
Date: Wed, 1 Nov 2023 14:56:28 +0000
Subject: [PATCH 19/47] Pickers readonly prop

---
 .../bbui/src/Form/Core/CheckboxGroup.svelte   |  6 +++++
 packages/bbui/src/Form/Core/RadioGroup.svelte |  6 +++++
 packages/client/manifest.json                 | 22 +++++++++++++++++++
 .../app/forms/MultiFieldSelect.svelte         |  3 +++
 .../components/app/forms/OptionsField.svelte  |  3 +++
 5 files changed, 40 insertions(+)

diff --git a/packages/bbui/src/Form/Core/CheckboxGroup.svelte b/packages/bbui/src/Form/Core/CheckboxGroup.svelte
index 2b8a1e438a..faf37f3ad8 100644
--- a/packages/bbui/src/Form/Core/CheckboxGroup.svelte
+++ b/packages/bbui/src/Form/Core/CheckboxGroup.svelte
@@ -8,6 +8,7 @@
   export let options = []
   export let error = null
   export let disabled = false
+  export let readonly = false
   export let getOptionLabel = option => option
   export let getOptionValue = option => option
 
@@ -40,6 +41,11 @@
         >
           <input
             on:change={onChange}
+            on:click={e => {
+              if (readonly) {
+                e.preventDefault()
+              }
+            }}
             type="checkbox"
             class="spectrum-Checkbox-input"
             value={optionValue}
diff --git a/packages/bbui/src/Form/Core/RadioGroup.svelte b/packages/bbui/src/Form/Core/RadioGroup.svelte
index f7afc10bbc..fc99fafd40 100644
--- a/packages/bbui/src/Form/Core/RadioGroup.svelte
+++ b/packages/bbui/src/Form/Core/RadioGroup.svelte
@@ -8,6 +8,7 @@
   export let options = []
   export let error = null
   export let disabled = false
+  export let readonly = false
   export let getOptionLabel = option => option
   export let getOptionValue = option => option
   export let getOptionTitle = option => option
@@ -43,6 +44,11 @@
       >
         <input
           on:change={onChange}
+          on:click={e => {
+            if (readonly) {
+              e.preventDefault()
+            }
+          }}
           bind:group={value}
           value={getOptionValue(option)}
           type="radio"
diff --git a/packages/client/manifest.json b/packages/client/manifest.json
index 6e175c44de..90c897139c 100644
--- a/packages/client/manifest.json
+++ b/packages/client/manifest.json
@@ -3071,6 +3071,17 @@
         "key": "disabled",
         "defaultValue": false
       },
+      {
+        "type": "boolean",
+        "label": "Read only",
+        "key": "readonly",
+        "defaultValue": false,
+        "dependsOn": {
+          "setting": "disabled",
+          "value": true,
+          "invert": true
+        }
+      },
       {
         "type": "select",
         "label": "Options source",
@@ -3196,6 +3207,17 @@
         "key": "disabled",
         "defaultValue": false
       },
+      {
+        "type": "boolean",
+        "label": "Read only",
+        "key": "readonly",
+        "defaultValue": false,
+        "dependsOn": {
+          "setting": "disabled",
+          "value": true,
+          "invert": true
+        }
+      },
       {
         "type": "select",
         "label": "Type",
diff --git a/packages/client/src/components/app/forms/MultiFieldSelect.svelte b/packages/client/src/components/app/forms/MultiFieldSelect.svelte
index 88e1ec5a8e..cb4879f86e 100644
--- a/packages/client/src/components/app/forms/MultiFieldSelect.svelte
+++ b/packages/client/src/components/app/forms/MultiFieldSelect.svelte
@@ -6,6 +6,7 @@
   export let label
   export let placeholder
   export let disabled = false
+  export let readonly = false
   export let validation
   export let defaultValue
   export let optionsSource = "schema"
@@ -71,6 +72,7 @@
         getOptionValue={flatOptions ? x => x : x => x.value}
         id={fieldState.fieldId}
         disabled={fieldState.disabled}
+        {readonly}
         on:change={handleChange}
         {placeholder}
         {options}
@@ -81,6 +83,7 @@
         value={fieldState.value || []}
         id={fieldState.fieldId}
         disabled={fieldState.disabled}
+        {readonly}
         error={fieldState.error}
         {options}
         {direction}
diff --git a/packages/client/src/components/app/forms/OptionsField.svelte b/packages/client/src/components/app/forms/OptionsField.svelte
index 3c229c0509..c01827471a 100644
--- a/packages/client/src/components/app/forms/OptionsField.svelte
+++ b/packages/client/src/components/app/forms/OptionsField.svelte
@@ -6,6 +6,7 @@
   export let label
   export let placeholder
   export let disabled = false
+  export let readonly = false
   export let optionsType = "select"
   export let validation
   export let defaultValue
@@ -58,6 +59,7 @@
         value={fieldState.value}
         id={fieldState.fieldId}
         disabled={fieldState.disabled}
+        {readonly}
         error={fieldState.error}
         {options}
         {placeholder}
@@ -72,6 +74,7 @@
         value={fieldState.value}
         id={fieldState.fieldId}
         disabled={fieldState.disabled}
+        {readonly}
         error={fieldState.error}
         {options}
         {direction}

From 56d5a0b8f6fbc0e265bdc0068f253d601d56c62a Mon Sep 17 00:00:00 2001
From: Mel O'Hagan <mel@budibase.com>
Date: Wed, 1 Nov 2023 16:01:45 +0000
Subject: [PATCH 20/47] Further read only settings

---
 packages/bbui/src/Form/Core/Checkbox.svelte   |  6 ++
 packages/bbui/src/Form/Core/DatePicker.svelte |  4 +-
 packages/bbui/src/Form/DatePicker.svelte      |  2 +
 packages/client/manifest.json                 | 57 ++++++++++++++++++-
 .../components/app/forms/BooleanField.svelte  |  2 +
 .../components/app/forms/DateTimeField.svelte |  2 +
 .../src/components/app/forms/JSONField.svelte |  2 +
 .../app/forms/RelationshipField.svelte        |  2 +
 8 files changed, 75 insertions(+), 2 deletions(-)

diff --git a/packages/bbui/src/Form/Core/Checkbox.svelte b/packages/bbui/src/Form/Core/Checkbox.svelte
index 3efc737bfb..3eaaf4dede 100644
--- a/packages/bbui/src/Form/Core/Checkbox.svelte
+++ b/packages/bbui/src/Form/Core/Checkbox.svelte
@@ -8,6 +8,7 @@
   export let id = null
   export let text = null
   export let disabled = false
+  export let readonly = false
   export let size
   export let indeterminate = false
 
@@ -29,6 +30,11 @@
     checked={value}
     {disabled}
     on:change={onChange}
+    on:click={e => {
+      if (readonly) {
+        e.preventDefault()
+      }
+    }}
     type="checkbox"
     class="spectrum-Checkbox-input"
     {id}
diff --git a/packages/bbui/src/Form/Core/DatePicker.svelte b/packages/bbui/src/Form/Core/DatePicker.svelte
index 7ce15292be..786aee40b6 100644
--- a/packages/bbui/src/Form/Core/DatePicker.svelte
+++ b/packages/bbui/src/Form/Core/DatePicker.svelte
@@ -9,6 +9,7 @@
 
   export let id = null
   export let disabled = false
+  export let readonly = false
   export let error = null
   export let enableTime = true
   export let value = null
@@ -186,7 +187,7 @@
   >
     <div
       id={flatpickrId}
-      class:is-disabled={disabled}
+      class:is-disabled={disabled || readonly}
       class:is-invalid={!!error}
       class="flatpickr spectrum-InputGroup spectrum-Datepicker"
       class:is-focused={open}
@@ -211,6 +212,7 @@
         {/if}
         <input
           {disabled}
+          {readonly}
           data-input
           type="text"
           class="spectrum-Textfield-input spectrum-InputGroup-input"
diff --git a/packages/bbui/src/Form/DatePicker.svelte b/packages/bbui/src/Form/DatePicker.svelte
index 04ce8b5467..f17871a576 100644
--- a/packages/bbui/src/Form/DatePicker.svelte
+++ b/packages/bbui/src/Form/DatePicker.svelte
@@ -7,6 +7,7 @@
   export let label = null
   export let labelPosition = "above"
   export let disabled = false
+  export let readonly = false
   export let error = null
   export let enableTime = true
   export let timeOnly = false
@@ -33,6 +34,7 @@
   <DatePicker
     {error}
     {disabled}
+    {readonly}
     {value}
     {placeholder}
     {enableTime}
diff --git a/packages/client/manifest.json b/packages/client/manifest.json
index 90c897139c..7559825267 100644
--- a/packages/client/manifest.json
+++ b/packages/client/manifest.json
@@ -2851,6 +2851,17 @@
         "key": "disabled",
         "defaultValue": false
       },
+      {
+        "type": "boolean",
+        "label": "Read only",
+        "key": "readonly",
+        "defaultValue": false,
+        "dependsOn": {
+          "setting": "disabled",
+          "value": true,
+          "invert": true
+        }
+      },
       {
         "type": "validation/number",
         "label": "Validation",
@@ -3392,6 +3403,17 @@
         "key": "disabled",
         "defaultValue": false
       },
+      {
+        "type": "boolean",
+        "label": "Read only",
+        "key": "readonly",
+        "defaultValue": false,
+        "dependsOn": {
+          "setting": "disabled",
+          "value": true,
+          "invert": true
+        }
+      },
       {
         "type": "validation/boolean",
         "label": "Validation",
@@ -3563,6 +3585,17 @@
         "key": "disabled",
         "defaultValue": false
       },
+      {
+        "type": "boolean",
+        "label": "Read only",
+        "key": "readonly",
+        "defaultValue": false,
+        "dependsOn": {
+          "setting": "disabled",
+          "value": true,
+          "invert": true
+        }
+      },
       {
         "type": "validation/datetime",
         "label": "Validation",
@@ -3836,7 +3869,7 @@
       },
       {
         "type": "boolean",
-        "label": "Disabled",
+        "label": "Read only",
         "key": "disabled",
         "defaultValue": false
       },
@@ -3912,6 +3945,17 @@
         "label": "Disabled",
         "key": "disabled",
         "defaultValue": false
+      },
+      {
+        "type": "boolean",
+        "label": "Read only",
+        "key": "readonly",
+        "defaultValue": false,
+        "dependsOn": {
+          "setting": "disabled",
+          "value": true,
+          "invert": true
+        }
       }
     ]
   },
@@ -3964,6 +4008,17 @@
         "label": "Disabled",
         "key": "disabled",
         "defaultValue": false
+      },
+      {
+        "type": "boolean",
+        "label": "Read only",
+        "key": "readonly",
+        "defaultValue": false,
+        "dependsOn": {
+          "setting": "disabled",
+          "value": true,
+          "invert": true
+        }
       }
     ]
   },
diff --git a/packages/client/src/components/app/forms/BooleanField.svelte b/packages/client/src/components/app/forms/BooleanField.svelte
index a65d041c29..9635a7d7b5 100644
--- a/packages/client/src/components/app/forms/BooleanField.svelte
+++ b/packages/client/src/components/app/forms/BooleanField.svelte
@@ -6,6 +6,7 @@
   export let label
   export let text
   export let disabled = false
+  export let readonly = false
   export let size
   export let validation
   export let defaultValue
@@ -49,6 +50,7 @@
     <CoreCheckbox
       value={fieldState.value}
       disabled={fieldState.disabled}
+      {readonly}
       error={fieldState.error}
       id={fieldState.fieldId}
       {size}
diff --git a/packages/client/src/components/app/forms/DateTimeField.svelte b/packages/client/src/components/app/forms/DateTimeField.svelte
index 6bcd20d250..8124c7861a 100644
--- a/packages/client/src/components/app/forms/DateTimeField.svelte
+++ b/packages/client/src/components/app/forms/DateTimeField.svelte
@@ -6,6 +6,7 @@
   export let label
   export let placeholder
   export let disabled = false
+  export let readonly = false
   export let enableTime = true
   export let timeOnly = false
   export let time24hr = false
@@ -40,6 +41,7 @@
       value={fieldState.value}
       on:change={handleChange}
       disabled={fieldState.disabled}
+      {readonly}
       error={fieldState.error}
       id={fieldState.fieldId}
       appendTo={document.getElementById("flatpickr-root")}
diff --git a/packages/client/src/components/app/forms/JSONField.svelte b/packages/client/src/components/app/forms/JSONField.svelte
index c80060d3d6..1ee09c8460 100644
--- a/packages/client/src/components/app/forms/JSONField.svelte
+++ b/packages/client/src/components/app/forms/JSONField.svelte
@@ -7,6 +7,7 @@
   export let label
   export let placeholder
   export let disabled = false
+  export let readonly = false
   export let defaultValue = ""
   export let onChange
 
@@ -60,6 +61,7 @@
         value={serialiseValue(fieldState.value)}
         on:change={handleChange}
         disabled={fieldState.disabled}
+        {readonly}
         error={fieldState.error}
         id={fieldState.fieldId}
         {placeholder}
diff --git a/packages/client/src/components/app/forms/RelationshipField.svelte b/packages/client/src/components/app/forms/RelationshipField.svelte
index 544a1a8434..9d2d6adf0f 100644
--- a/packages/client/src/components/app/forms/RelationshipField.svelte
+++ b/packages/client/src/components/app/forms/RelationshipField.svelte
@@ -11,6 +11,7 @@
   export let label
   export let placeholder
   export let disabled = false
+  export let readonly = false
   export let validation
   export let autocomplete = true
   export let defaultValue
@@ -200,6 +201,7 @@
       on:loadMore={loadMore}
       id={fieldState.fieldId}
       disabled={fieldState.disabled}
+      {readonly}
       error={fieldState.error}
       getOptionLabel={getDisplayName}
       getOptionValue={option => option._id}

From 33e37261b2e8a16404efb68467eb27eea7e42ac7 Mon Sep 17 00:00:00 2001
From: Mel O'Hagan <mel@budibase.com>
Date: Wed, 1 Nov 2023 16:27:52 +0000
Subject: [PATCH 21/47] Use fieldState

---
 .../client/src/components/app/forms/AttachmentField.svelte   | 4 +++-
 packages/client/src/components/app/forms/BooleanField.svelte | 3 ++-
 .../client/src/components/app/forms/CodeScannerField.svelte  | 4 +++-
 .../client/src/components/app/forms/DateTimeField.svelte     | 3 ++-
 packages/client/src/components/app/forms/Field.svelte        | 2 ++
 packages/client/src/components/app/forms/Form.svelte         | 4 +++-
 packages/client/src/components/app/forms/InnerForm.svelte    | 3 +++
 packages/client/src/components/app/forms/JSONField.svelte    | 3 ++-
 .../client/src/components/app/forms/LongFormField.svelte     | 5 +++--
 .../client/src/components/app/forms/MultiFieldSelect.svelte  | 5 +++--
 packages/client/src/components/app/forms/OptionsField.svelte | 5 +++--
 .../client/src/components/app/forms/RelationshipField.svelte | 3 ++-
 packages/client/src/components/app/forms/StringField.svelte  | 5 +++--
 13 files changed, 34 insertions(+), 15 deletions(-)

diff --git a/packages/client/src/components/app/forms/AttachmentField.svelte b/packages/client/src/components/app/forms/AttachmentField.svelte
index e24115ebc0..861d881733 100644
--- a/packages/client/src/components/app/forms/AttachmentField.svelte
+++ b/packages/client/src/components/app/forms/AttachmentField.svelte
@@ -6,6 +6,7 @@
   export let field
   export let label
   export let disabled = false
+  export let readonly = false
   export let compact = false
   export let validation
   export let extensions
@@ -71,6 +72,7 @@
   {label}
   {field}
   {disabled}
+  {readonly}
   {validation}
   type="attachment"
   bind:fieldState
@@ -81,7 +83,7 @@
     {#if fieldState}
       <CoreDropzone
         value={fieldState.value}
-        disabled={fieldState.disabled}
+        disabled={fieldState.disabled || fieldState.readonly}
         error={fieldState.error}
         on:change={handleChange}
         {processFiles}
diff --git a/packages/client/src/components/app/forms/BooleanField.svelte b/packages/client/src/components/app/forms/BooleanField.svelte
index 9635a7d7b5..1f59ddcfa6 100644
--- a/packages/client/src/components/app/forms/BooleanField.svelte
+++ b/packages/client/src/components/app/forms/BooleanField.svelte
@@ -40,6 +40,7 @@
   {label}
   {field}
   {disabled}
+  {readonly}
   {validation}
   defaultValue={isTruthy(defaultValue)}
   type="boolean"
@@ -50,7 +51,7 @@
     <CoreCheckbox
       value={fieldState.value}
       disabled={fieldState.disabled}
-      {readonly}
+      readonly={fieldState.readonly}
       error={fieldState.error}
       id={fieldState.fieldId}
       {size}
diff --git a/packages/client/src/components/app/forms/CodeScannerField.svelte b/packages/client/src/components/app/forms/CodeScannerField.svelte
index c408f78d7c..a9c115e852 100644
--- a/packages/client/src/components/app/forms/CodeScannerField.svelte
+++ b/packages/client/src/components/app/forms/CodeScannerField.svelte
@@ -6,6 +6,7 @@
   export let label
   export let type = "barcodeqr"
   export let disabled = false
+  export let readonly = false
   export let validation
   export let defaultValue = ""
   export let onChange
@@ -32,6 +33,7 @@
   {label}
   {field}
   {disabled}
+  {readonly}
   {validation}
   {defaultValue}
   {type}
@@ -42,7 +44,7 @@
     <CodeScanner
       value={fieldState.value}
       on:change={handleUpdate}
-      disabled={fieldState.disabled}
+      disabled={fieldState.disabled || fieldState.readonly}
       {allowManualEntry}
       scanButtonText={scanText}
       {beepOnScan}
diff --git a/packages/client/src/components/app/forms/DateTimeField.svelte b/packages/client/src/components/app/forms/DateTimeField.svelte
index 8124c7861a..22a76e5ef2 100644
--- a/packages/client/src/components/app/forms/DateTimeField.svelte
+++ b/packages/client/src/components/app/forms/DateTimeField.svelte
@@ -30,6 +30,7 @@
   {label}
   {field}
   {disabled}
+  {readonly}
   {validation}
   {defaultValue}
   type="datetime"
@@ -41,7 +42,7 @@
       value={fieldState.value}
       on:change={handleChange}
       disabled={fieldState.disabled}
-      {readonly}
+      readonly={fieldState.readonly}
       error={fieldState.error}
       id={fieldState.fieldId}
       appendTo={document.getElementById("flatpickr-root")}
diff --git a/packages/client/src/components/app/forms/Field.svelte b/packages/client/src/components/app/forms/Field.svelte
index 5d4da5afef..16115583a7 100644
--- a/packages/client/src/components/app/forms/Field.svelte
+++ b/packages/client/src/components/app/forms/Field.svelte
@@ -11,6 +11,7 @@
   export let defaultValue
   export let type
   export let disabled = false
+  export let readonly = false
   export let validation
 
   // Get contexts
@@ -29,6 +30,7 @@
     type,
     defaultValue,
     disabled,
+    readonly,
     validation,
     formStep
   )
diff --git a/packages/client/src/components/app/forms/Form.svelte b/packages/client/src/components/app/forms/Form.svelte
index 87883fe4b6..1a740585f3 100644
--- a/packages/client/src/components/app/forms/Form.svelte
+++ b/packages/client/src/components/app/forms/Form.svelte
@@ -8,6 +8,7 @@
   export let theme
   export let size
   export let disabled = false
+  export let readonly = false
   export let actionType = "Create"
   export let initialFormStep = 1
 
@@ -39,7 +40,7 @@
   $: schemaKey = generateSchemaKey(schema)
   $: initialValues = getInitialValues(actionType, dataSource, $context)
   $: resetKey = Helpers.hashString(
-    schemaKey + JSON.stringify(initialValues) + disabled
+    schemaKey + JSON.stringify(initialValues) + disabled + readonly
   )
 
   // Returns the closes data context which isn't a built in context
@@ -97,6 +98,7 @@
       {theme}
       {size}
       {disabled}
+      {readonly}
       {actionType}
       {schema}
       {table}
diff --git a/packages/client/src/components/app/forms/InnerForm.svelte b/packages/client/src/components/app/forms/InnerForm.svelte
index 4dacf36244..6ebe9de7ec 100644
--- a/packages/client/src/components/app/forms/InnerForm.svelte
+++ b/packages/client/src/components/app/forms/InnerForm.svelte
@@ -6,6 +6,7 @@
 
   export let dataSource
   export let disabled = false
+  export let readonly = false
   export let initialValues
   export let size
   export let schema
@@ -148,6 +149,7 @@
       type,
       defaultValue = null,
       fieldDisabled = false,
+      fieldReadOnly = false,
       validationRules,
       step = 1
     ) => {
@@ -205,6 +207,7 @@
           error: initialError,
           disabled:
             disabled || fieldDisabled || (isAutoColumn && !editAutoColumns),
+          readonly: readonly || fieldReadOnly,
           defaultValue,
           validator,
           lastUpdate: Date.now(),
diff --git a/packages/client/src/components/app/forms/JSONField.svelte b/packages/client/src/components/app/forms/JSONField.svelte
index 1ee09c8460..cf96f54a23 100644
--- a/packages/client/src/components/app/forms/JSONField.svelte
+++ b/packages/client/src/components/app/forms/JSONField.svelte
@@ -49,6 +49,7 @@
   {label}
   {field}
   {disabled}
+  {readonly}
   {validation}
   {defaultValue}
   type="json"
@@ -61,7 +62,7 @@
         value={serialiseValue(fieldState.value)}
         on:change={handleChange}
         disabled={fieldState.disabled}
-        {readonly}
+        readonly={fieldState.readonly}
         error={fieldState.error}
         id={fieldState.fieldId}
         {placeholder}
diff --git a/packages/client/src/components/app/forms/LongFormField.svelte b/packages/client/src/components/app/forms/LongFormField.svelte
index 8482a6a68e..a9087a0a9c 100644
--- a/packages/client/src/components/app/forms/LongFormField.svelte
+++ b/packages/client/src/components/app/forms/LongFormField.svelte
@@ -59,6 +59,7 @@
   {label}
   {field}
   {disabled}
+  {readonly}
   {validation}
   {defaultValue}
   type="longform"
@@ -72,7 +73,7 @@
         value={fieldState.value}
         on:change={handleChange}
         disabled={fieldState.disabled}
-        {readonly}
+        readonly={fieldState.readonly}
         error={fieldState.error}
         id={fieldState.fieldId}
         {placeholder}
@@ -90,7 +91,7 @@
         value={fieldState.value}
         on:change={handleChange}
         disabled={fieldState.disabled}
-        {readonly}
+        readonly={fieldState.readonly}
         error={fieldState.error}
         id={fieldState.fieldId}
         {placeholder}
diff --git a/packages/client/src/components/app/forms/MultiFieldSelect.svelte b/packages/client/src/components/app/forms/MultiFieldSelect.svelte
index cb4879f86e..4ee691061a 100644
--- a/packages/client/src/components/app/forms/MultiFieldSelect.svelte
+++ b/packages/client/src/components/app/forms/MultiFieldSelect.svelte
@@ -56,6 +56,7 @@
   {field}
   {label}
   {disabled}
+  {readonly}
   {validation}
   defaultValue={expandedDefaultValue}
   type="array"
@@ -72,7 +73,7 @@
         getOptionValue={flatOptions ? x => x : x => x.value}
         id={fieldState.fieldId}
         disabled={fieldState.disabled}
-        {readonly}
+        readonly={fieldState.readonly}
         on:change={handleChange}
         {placeholder}
         {options}
@@ -83,7 +84,7 @@
         value={fieldState.value || []}
         id={fieldState.fieldId}
         disabled={fieldState.disabled}
-        {readonly}
+        readonly={fieldState.readonly}
         error={fieldState.error}
         {options}
         {direction}
diff --git a/packages/client/src/components/app/forms/OptionsField.svelte b/packages/client/src/components/app/forms/OptionsField.svelte
index c01827471a..dc18df8dbe 100644
--- a/packages/client/src/components/app/forms/OptionsField.svelte
+++ b/packages/client/src/components/app/forms/OptionsField.svelte
@@ -46,6 +46,7 @@
   {field}
   {label}
   {disabled}
+  {readonly}
   {validation}
   {defaultValue}
   type="options"
@@ -59,7 +60,7 @@
         value={fieldState.value}
         id={fieldState.fieldId}
         disabled={fieldState.disabled}
-        {readonly}
+        readonly={fieldState.readonly}
         error={fieldState.error}
         {options}
         {placeholder}
@@ -74,7 +75,7 @@
         value={fieldState.value}
         id={fieldState.fieldId}
         disabled={fieldState.disabled}
-        {readonly}
+        readonly={fieldState.readonly}
         error={fieldState.error}
         {options}
         {direction}
diff --git a/packages/client/src/components/app/forms/RelationshipField.svelte b/packages/client/src/components/app/forms/RelationshipField.svelte
index 9d2d6adf0f..10fcaa904f 100644
--- a/packages/client/src/components/app/forms/RelationshipField.svelte
+++ b/packages/client/src/components/app/forms/RelationshipField.svelte
@@ -184,6 +184,7 @@
   {label}
   {field}
   {disabled}
+  {readonly}
   {validation}
   defaultValue={expandedDefaultValue}
   {type}
@@ -201,7 +202,7 @@
       on:loadMore={loadMore}
       id={fieldState.fieldId}
       disabled={fieldState.disabled}
-      {readonly}
+      readonly={fieldState.readonly}
       error={fieldState.error}
       getOptionLabel={getDisplayName}
       getOptionValue={option => option._id}
diff --git a/packages/client/src/components/app/forms/StringField.svelte b/packages/client/src/components/app/forms/StringField.svelte
index 624611c733..674be9f1b2 100644
--- a/packages/client/src/components/app/forms/StringField.svelte
+++ b/packages/client/src/components/app/forms/StringField.svelte
@@ -7,11 +7,11 @@
   export let placeholder
   export let type = "text"
   export let disabled = false
+  export let readonly = false
   export let validation
   export let defaultValue = ""
   export let align
   export let onChange
-  export let readonly = false
 
   let fieldState
   let fieldApi
@@ -28,6 +28,7 @@
   {label}
   {field}
   {disabled}
+  {readonly}
   {validation}
   {defaultValue}
   type={type === "number" ? "number" : "string"}
@@ -40,12 +41,12 @@
       value={fieldState.value}
       on:change={handleChange}
       disabled={fieldState.disabled}
+      readonly={fieldState.readonly}
       error={fieldState.error}
       id={fieldState.fieldId}
       {placeholder}
       {type}
       {align}
-      {readonly}
     />
   {/if}
 </Field>

From 5923ae2983d588d1f1d5a5fd3550a04cda47d671 Mon Sep 17 00:00:00 2001
From: Mel O'Hagan <mel@budibase.com>
Date: Wed, 1 Nov 2023 16:40:23 +0000
Subject: [PATCH 22/47] Make form block view readonly

---
 packages/bbui/src/Markdown/MarkdownEditor.svelte           | 2 +-
 packages/client/manifest.json                              | 7 +------
 .../src/components/app/blocks/form/InnerFormBlock.svelte   | 3 ++-
 3 files changed, 4 insertions(+), 8 deletions(-)

diff --git a/packages/bbui/src/Markdown/MarkdownEditor.svelte b/packages/bbui/src/Markdown/MarkdownEditor.svelte
index 27035d8033..225d25a0eb 100644
--- a/packages/bbui/src/Markdown/MarkdownEditor.svelte
+++ b/packages/bbui/src/Markdown/MarkdownEditor.svelte
@@ -56,7 +56,7 @@
     easyMDEOptions={{
       initialValue: value,
       placeholder,
-      toolbar: readonly ? false : undefined,
+      toolbar: disabled || readonly ? false : undefined,
       ...easyMDEOptions,
     }}
   />
diff --git a/packages/client/manifest.json b/packages/client/manifest.json
index 7559825267..749e2cf194 100644
--- a/packages/client/manifest.json
+++ b/packages/client/manifest.json
@@ -5640,12 +5640,7 @@
             "type": "boolean",
             "label": "Disabled",
             "key": "disabled",
-            "defaultValue": false,
-            "dependsOn": {
-              "setting": "actionType",
-              "value": "View",
-              "invert": true
-            }
+            "defaultValue": false
           }
         ]
       },
diff --git a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
index f7e9a0d2ed..b2c9888c73 100644
--- a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
+++ b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
@@ -136,7 +136,8 @@
       actionType: actionType === "Create" ? "Create" : "Update",
       dataSource,
       size,
-      disabled: disabled || actionType === "View",
+      disabled,
+      readonly: !disabled && actionType === "View",
     }}
     styles={{
       normal: {

From 738082dc27e564530e7af107c99beb7c27956733 Mon Sep 17 00:00:00 2001
From: Mel O'Hagan <mel@budibase.com>
Date: Thu, 2 Nov 2023 15:49:16 +0000
Subject: [PATCH 23/47] Remove readonly key

---
 packages/bbui/src/Markdown/MarkdownEditor.svelte | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/bbui/src/Markdown/MarkdownEditor.svelte b/packages/bbui/src/Markdown/MarkdownEditor.svelte
index 225d25a0eb..6c711c9d28 100644
--- a/packages/bbui/src/Markdown/MarkdownEditor.svelte
+++ b/packages/bbui/src/Markdown/MarkdownEditor.svelte
@@ -45,7 +45,7 @@
   const debouncedUpdate = debounce(update, 250)
 </script>
 
-{#key (height, readonly)}
+{#key height}
   <SpectrumMDE
     bind:mde
     scroll={true}

From 12a7811847fd15cdd8e2e5b3c64e939460f54a56 Mon Sep 17 00:00:00 2001
From: Michael Drury <me@michaeldrury.co.uk>
Date: Fri, 3 Nov 2023 13:05:23 +0000
Subject: [PATCH 24/47] Revert "Reverting changes to bull parameters"

---
 packages/backend-core/src/index.ts            |  1 +
 .../backend-core/src/queue/inMemoryQueue.ts   |  2 +-
 packages/backend-core/src/queue/queue.ts      | 20 ++++++--
 packages/backend-core/src/utils/Duration.ts   | 49 +++++++++++++++++++
 packages/backend-core/src/utils/index.ts      |  1 +
 .../src/utils/tests/Duration.spec.ts          | 19 +++++++
 6 files changed, 88 insertions(+), 4 deletions(-)
 create mode 100644 packages/backend-core/src/utils/Duration.ts
 create mode 100644 packages/backend-core/src/utils/tests/Duration.spec.ts

diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts
index ffffd8240a..c7cf9f56cc 100644
--- a/packages/backend-core/src/index.ts
+++ b/packages/backend-core/src/index.ts
@@ -30,6 +30,7 @@ export * as timers from "./timers"
 export { default as env } from "./environment"
 export * as blacklist from "./blacklist"
 export * as docUpdates from "./docUpdates"
+export * from "./utils/Duration"
 export { SearchParams } from "./db"
 // Add context to tenancy for backwards compatibility
 // only do this for external usages to prevent internal
diff --git a/packages/backend-core/src/queue/inMemoryQueue.ts b/packages/backend-core/src/queue/inMemoryQueue.ts
index af2ec6dbaa..a8add7ecb6 100644
--- a/packages/backend-core/src/queue/inMemoryQueue.ts
+++ b/packages/backend-core/src/queue/inMemoryQueue.ts
@@ -36,7 +36,7 @@ class InMemoryQueue {
    * @param opts This is not used by the in memory queue as there is no real use
    * case when in memory, but is the same API as Bull
    */
-  constructor(name: string, opts = null) {
+  constructor(name: string, opts?: any) {
     this._name = name
     this._opts = opts
     this._messages = []
diff --git a/packages/backend-core/src/queue/queue.ts b/packages/backend-core/src/queue/queue.ts
index 0658147709..c0d1861de3 100644
--- a/packages/backend-core/src/queue/queue.ts
+++ b/packages/backend-core/src/queue/queue.ts
@@ -2,11 +2,18 @@ import env from "../environment"
 import { getRedisOptions } from "../redis/utils"
 import { JobQueue } from "./constants"
 import InMemoryQueue from "./inMemoryQueue"
-import BullQueue from "bull"
+import BullQueue, { QueueOptions } from "bull"
 import { addListeners, StalledFn } from "./listeners"
+import { Duration } from "../utils"
 import * as timers from "../timers"
+import * as Redis from "ioredis"
 
-const CLEANUP_PERIOD_MS = 60 * 1000
+// the queue lock is held for 5 minutes
+const QUEUE_LOCK_MS = Duration.fromMinutes(5).toMs()
+// queue lock is refreshed every 30 seconds
+const QUEUE_LOCK_RENEW_INTERNAL_MS = Duration.fromSeconds(30).toMs()
+// cleanup the queue every 60 seconds
+const CLEANUP_PERIOD_MS = Duration.fromSeconds(60).toMs()
 let QUEUES: BullQueue.Queue[] | InMemoryQueue[] = []
 let cleanupInterval: NodeJS.Timeout
 
@@ -21,7 +28,14 @@ export function createQueue<T>(
   opts: { removeStalledCb?: StalledFn } = {}
 ): BullQueue.Queue<T> {
   const { opts: redisOpts, redisProtocolUrl } = getRedisOptions()
-  const queueConfig: any = redisProtocolUrl || { redis: redisOpts }
+  const queueConfig: QueueOptions = {
+    redis: redisProtocolUrl! || (redisOpts as Redis.RedisOptions),
+    settings: {
+      maxStalledCount: 0,
+      lockDuration: QUEUE_LOCK_MS,
+      lockRenewTime: QUEUE_LOCK_RENEW_INTERNAL_MS,
+    },
+  }
   let queue: any
   if (!env.isTest()) {
     queue = new BullQueue(jobQueue, queueConfig)
diff --git a/packages/backend-core/src/utils/Duration.ts b/packages/backend-core/src/utils/Duration.ts
new file mode 100644
index 0000000000..f376c2f7c7
--- /dev/null
+++ b/packages/backend-core/src/utils/Duration.ts
@@ -0,0 +1,49 @@
+export enum DurationType {
+  MILLISECONDS = "milliseconds",
+  SECONDS = "seconds",
+  MINUTES = "minutes",
+  HOURS = "hours",
+  DAYS = "days",
+}
+
+const conversion: Record<DurationType, number> = {
+  milliseconds: 1,
+  seconds: 1000,
+  minutes: 60 * 1000,
+  hours: 60 * 60 * 1000,
+  days: 24 * 60 * 60 * 1000,
+}
+
+export class Duration {
+  static convert(from: DurationType, to: DurationType, duration: number) {
+    const milliseconds = duration * conversion[from]
+    return milliseconds / conversion[to]
+  }
+
+  static from(from: DurationType, duration: number) {
+    return {
+      to: (to: DurationType) => {
+        return Duration.convert(from, to, duration)
+      },
+      toMs: () => {
+        return Duration.convert(from, DurationType.MILLISECONDS, duration)
+      },
+    }
+  }
+
+  static fromSeconds(duration: number) {
+    return Duration.from(DurationType.SECONDS, duration)
+  }
+
+  static fromMinutes(duration: number) {
+    return Duration.from(DurationType.MINUTES, duration)
+  }
+
+  static fromHours(duration: number) {
+    return Duration.from(DurationType.HOURS, duration)
+  }
+
+  static fromDays(duration: number) {
+    return Duration.from(DurationType.DAYS, duration)
+  }
+}
diff --git a/packages/backend-core/src/utils/index.ts b/packages/backend-core/src/utils/index.ts
index 318a7f13ba..ac17227459 100644
--- a/packages/backend-core/src/utils/index.ts
+++ b/packages/backend-core/src/utils/index.ts
@@ -1,3 +1,4 @@
 export * from "./hashing"
 export * from "./utils"
 export * from "./stringUtils"
+export * from "./Duration"
diff --git a/packages/backend-core/src/utils/tests/Duration.spec.ts b/packages/backend-core/src/utils/tests/Duration.spec.ts
new file mode 100644
index 0000000000..46b996f788
--- /dev/null
+++ b/packages/backend-core/src/utils/tests/Duration.spec.ts
@@ -0,0 +1,19 @@
+import { Duration, DurationType } from "../Duration"
+
+describe("duration", () => {
+  it("should convert minutes to milliseconds", () => {
+    expect(Duration.fromMinutes(5).toMs()).toBe(300000)
+  })
+
+  it("should convert seconds to milliseconds", () => {
+    expect(Duration.fromSeconds(30).toMs()).toBe(30000)
+  })
+
+  it("should convert days to milliseconds", () => {
+    expect(Duration.fromDays(1).toMs()).toBe(86400000)
+  })
+
+  it("should convert minutes to days", () => {
+    expect(Duration.fromMinutes(1440).to(DurationType.DAYS)).toBe(1)
+  })
+})

From a03a00c4af0e3deffb55c16c24c0a37748fae594 Mon Sep 17 00:00:00 2001
From: Mel O'Hagan <mel@budibase.com>
Date: Fri, 3 Nov 2023 14:08:46 +0000
Subject: [PATCH 25/47] Toggle preview

---
 packages/bbui/src/Markdown/MarkdownEditor.svelte | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/packages/bbui/src/Markdown/MarkdownEditor.svelte b/packages/bbui/src/Markdown/MarkdownEditor.svelte
index 6c711c9d28..888187c8da 100644
--- a/packages/bbui/src/Markdown/MarkdownEditor.svelte
+++ b/packages/bbui/src/Markdown/MarkdownEditor.svelte
@@ -20,7 +20,9 @@
   // control
   $: checkValue(value)
   $: mde?.codemirror.on("change", debouncedUpdate)
-  $: mde?.codemirror.setOption("readOnly", readonly)
+  $: if (readonly || disabled) {
+    mde?.togglePreview()
+  }
 
   const checkValue = val => {
     if (mde && val !== latestValue) {

From 86f7bd192fb4c6bbe12636e94cca69dc28957d23 Mon Sep 17 00:00:00 2001
From: mike12345567 <me@michaeldrury.co.uk>
Date: Fri, 3 Nov 2023 14:55:56 +0000
Subject: [PATCH 26/47] Moving audit log init to be part of the server startup.

---
 packages/worker/src/index.ts | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/packages/worker/src/index.ts b/packages/worker/src/index.ts
index 3905a21c73..4b1d11ecf7 100644
--- a/packages/worker/src/index.ts
+++ b/packages/worker/src/index.ts
@@ -31,10 +31,6 @@ import destroyable from "server-destroy"
 import { initPro } from "./initPro"
 import { handleScimBody } from "./middleware/handleScimBody"
 
-// configure events to use the pro audit log write
-// can't integrate directly into backend-core due to cyclic issues
-events.processors.init(proSdk.auditLogs.write)
-
 if (coreEnv.ENABLE_SSO_MAINTENANCE_MODE) {
   console.warn(
     "Warning: ENABLE_SSO_MAINTENANCE_MODE is set. It is recommended this flag is disabled if maintenance is not in progress"
@@ -93,6 +89,9 @@ export default server.listen(parseInt(env.PORT || "4002"), async () => {
   console.log(`Worker running on ${JSON.stringify(server.address())}`)
   await initPro()
   await redis.init()
+  // configure events to use the pro audit log write
+  // can't integrate directly into backend-core due to cyclic issues
+  await events.processors.init(proSdk.auditLogs.write)
 })
 
 process.on("uncaughtException", err => {

From f8f1ec4ce930f93e57a74fa57679b9cc582168db Mon Sep 17 00:00:00 2001
From: mike12345567 <me@michaeldrury.co.uk>
Date: Fri, 3 Nov 2023 17:17:20 +0000
Subject: [PATCH 27/47] Removing old redisProtocol string - it is causing
 confusion and should not be necessary.

---
 packages/backend-core/src/queue/queue.ts |  5 ++--
 packages/backend-core/src/redis/redis.ts |  4 +--
 packages/backend-core/src/redis/utils.ts | 38 +++++++++++++-----------
 3 files changed, 23 insertions(+), 24 deletions(-)

diff --git a/packages/backend-core/src/queue/queue.ts b/packages/backend-core/src/queue/queue.ts
index c0d1861de3..b460a7312b 100644
--- a/packages/backend-core/src/queue/queue.ts
+++ b/packages/backend-core/src/queue/queue.ts
@@ -6,7 +6,6 @@ import BullQueue, { QueueOptions } from "bull"
 import { addListeners, StalledFn } from "./listeners"
 import { Duration } from "../utils"
 import * as timers from "../timers"
-import * as Redis from "ioredis"
 
 // the queue lock is held for 5 minutes
 const QUEUE_LOCK_MS = Duration.fromMinutes(5).toMs()
@@ -27,9 +26,9 @@ export function createQueue<T>(
   jobQueue: JobQueue,
   opts: { removeStalledCb?: StalledFn } = {}
 ): BullQueue.Queue<T> {
-  const { opts: redisOpts, redisProtocolUrl } = getRedisOptions()
+  const { opts: redisOpts } = getRedisOptions()
   const queueConfig: QueueOptions = {
-    redis: redisProtocolUrl! || (redisOpts as Redis.RedisOptions),
+    redis: redisOpts,
     settings: {
       maxStalledCount: 0,
       lockDuration: QUEUE_LOCK_MS,
diff --git a/packages/backend-core/src/redis/redis.ts b/packages/backend-core/src/redis/redis.ts
index d1e2d8989e..1ae2bd7794 100644
--- a/packages/backend-core/src/redis/redis.ts
+++ b/packages/backend-core/src/redis/redis.ts
@@ -91,12 +91,10 @@ function init(selectDb = DEFAULT_SELECT_DB) {
   if (client) {
     client.disconnect()
   }
-  const { redisProtocolUrl, opts, host, port } = getRedisOptions()
+  const { opts, host, port } = getRedisOptions()
 
   if (CLUSTERED) {
     client = new RedisCore.Cluster([{ host, port }], opts)
-  } else if (redisProtocolUrl) {
-    client = new RedisCore(redisProtocolUrl)
   } else {
     client = new RedisCore(opts)
   }
diff --git a/packages/backend-core/src/redis/utils.ts b/packages/backend-core/src/redis/utils.ts
index 34b7275a2b..6cac7b2633 100644
--- a/packages/backend-core/src/redis/utils.ts
+++ b/packages/backend-core/src/redis/utils.ts
@@ -1,4 +1,5 @@
 import env from "../environment"
+import * as Redis from "ioredis"
 
 const SLOT_REFRESH_MS = 2000
 const CONNECT_TIMEOUT_MS = 10000
@@ -74,28 +75,29 @@ export function getRedisOptions() {
   }
   const [host, port] = url.split(":")
 
-  let redisProtocolUrl
-
-  // fully qualified redis URL
-  if (/rediss?:\/\//.test(env.REDIS_URL)) {
-    redisProtocolUrl = env.REDIS_URL
-  }
-
-  const opts: any = {
+  let redisOpts: Redis.RedisOptions = {
     connectTimeout: CONNECT_TIMEOUT_MS,
+    port: parseInt(port),
+    host,
+    password,
   }
+  let opts: Redis.ClusterOptions | Redis.RedisOptions = redisOpts
   if (env.REDIS_CLUSTERED) {
-    opts.redisOptions = {}
-    opts.redisOptions.tls = {}
-    opts.redisOptions.password = password
-    opts.slotsRefreshTimeout = SLOT_REFRESH_MS
-    opts.dnsLookup = (address: string, callback: any) => callback(null, address)
-  } else {
-    opts.host = host
-    opts.port = port
-    opts.password = password
+    opts = {
+      connectTimeout: CONNECT_TIMEOUT_MS,
+      redisOptions: {
+        ...redisOpts,
+        tls: {},
+      },
+      slotsRefreshTimeout: SLOT_REFRESH_MS,
+      dnsLookup: (address: string, callback: any) => callback(null, address),
+    } as Redis.ClusterOptions
+  }
+  return {
+    opts,
+    host,
+    port: parseInt(port),
   }
-  return { opts, host, port: parseInt(port), redisProtocolUrl }
 }
 
 export function addDbPrefix(db: string, key: string) {

From 001cf0130360ea5874fc4979bad65c8c557a1e74 Mon Sep 17 00:00:00 2001
From: mike12345567 <me@michaeldrury.co.uk>
Date: Fri, 3 Nov 2023 18:00:13 +0000
Subject: [PATCH 28/47] Updating public API rate limiting functionality to be
 better typed as well.

---
 packages/backend-core/src/redis/utils.ts      |  3 +-
 .../server/src/api/routes/public/index.ts     | 47 +++++++++----------
 2 files changed, 25 insertions(+), 25 deletions(-)

diff --git a/packages/backend-core/src/redis/utils.ts b/packages/backend-core/src/redis/utils.ts
index 6cac7b2633..e0bdcfcd20 100644
--- a/packages/backend-core/src/redis/utils.ts
+++ b/packages/backend-core/src/redis/utils.ts
@@ -43,7 +43,7 @@ export enum Databases {
 export enum SelectableDatabase {
   DEFAULT = 0,
   SOCKET_IO = 1,
-  UNUSED_1 = 2,
+  RATE_LIMITING = 2,
   UNUSED_2 = 3,
   UNUSED_3 = 4,
   UNUSED_4 = 5,
@@ -96,6 +96,7 @@ export function getRedisOptions() {
   return {
     opts,
     host,
+    password,
     port: parseInt(port),
   }
 }
diff --git a/packages/server/src/api/routes/public/index.ts b/packages/server/src/api/routes/public/index.ts
index 4cc1eff8a4..ab10b2ed74 100644
--- a/packages/server/src/api/routes/public/index.ts
+++ b/packages/server/src/api/routes/public/index.ts
@@ -15,6 +15,16 @@ import env from "../../../environment"
 const Router = require("@koa/router")
 const { RateLimit, Stores } = require("koa2-ratelimit")
 import { middleware, redis } from "@budibase/backend-core"
+import { SelectableDatabase } from "@budibase/backend-core/src/redis/utils"
+
+interface KoaRateLimitOptions {
+  socket: {
+    host: string
+    port: number
+  }
+  password?: string
+  database?: number
+}
 
 const PREFIX = "/api/public/v1"
 // allow a lot more requests when in test
@@ -29,32 +39,21 @@ function getApiLimitPerSecond(): number {
 
 let rateLimitStore: any = null
 if (!env.isTest()) {
-  const REDIS_OPTS = redis.utils.getRedisOptions()
-  let options
-  if (REDIS_OPTS.redisProtocolUrl) {
-    // fully qualified redis URL
-    options = {
-      url: REDIS_OPTS.redisProtocolUrl,
-    }
-  } else {
-    options = {
-      socket: {
-        host: REDIS_OPTS.host,
-        port: REDIS_OPTS.port,
-      },
-    }
+  const { password, host, port } = redis.utils.getRedisOptions()
+  let options: KoaRateLimitOptions = {
+    socket: {
+      host: host,
+      port: port,
+    },
+  }
 
-    if (REDIS_OPTS.opts?.password || REDIS_OPTS.opts.redisOptions?.password) {
-      // @ts-ignore
-      options.password =
-        REDIS_OPTS.opts.password || REDIS_OPTS.opts.redisOptions.password
-    }
+  if (password) {
+    options.password = password
+  }
 
-    if (!env.REDIS_CLUSTERED) {
-      // @ts-ignore
-      // Can't set direct redis db in clustered env
-      options.database = 1
-    }
+  if (!env.REDIS_CLUSTERED) {
+    // Can't set direct redis db in clustered env
+    options.database = SelectableDatabase.RATE_LIMITING
   }
   rateLimitStore = new Stores.Redis(options)
   RateLimit.defaultOptions({

From 7bf307b0c2e86284b93741e94e0e106261d2e638 Mon Sep 17 00:00:00 2001
From: mike12345567 <me@michaeldrury.co.uk>
Date: Fri, 3 Nov 2023 18:03:11 +0000
Subject: [PATCH 29/47] Further updates to typing.

---
 packages/backend-core/src/redis/utils.ts      | 20 +++++++++++--------
 .../server/src/api/routes/public/index.ts     |  2 +-
 2 files changed, 13 insertions(+), 9 deletions(-)

diff --git a/packages/backend-core/src/redis/utils.ts b/packages/backend-core/src/redis/utils.ts
index e0bdcfcd20..5187fe13f8 100644
--- a/packages/backend-core/src/redis/utils.ts
+++ b/packages/backend-core/src/redis/utils.ts
@@ -59,7 +59,7 @@ export enum SelectableDatabase {
   UNUSED_14 = 15,
 }
 
-export function getRedisOptions() {
+export function getRedisConnectionDetails() {
   let password = env.REDIS_PASSWORD
   let url: string[] | string = env.REDIS_URL.split("//")
   // get rid of the protocol
@@ -75,9 +75,18 @@ export function getRedisOptions() {
   }
   const [host, port] = url.split(":")
 
+  return {
+    host,
+    password,
+    port: parseInt(port),
+  }
+}
+
+export function getRedisOptions() {
+  const { host, password, port } = getRedisConnectionDetails()
   let redisOpts: Redis.RedisOptions = {
     connectTimeout: CONNECT_TIMEOUT_MS,
-    port: parseInt(port),
+    port: port,
     host,
     password,
   }
@@ -93,12 +102,7 @@ export function getRedisOptions() {
       dnsLookup: (address: string, callback: any) => callback(null, address),
     } as Redis.ClusterOptions
   }
-  return {
-    opts,
-    host,
-    password,
-    port: parseInt(port),
-  }
+  return opts
 }
 
 export function addDbPrefix(db: string, key: string) {
diff --git a/packages/server/src/api/routes/public/index.ts b/packages/server/src/api/routes/public/index.ts
index ab10b2ed74..b37ed931fc 100644
--- a/packages/server/src/api/routes/public/index.ts
+++ b/packages/server/src/api/routes/public/index.ts
@@ -39,7 +39,7 @@ function getApiLimitPerSecond(): number {
 
 let rateLimitStore: any = null
 if (!env.isTest()) {
-  const { password, host, port } = redis.utils.getRedisOptions()
+  const { password, host, port } = redis.utils.getRedisConnectionDetails()
   let options: KoaRateLimitOptions = {
     socket: {
       host: host,

From 08c4ba00975d074c10f01cabc79aa92089f8022b Mon Sep 17 00:00:00 2001
From: mike12345567 <me@michaeldrury.co.uk>
Date: Fri, 3 Nov 2023 18:06:12 +0000
Subject: [PATCH 30/47] Updating redis option functions usage, as it is no
 longer returned as a part of the getRedisOptions response.

---
 packages/backend-core/src/queue/queue.ts | 2 +-
 packages/backend-core/src/redis/redis.ts | 4 +++-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/packages/backend-core/src/queue/queue.ts b/packages/backend-core/src/queue/queue.ts
index b460a7312b..0657437a3b 100644
--- a/packages/backend-core/src/queue/queue.ts
+++ b/packages/backend-core/src/queue/queue.ts
@@ -26,7 +26,7 @@ export function createQueue<T>(
   jobQueue: JobQueue,
   opts: { removeStalledCb?: StalledFn } = {}
 ): BullQueue.Queue<T> {
-  const { opts: redisOpts } = getRedisOptions()
+  const redisOpts = getRedisOptions()
   const queueConfig: QueueOptions = {
     redis: redisOpts,
     settings: {
diff --git a/packages/backend-core/src/redis/redis.ts b/packages/backend-core/src/redis/redis.ts
index 1ae2bd7794..6f1b573718 100644
--- a/packages/backend-core/src/redis/redis.ts
+++ b/packages/backend-core/src/redis/redis.ts
@@ -16,6 +16,7 @@ import {
   getRedisOptions,
   SEPARATOR,
   SelectableDatabase,
+  getRedisConnectionDetails,
 } from "./utils"
 import * as timers from "../timers"
 
@@ -91,7 +92,8 @@ function init(selectDb = DEFAULT_SELECT_DB) {
   if (client) {
     client.disconnect()
   }
-  const { opts, host, port } = getRedisOptions()
+  const { host, port } = getRedisConnectionDetails()
+  const opts = getRedisOptions()
 
   if (CLUSTERED) {
     client = new RedisCore.Cluster([{ host, port }], opts)

From c43bfda45df9acac45ca136156a41046547283b3 Mon Sep 17 00:00:00 2001
From: mike12345567 <me@michaeldrury.co.uk>
Date: Mon, 6 Nov 2023 12:38:10 +0000
Subject: [PATCH 31/47] Fix for user invitations throwing a 501 error due to
 the way the search was being performed.

---
 .../app/[application]/_components/BuilderSidePanel.svelte     | 4 ++--
 packages/frontend-core/src/fetch/UserFetch.js                 | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte b/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte
index f9a40b09a6..a67c2d3c61 100644
--- a/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte
@@ -112,9 +112,9 @@
     }
     await usersFetch.update({
       query: {
-        appId: query || !filterByAppAccess ? null : prodAppId,
-        email: query,
+        string: { email: query },
       },
+      appId: query || !filterByAppAccess ? null : prodAppId,
       limit: 50,
       paginate: query || !filterByAppAccess ? null : false,
     })
diff --git a/packages/frontend-core/src/fetch/UserFetch.js b/packages/frontend-core/src/fetch/UserFetch.js
index b1478c3a6d..65bfe36058 100644
--- a/packages/frontend-core/src/fetch/UserFetch.js
+++ b/packages/frontend-core/src/fetch/UserFetch.js
@@ -33,7 +33,7 @@ export default class UserFetch extends DataFetch {
     let finalQuery
     // convert old format to new one - we now allow use of the lucene format
     const { appId, paginated, ...rest } = query
-    if (!LuceneUtils.hasFilters(query) && rest.email) {
+    if (!LuceneUtils.hasFilters(query) && rest.email != null) {
       finalQuery = { string: { email: rest.email } }
     } else {
       finalQuery = rest

From 6ee7ae953d9c23a1931ed8d0311f3a340d1bf7d2 Mon Sep 17 00:00:00 2001
From: Mel O'Hagan <mel@budibase.com>
Date: Mon, 6 Nov 2023 13:56:31 +0000
Subject: [PATCH 32/47] Don't trigger required validation early

---
 .../client/src/components/app/forms/RelationshipField.svelte  | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/packages/client/src/components/app/forms/RelationshipField.svelte b/packages/client/src/components/app/forms/RelationshipField.svelte
index 544a1a8434..2c567c49aa 100644
--- a/packages/client/src/components/app/forms/RelationshipField.svelte
+++ b/packages/client/src/components/app/forms/RelationshipField.svelte
@@ -137,7 +137,9 @@
       typeof value === "object" ? value._id : value
     )
     // Make sure field state is valid
-    fieldApi.setValue(values)
+    if (values?.length > 0) {
+      fieldApi.setValue(values)
+    }
     return values
   }
 

From dbcbb2e6b7680ebb6cfb7d01b1d4663703a35c12 Mon Sep 17 00:00:00 2001
From: Sam Rose <hello@samwho.dev>
Date: Mon, 6 Nov 2023 15:33:02 +0000
Subject: [PATCH 33/47] Add test to row patch endpoint, it succeeds. Problem
 must be elsewhere.

---
 .../server/src/api/routes/tests/row.spec.ts   | 50 +++++++++++++++++++
 1 file changed, 50 insertions(+)

diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts
index 92d581d930..48f7ab4f09 100644
--- a/packages/server/src/api/routes/tests/row.spec.ts
+++ b/packages/server/src/api/routes/tests/row.spec.ts
@@ -563,6 +563,56 @@ describe.each([
       await assertRowUsage(rowUsage)
       await assertQueryUsage(queryUsage)
     })
+
+    it("should not overwrite links if those links are not set", async () => {
+      let linkField: FieldSchema = {
+        type: FieldType.LINK,
+        name: "",
+        fieldName: "",
+        constraints: {
+          type: "array",
+          presence: false,
+        },
+        relationshipType: RelationshipType.ONE_TO_MANY,
+        tableId: InternalTable.USER_METADATA,
+      }
+
+      let table = await config.api.table.create({
+        name: "TestTable",
+        type: "table",
+        sourceType: TableSourceType.INTERNAL,
+        sourceId: INTERNAL_TABLE_SOURCE_ID,
+        schema: {
+          user1: { ...linkField, name: "user1", fieldName: "user1" },
+          user2: { ...linkField, name: "user2", fieldName: "user2" },
+        },
+      })
+
+      let user1 = await config.createUser()
+      let user2 = await config.createUser()
+
+      let row = await config.api.row.save(table._id!, {
+        user1: [{ _id: user1._id }],
+        user2: [{ _id: user2._id }],
+      })
+
+      let getResp = await config.api.row.get(table._id!, row._id!)
+      expect(getResp.body.user1[0]._id).toEqual(user1._id)
+      expect(getResp.body.user2[0]._id).toEqual(user2._id)
+
+      let patchResp = await config.api.row.patch(table._id!, {
+        _id: row._id!,
+        _rev: row._rev!,
+        tableId: table._id!,
+        user1: [{ _id: user2._id }],
+      })
+      expect(patchResp.user1[0]._id).toEqual(user2._id)
+      expect(patchResp.user2[0]._id).toEqual(user2._id)
+
+      getResp = await config.api.row.get(table._id!, row._id!)
+      expect(getResp.body.user1[0]._id).toEqual(user2._id)
+      expect(getResp.body.user2[0]._id).toEqual(user2._id)
+    })
   })
 
   describe("destroy", () => {

From b02512fd3c4203b6dc6697b5a6b7500e14612971 Mon Sep 17 00:00:00 2001
From: Sam Rose <hello@samwho.dev>
Date: Mon, 6 Nov 2023 15:56:58 +0000
Subject: [PATCH 34/47] Create a test of a table with 2 link fields in
 updateRow.spec.ts.

---
 .../src/automations/tests/updateRow.spec.js   |  44 -------
 .../src/automations/tests/updateRow.spec.ts   | 107 ++++++++++++++++++
 .../src/automations/tests/utilities/index.ts  |   6 +-
 3 files changed, 110 insertions(+), 47 deletions(-)
 delete mode 100644 packages/server/src/automations/tests/updateRow.spec.js
 create mode 100644 packages/server/src/automations/tests/updateRow.spec.ts

diff --git a/packages/server/src/automations/tests/updateRow.spec.js b/packages/server/src/automations/tests/updateRow.spec.js
deleted file mode 100644
index 77383d80e9..0000000000
--- a/packages/server/src/automations/tests/updateRow.spec.js
+++ /dev/null
@@ -1,44 +0,0 @@
-const setup = require("./utilities")
-
-describe("test the update row action", () => {
-  let table, row, inputs
-  let config = setup.getConfig()
-
-  beforeAll(async () => {
-    await config.init()
-    table = await config.createTable()
-    row = await config.createRow()
-    inputs = {
-      rowId: row._id,
-      row: {
-        ...row,
-        name: "Updated name",
-        // put a falsy option in to be removed
-        description: "",
-      }
-    }
-  })
-
-  afterAll(setup.afterAll)
-
-  it("should be able to run the action", async () => {
-    const res = await setup.runStep(setup.actions.UPDATE_ROW.stepId, inputs)
-    expect(res.success).toEqual(true)
-    const updatedRow = await config.getRow(table._id, res.id)
-    expect(updatedRow.name).toEqual("Updated name")
-    expect(updatedRow.description).not.toEqual("")
-  })
-
-  it("should check invalid inputs return an error", async () => {
-    const res = await setup.runStep(setup.actions.UPDATE_ROW.stepId, {})
-    expect(res.success).toEqual(false)
-  })
-
-  it("should return an error when table doesn't exist", async () => {
-    const res = await setup.runStep(setup.actions.UPDATE_ROW.stepId, {
-      row: { _id: "invalid" },
-      rowId: "invalid",
-    })
-    expect(res.success).toEqual(false)
-  })
-})
diff --git a/packages/server/src/automations/tests/updateRow.spec.ts b/packages/server/src/automations/tests/updateRow.spec.ts
new file mode 100644
index 0000000000..63925b0d3b
--- /dev/null
+++ b/packages/server/src/automations/tests/updateRow.spec.ts
@@ -0,0 +1,107 @@
+import {
+  FieldSchema,
+  FieldType,
+  INTERNAL_TABLE_SOURCE_ID,
+  InternalTable,
+  RelationshipType,
+  Row,
+  Table,
+  TableSourceType,
+} from "@budibase/types"
+
+import * as setup from "./utilities"
+
+describe("test the update row action", () => {
+  let table: Table, row: Row, inputs: any
+  let config = setup.getConfig()
+
+  beforeAll(async () => {
+    await config.init()
+    table = await config.createTable()
+    row = await config.createRow()
+    inputs = {
+      rowId: row._id,
+      row: {
+        ...row,
+        name: "Updated name",
+        // put a falsy option in to be removed
+        description: "",
+      },
+    }
+  })
+
+  afterAll(setup.afterAll)
+
+  it("should be able to run the action", async () => {
+    const res = await setup.runStep(setup.actions.UPDATE_ROW.stepId, inputs)
+    expect(res.success).toEqual(true)
+    const updatedRow = await config.getRow(table._id!, res.id)
+    expect(updatedRow.name).toEqual("Updated name")
+    expect(updatedRow.description).not.toEqual("")
+  })
+
+  it("should check invalid inputs return an error", async () => {
+    const res = await setup.runStep(setup.actions.UPDATE_ROW.stepId, {})
+    expect(res.success).toEqual(false)
+  })
+
+  it("should return an error when table doesn't exist", async () => {
+    const res = await setup.runStep(setup.actions.UPDATE_ROW.stepId, {
+      row: { _id: "invalid" },
+      rowId: "invalid",
+    })
+    expect(res.success).toEqual(false)
+  })
+
+  it("should not overwrite links if those links are not set", async () => {
+    let linkField: FieldSchema = {
+      type: FieldType.LINK,
+      name: "",
+      fieldName: "",
+      constraints: {
+        type: "array",
+        presence: false,
+      },
+      relationshipType: RelationshipType.ONE_TO_MANY,
+      tableId: InternalTable.USER_METADATA,
+    }
+
+    let table = await config.api.table.create({
+      name: "TestTable",
+      type: "table",
+      sourceType: TableSourceType.INTERNAL,
+      sourceId: INTERNAL_TABLE_SOURCE_ID,
+      schema: {
+        user1: { ...linkField, name: "user1", fieldName: "user1" },
+        user2: { ...linkField, name: "user2", fieldName: "user2" },
+      },
+    })
+
+    let user1 = await config.createUser()
+    let user2 = await config.createUser()
+
+    let row = await config.api.row.save(table._id!, {
+      user1: [{ _id: user1._id }],
+      user2: [{ _id: user2._id }],
+    })
+
+    let getResp = await config.api.row.get(table._id!, row._id!)
+    expect(getResp.body.user1[0]._id).toEqual(user1._id)
+    expect(getResp.body.user2[0]._id).toEqual(user2._id)
+
+    let stepResp = await setup.runStep(setup.actions.UPDATE_ROW.stepId, {
+      rowId: row._id,
+      row: {
+        _id: row._id,
+        _rev: row._rev,
+        tableId: row.tableId,
+        user1: [{ _id: user2._id }],
+      },
+    })
+    expect(stepResp.success).toEqual(true)
+
+    getResp = await config.api.row.get(table._id!, row._id!)
+    expect(getResp.body.user1[0]._id).toEqual(user2._id)
+    expect(getResp.body.user2[0]._id).toEqual(user2._id)
+  })
+})
diff --git a/packages/server/src/automations/tests/utilities/index.ts b/packages/server/src/automations/tests/utilities/index.ts
index 9ba4f950f3..cd3ea289ca 100644
--- a/packages/server/src/automations/tests/utilities/index.ts
+++ b/packages/server/src/automations/tests/utilities/index.ts
@@ -4,11 +4,11 @@ import { BUILTIN_ACTION_DEFINITIONS, getAction } from "../../actions"
 import emitter from "../../../events/index"
 import env from "../../../environment"
 
-let config: any
+let config: TestConfig
 
-export function getConfig() {
+export function getConfig(): TestConfig {
   if (!config) {
-    config = new TestConfig(false)
+    config = new TestConfig(true)
   }
   return config
 }

From 2684b73768ce990b542a7575feeb864b1fbb613c Mon Sep 17 00:00:00 2001
From: Sam Rose <hello@samwho.dev>
Date: Mon, 6 Nov 2023 16:40:27 +0000
Subject: [PATCH 35/47] Fix type error.

---
 packages/server/src/automations/tests/automation.spec.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/server/src/automations/tests/automation.spec.ts b/packages/server/src/automations/tests/automation.spec.ts
index 67ff6d40ec..c37c9cc7ce 100644
--- a/packages/server/src/automations/tests/automation.spec.ts
+++ b/packages/server/src/automations/tests/automation.spec.ts
@@ -36,7 +36,7 @@ describe("Run through some parts of the automations system", () => {
   it("should be able to init in builder", async () => {
     const automation: Automation = {
       ...basicAutomation(),
-      appId: config.appId,
+      appId: config.appId!,
     }
     const fields: any = { a: 1, appId: config.appId }
     await triggers.externalTrigger(automation, fields)

From 9bb9fb55496b05ac81f379a106c1de9dc2b48f4d Mon Sep 17 00:00:00 2001
From: Dean <deanhannigan@gmail.com>
Date: Mon, 6 Nov 2023 17:00:47 +0000
Subject: [PATCH 36/47] Reintroduce filtering and sorting for exported row data

---
 packages/server/src/api/controllers/row/index.ts    |  4 +++-
 packages/server/src/sdk/app/rows/search.ts          | 10 +++++++++-
 packages/server/src/sdk/app/rows/search/external.ts | 10 ++++++----
 packages/server/src/sdk/app/rows/search/internal.ts |  9 +++++++--
 packages/types/src/api/web/app/rows.ts              |  3 +++
 5 files changed, 28 insertions(+), 8 deletions(-)

diff --git a/packages/server/src/api/controllers/row/index.ts b/packages/server/src/api/controllers/row/index.ts
index 1a6747a085..018283c8c5 100644
--- a/packages/server/src/api/controllers/row/index.ts
+++ b/packages/server/src/api/controllers/row/index.ts
@@ -254,7 +254,7 @@ export const exportRows = async (
 
   const format = ctx.query.format
 
-  const { rows, columns, query } = ctx.request.body
+  const { rows, columns, query, sort, sortOrder } = ctx.request.body
   if (typeof format !== "string" || !exporters.isFormat(format)) {
     ctx.throw(
       400,
@@ -272,6 +272,8 @@ export const exportRows = async (
         rowIds: rows,
         columns,
         query,
+        sort,
+        sortOrder,
       })
       ctx.attachment(fileName)
       return apiFileReturn(content)
diff --git a/packages/server/src/sdk/app/rows/search.ts b/packages/server/src/sdk/app/rows/search.ts
index 31f7c74601..36ef538d8b 100644
--- a/packages/server/src/sdk/app/rows/search.ts
+++ b/packages/server/src/sdk/app/rows/search.ts
@@ -1,4 +1,10 @@
-import { Row, SearchFilters, SearchParams } from "@budibase/types"
+import {
+  Row,
+  SearchFilters,
+  SearchParams,
+  SortOrder,
+  SortType,
+} from "@budibase/types"
 import { isExternalTableID } from "../../../integrations/utils"
 import * as internal from "./search/internal"
 import * as external from "./search/external"
@@ -32,6 +38,8 @@ export interface ExportRowsParams {
   rowIds?: string[]
   columns?: string[]
   query?: SearchFilters
+  sort?: string
+  sortOrder?: SortOrder
 }
 
 export interface ExportRowsResult {
diff --git a/packages/server/src/sdk/app/rows/search/external.ts b/packages/server/src/sdk/app/rows/search/external.ts
index 981ae1bf8d..974900ba6d 100644
--- a/packages/server/src/sdk/app/rows/search/external.ts
+++ b/packages/server/src/sdk/app/rows/search/external.ts
@@ -98,12 +98,12 @@ export async function search(options: SearchParams) {
 export async function exportRows(
   options: ExportRowsParams
 ): Promise<ExportRowsResult> {
-  const { tableId, format, columns, rowIds } = options
+  const { tableId, format, columns, rowIds, query, sort, sortOrder } = options
   const { datasourceId, tableName } = breakExternalTableId(tableId)
 
-  let query: SearchFilters = {}
+  let requestQuery: SearchFilters = {}
   if (rowIds?.length) {
-    query = {
+    requestQuery = {
       oneOf: {
         _id: rowIds.map((row: string) => {
           const ids = JSON.parse(
@@ -119,6 +119,8 @@ export async function exportRows(
         }),
       },
     }
+  } else {
+    requestQuery = query || {}
   }
 
   const datasource = await sdk.datasources.get(datasourceId!)
@@ -126,7 +128,7 @@ export async function exportRows(
     throw new HTTPError("Datasource has not been configured for plus API.", 400)
   }
 
-  let result = await search({ tableId, query })
+  let result = await search({ tableId, query: requestQuery, sort, sortOrder })
   let rows: Row[] = []
 
   // Filter data to only specified columns if required
diff --git a/packages/server/src/sdk/app/rows/search/internal.ts b/packages/server/src/sdk/app/rows/search/internal.ts
index 1aec8a321e..e31bda1a15 100644
--- a/packages/server/src/sdk/app/rows/search/internal.ts
+++ b/packages/server/src/sdk/app/rows/search/internal.ts
@@ -84,7 +84,7 @@ export async function search(options: SearchParams) {
 export async function exportRows(
   options: ExportRowsParams
 ): Promise<ExportRowsResult> {
-  const { tableId, format, rowIds, columns, query } = options
+  const { tableId, format, rowIds, columns, query, sort, sortOrder } = options
   const db = context.getAppDB()
   const table = await sdk.tables.getTable(tableId)
 
@@ -99,7 +99,12 @@ export async function exportRows(
 
     result = await outputProcessing(table, response)
   } else if (query) {
-    let searchResponse = await search({ tableId, query })
+    let searchResponse = await search({
+      tableId,
+      query,
+      sort,
+      sortOrder,
+    })
     result = searchResponse.rows
   }
 
diff --git a/packages/types/src/api/web/app/rows.ts b/packages/types/src/api/web/app/rows.ts
index 62ea90a6a4..dad3286754 100644
--- a/packages/types/src/api/web/app/rows.ts
+++ b/packages/types/src/api/web/app/rows.ts
@@ -1,5 +1,6 @@
 import { SearchFilters, SearchParams } from "../../../sdk"
 import { Row } from "../../../documents"
+import { SortOrder } from "../../../api"
 import { ReadStream } from "fs"
 
 export interface SaveRowRequest extends Row {}
@@ -34,6 +35,8 @@ export interface ExportRowsRequest {
   rows: string[]
   columns?: string[]
   query?: SearchFilters
+  sort?: string
+  sortOrder?: SortOrder
 }
 
 export type ExportRowsResponse = ReadStream

From f21addeb7170f5e83a662e9fb05b6f3d712f8798 Mon Sep 17 00:00:00 2001
From: Sam Rose <hello@samwho.dev>
Date: Mon, 6 Nov 2023 17:34:30 +0000
Subject: [PATCH 37/47] Add another test to make sure relationships are cleared
 when asked.

---
 .../src/automations/tests/updateRow.spec.ts   | 70 +++++++++++++++++--
 .../server/src/tests/utilities/api/table.ts   | 10 ++-
 2 files changed, 75 insertions(+), 5 deletions(-)

diff --git a/packages/server/src/automations/tests/updateRow.spec.ts b/packages/server/src/automations/tests/updateRow.spec.ts
index 63925b0d3b..7e369f1ecb 100644
--- a/packages/server/src/automations/tests/updateRow.spec.ts
+++ b/packages/server/src/automations/tests/updateRow.spec.ts
@@ -10,6 +10,7 @@ import {
 } from "@budibase/types"
 
 import * as setup from "./utilities"
+import * as uuid from "uuid"
 
 describe("test the update row action", () => {
   let table: Table, row: Row, inputs: any
@@ -67,13 +68,13 @@ describe("test the update row action", () => {
     }
 
     let table = await config.api.table.create({
-      name: "TestTable",
+      name: uuid.v4(),
       type: "table",
       sourceType: TableSourceType.INTERNAL,
       sourceId: INTERNAL_TABLE_SOURCE_ID,
       schema: {
-        user1: { ...linkField, name: "user1", fieldName: "user1" },
-        user2: { ...linkField, name: "user2", fieldName: "user2" },
+        user1: { ...linkField, name: "user1", fieldName: uuid.v4() },
+        user2: { ...linkField, name: "user2", fieldName: uuid.v4() },
       },
     })
 
@@ -95,7 +96,8 @@ describe("test the update row action", () => {
         _id: row._id,
         _rev: row._rev,
         tableId: row.tableId,
-        user1: [{ _id: user2._id }],
+        user1: [user2._id],
+        user2: "",
       },
     })
     expect(stepResp.success).toEqual(true)
@@ -104,4 +106,64 @@ describe("test the update row action", () => {
     expect(getResp.body.user1[0]._id).toEqual(user2._id)
     expect(getResp.body.user2[0]._id).toEqual(user2._id)
   })
+
+  it("should overwrite links if those links are not set and we ask it do", async () => {
+    let linkField: FieldSchema = {
+      type: FieldType.LINK,
+      name: "",
+      fieldName: "",
+      constraints: {
+        type: "array",
+        presence: false,
+      },
+      relationshipType: RelationshipType.ONE_TO_MANY,
+      tableId: InternalTable.USER_METADATA,
+    }
+
+    let table = await config.api.table.create({
+      name: uuid.v4(),
+      type: "table",
+      sourceType: TableSourceType.INTERNAL,
+      sourceId: INTERNAL_TABLE_SOURCE_ID,
+      schema: {
+        user1: { ...linkField, name: "user1", fieldName: uuid.v4() },
+        user2: { ...linkField, name: "user2", fieldName: uuid.v4() },
+      },
+    })
+
+    let user1 = await config.createUser()
+    let user2 = await config.createUser()
+
+    let row = await config.api.row.save(table._id!, {
+      user1: [{ _id: user1._id }],
+      user2: [{ _id: user2._id }],
+    })
+
+    let getResp = await config.api.row.get(table._id!, row._id!)
+    expect(getResp.body.user1[0]._id).toEqual(user1._id)
+    expect(getResp.body.user2[0]._id).toEqual(user2._id)
+
+    let stepResp = await setup.runStep(setup.actions.UPDATE_ROW.stepId, {
+      rowId: row._id,
+      row: {
+        _id: row._id,
+        _rev: row._rev,
+        tableId: row.tableId,
+        user1: [user2._id],
+        user2: "",
+      },
+      meta: {
+        fields: {
+          user2: {
+            clearRelationships: true,
+          },
+        },
+      },
+    })
+    expect(stepResp.success).toEqual(true)
+
+    getResp = await config.api.row.get(table._id!, row._id!)
+    expect(getResp.body.user1[0]._id).toEqual(user2._id)
+    expect(getResp.body.user2).toBeUndefined()
+  })
 })
diff --git a/packages/server/src/tests/utilities/api/table.ts b/packages/server/src/tests/utilities/api/table.ts
index b80c940697..ffd9e19ee8 100644
--- a/packages/server/src/tests/utilities/api/table.ts
+++ b/packages/server/src/tests/utilities/api/table.ts
@@ -22,7 +22,15 @@ export class TableAPI extends TestAPI {
       .send(data)
       .set(this.config.defaultHeaders())
       .expect("Content-Type", /json/)
-      .expect(expectStatus)
+
+    if (res.status !== expectStatus) {
+      throw new Error(
+        `Expected status ${expectStatus} but got ${
+          res.status
+        } with body ${JSON.stringify(res.body)}`
+      )
+    }
+
     return res.body
   }
 

From a5246bc08aa8a335d1d06efa7bfbd395cdd39d49 Mon Sep 17 00:00:00 2001
From: Greg <129239095+gcglinton@users.noreply.github.com>
Date: Mon, 6 Nov 2023 13:53:22 -0500
Subject: [PATCH 38/47] Update nginx-ssl.conf

---
 hosting/letsencrypt/nginx-ssl.conf | 61 +++++++++++++++++++++++++++---
 1 file changed, 56 insertions(+), 5 deletions(-)

diff --git a/hosting/letsencrypt/nginx-ssl.conf b/hosting/letsencrypt/nginx-ssl.conf
index 50c5e0198a..b3f51e5cc5 100644
--- a/hosting/letsencrypt/nginx-ssl.conf
+++ b/hosting/letsencrypt/nginx-ssl.conf
@@ -2,16 +2,18 @@ server {
     listen       443 ssl default_server;
     listen  [::]:443 ssl default_server;
     server_name  _;
-    ssl_certificate /etc/letsencrypt/live/CUSTOM_DOMAIN/fullchain.pem;
-    ssl_certificate_key /etc/letsencrypt/live/CUSTOM_DOMAIN/privkey.pem;
-    include /etc/letsencrypt/options-ssl-nginx.conf;
-    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
-
+    error_log            /dev/stderr warn;
+    access_log           /dev/stdout main;
     client_max_body_size 1000m;
     ignore_invalid_headers off;
     proxy_buffering off;
     # port_in_redirect off;
 
+    ssl_certificate /etc/letsencrypt/live/CUSTOM_DOMAIN/fullchain.pem;
+    ssl_certificate_key /etc/letsencrypt/live/CUSTOM_DOMAIN/privkey.pem;
+    include /etc/letsencrypt/options-ssl-nginx.conf;
+    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
+
     location ^~ /.well-known/acme-challenge/ {
         default_type "text/plain";
         root         /var/www/html;
@@ -47,6 +49,24 @@ server {
         rewrite ^/worker/(.*)$ /$1 break;
     }
 
+    location /api/backups/ {
+        # calls to export apps are limited
+        limit_req zone=ratelimit burst=20 nodelay;
+
+        # 1800s timeout for app export requests
+        proxy_read_timeout 1800s;
+        proxy_connect_timeout 1800s;
+        proxy_send_timeout 1800s;
+
+        proxy_http_version 1.1;
+        proxy_set_header    Connection          $connection_upgrade;
+        proxy_set_header    Upgrade             $http_upgrade;
+        proxy_set_header    X-Real-IP           $remote_addr;
+        proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
+
+        proxy_pass      http://127.0.0.1:4001;
+    }
+
     location /api/ {
         # calls to the API are rate limited with bursting
         limit_req zone=ratelimit burst=20 nodelay;
@@ -70,18 +90,49 @@ server {
         rewrite ^/db/(.*)$ /$1 break;
     }
 
+    location /socket/ {
+        proxy_http_version  1.1;
+        proxy_set_header    Upgrade     $http_upgrade;
+        proxy_set_header    Connection  'upgrade';
+        proxy_set_header    Host        $host;
+        proxy_cache_bypass  $http_upgrade;
+        proxy_pass          http://127.0.0.1:4001;
+    }
+
     location / {
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_set_header Host $http_host;
 
         proxy_connect_timeout 300;
         proxy_http_version 1.1;
         proxy_set_header Connection "";
         chunked_transfer_encoding off;
+        
         proxy_pass      http://127.0.0.1:9000;
     }
 
+    location /files/signed/ {
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+
+        # IMPORTANT: Signed urls will inspect the host header of the request.
+        # Normally a signed url will need to be generated with a specified client host in mind.
+        # To support dynamic hosts, e.g. some unknown self-hosted installation url,
+        # use a predefined host header. The host 'minio-service' is also used at the time of url signing.
+        proxy_set_header Host minio-service;
+
+        proxy_connect_timeout 300;
+        proxy_http_version 1.1;
+        proxy_set_header Connection "";
+        chunked_transfer_encoding off;
+
+        proxy_pass      http://127.0.0.1:9000;
+        rewrite ^/files/signed/(.*)$ /$1 break;
+    }
+
     client_header_timeout 60;
     client_body_timeout   60;
     keepalive_timeout     60;

From 466d5350e23ea8cd5ac67eaa35e2bfb6374c691d Mon Sep 17 00:00:00 2001
From: Budibase Staging Release Bot <>
Date: Mon, 6 Nov 2023 19:50:27 +0000
Subject: [PATCH 39/47] Bump version to 2.13.1

---
 lerna.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lerna.json b/lerna.json
index 611cf7d32b..2126adac29 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,5 +1,5 @@
 {
-  "version": "2.13.0",
+  "version": "2.13.1",
   "npmClient": "yarn",
   "packages": [
     "packages/*"

From c169a7376499747dace7471320737cb22c121319 Mon Sep 17 00:00:00 2001
From: Mel O'Hagan <mel@budibase.com>
Date: Tue, 7 Nov 2023 09:19:29 +0000
Subject: [PATCH 40/47] readonly css

---
 packages/bbui/src/Form/Core/Checkbox.svelte           | 9 ++++-----
 packages/bbui/src/Form/Core/CheckboxGroup.svelte      | 9 ++++-----
 packages/bbui/src/Form/Core/RadioGroup.svelte         | 9 ++++-----
 packages/client/src/components/app/forms/Field.svelte | 4 ++++
 4 files changed, 16 insertions(+), 15 deletions(-)

diff --git a/packages/bbui/src/Form/Core/Checkbox.svelte b/packages/bbui/src/Form/Core/Checkbox.svelte
index 3eaaf4dede..e24f5669eb 100644
--- a/packages/bbui/src/Form/Core/Checkbox.svelte
+++ b/packages/bbui/src/Form/Core/Checkbox.svelte
@@ -25,16 +25,12 @@
   class:is-invalid={!!error}
   class:checked={value}
   class:is-indeterminate={indeterminate}
+  class:readonly
 >
   <input
     checked={value}
     {disabled}
     on:change={onChange}
-    on:click={e => {
-      if (readonly) {
-        e.preventDefault()
-      }
-    }}
     type="checkbox"
     class="spectrum-Checkbox-input"
     {id}
@@ -74,4 +70,7 @@
   .spectrum-Checkbox-input {
     opacity: 0;
   }
+  .readonly {
+    pointer-events: none;
+  }
 </style>
diff --git a/packages/bbui/src/Form/Core/CheckboxGroup.svelte b/packages/bbui/src/Form/Core/CheckboxGroup.svelte
index faf37f3ad8..66ac55561b 100644
--- a/packages/bbui/src/Form/Core/CheckboxGroup.svelte
+++ b/packages/bbui/src/Form/Core/CheckboxGroup.svelte
@@ -35,17 +35,13 @@
         title={getOptionLabel(option)}
         class="spectrum-Checkbox spectrum-FieldGroup-item"
         class:is-invalid={!!error}
+        class:readonly
       >
         <label
           class="spectrum-Checkbox spectrum-Checkbox--sizeM spectrum-FieldGroup-item"
         >
           <input
             on:change={onChange}
-            on:click={e => {
-              if (readonly) {
-                e.preventDefault()
-              }
-            }}
             type="checkbox"
             class="spectrum-Checkbox-input"
             value={optionValue}
@@ -72,4 +68,7 @@
   .spectrum-Checkbox-input {
     opacity: 0;
   }
+  .readonly {
+    pointer-events: none;
+  }
 </style>
diff --git a/packages/bbui/src/Form/Core/RadioGroup.svelte b/packages/bbui/src/Form/Core/RadioGroup.svelte
index fc99fafd40..648c46b5fc 100644
--- a/packages/bbui/src/Form/Core/RadioGroup.svelte
+++ b/packages/bbui/src/Form/Core/RadioGroup.svelte
@@ -41,14 +41,10 @@
         title={getOptionTitle(option)}
         class="spectrum-Radio spectrum-FieldGroup-item spectrum-Radio--emphasized"
         class:is-invalid={!!error}
+        class:readonly
       >
         <input
           on:change={onChange}
-          on:click={e => {
-            if (readonly) {
-              e.preventDefault()
-            }
-          }}
           bind:group={value}
           value={getOptionValue(option)}
           type="radio"
@@ -68,4 +64,7 @@
   .spectrum-Radio-input {
     opacity: 0;
   }
+  .readonly {
+    pointer-events: none;
+  }
 </style>
diff --git a/packages/client/src/components/app/forms/Field.svelte b/packages/client/src/components/app/forms/Field.svelte
index 1fa1aeda5a..8878b25989 100644
--- a/packages/client/src/components/app/forms/Field.svelte
+++ b/packages/client/src/components/app/forms/Field.svelte
@@ -78,6 +78,7 @@
       contenteditable={$component.editing}
       on:blur={$component.editing ? updateLabel : null}
       class:hidden={!label}
+      class:readonly
       for={fieldState?.fieldId}
       class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${labelClass}`}
     >
@@ -138,4 +139,7 @@
   .spectrum-FieldLabel--left {
     padding-right: var(--spectrum-global-dimension-size-200);
   }
+  .readonly {
+    pointer-events: none;
+  }
 </style>

From f28eb054f38b66c40b33d9b9f946481c15b3556f Mon Sep 17 00:00:00 2001
From: melohagan <101575380+melohagan@users.noreply.github.com>
Date: Tue, 7 Nov 2023 11:59:51 +0000
Subject: [PATCH 41/47] Set the relationship type (#12275)

* Set the relationship type

* Add users type

* Set user relationship type saveColumn

* Set relationshipType for users type on save

* Add relatioshipType to schema

* Refactor

* Check isUsersColumn

* Make relationshipType optional

---------

Co-authored-by: Michael Drury <me@michaeldrury.co.uk>
---
 .../backend/DataTable/modals/CreateEditColumn.svelte   | 10 ++++++++++
 packages/types/src/documents/app/table/schema.ts       |  1 +
 2 files changed, 11 insertions(+)

diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte
index d5a9aba488..4eb1f962f0 100644
--- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte
+++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte
@@ -44,6 +44,8 @@
   const NUMBER_TYPE = FIELDS.NUMBER.type
   const JSON_TYPE = FIELDS.JSON.type
   const DATE_TYPE = FIELDS.DATETIME.type
+  const USER_TYPE = FIELDS.USER.subtype
+  const USERS_TYPE = FIELDS.USERS.subtype
 
   const dispatch = createEventDispatcher()
   const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"]
@@ -287,6 +289,14 @@
     if (saveColumn.type !== LINK_TYPE) {
       delete saveColumn.fieldName
     }
+    if (isUsersColumn(saveColumn)) {
+      if (saveColumn.subtype === USER_TYPE) {
+        saveColumn.relationshipType = RelationshipType.ONE_TO_MANY
+      } else if (saveColumn.subtype === USERS_TYPE) {
+        saveColumn.relationshipType = RelationshipType.MANY_TO_MANY
+      }
+    }
+
     try {
       await tables.saveField({
         originalName,
diff --git a/packages/types/src/documents/app/table/schema.ts b/packages/types/src/documents/app/table/schema.ts
index 755ccf61e7..19a7303072 100644
--- a/packages/types/src/documents/app/table/schema.ts
+++ b/packages/types/src/documents/app/table/schema.ts
@@ -102,6 +102,7 @@ export interface BBReferenceFieldMetadata
   extends Omit<BaseFieldSchema, "subtype"> {
   type: FieldType.BB_REFERENCE
   subtype: FieldSubtype.USER | FieldSubtype.USERS
+  relationshipType?: RelationshipType
 }
 
 export interface FieldConstraints {

From dc59245b39da5c4b8ed95cebba5931adf8028d4c Mon Sep 17 00:00:00 2001
From: Budibase Staging Release Bot <>
Date: Tue, 7 Nov 2023 12:20:21 +0000
Subject: [PATCH 42/47] Bump version to 2.13.2

---
 lerna.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lerna.json b/lerna.json
index 2126adac29..bfcac5633c 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,5 +1,5 @@
 {
-  "version": "2.13.1",
+  "version": "2.13.2",
   "npmClient": "yarn",
   "packages": [
     "packages/*"

From 7dd4521ea83a3089902c66c8c9968668b7cbcf7a Mon Sep 17 00:00:00 2001
From: Adria Navarro <adria@budibase.com>
Date: Tue, 7 Nov 2023 13:55:34 +0100
Subject: [PATCH 43/47] Fix fetching tag

---
 .github/workflows/tag-release.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/tag-release.yml b/.github/workflows/tag-release.yml
index 78c07a037c..13d59d1019 100644
--- a/.github/workflows/tag-release.yml
+++ b/.github/workflows/tag-release.yml
@@ -45,8 +45,8 @@ jobs:
           BUMP_TYPE=${BUMP_TYPE_INPUT:-"patch"}
           ./versionCommit.sh $BUMP_TYPE
 
-
-          new_version=$(./getCurrentVersion.sh)
+          cd ..
+          new_version=$(./scripts/getCurrentVersion.sh)
           echo "version=$new_version" >> $GITHUB_OUTPUT
 
   trigger-release:

From cc5418e16e942267217ec62658ce191039634e37 Mon Sep 17 00:00:00 2001
From: Andrew Kingston <andrew@kingston.dev>
Date: Tue, 7 Nov 2023 15:10:27 +0000
Subject: [PATCH 44/47] Update pull_request_template.md

Remove colon in pull request template which break styling
---
 pull_request_template.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pull_request_template.md b/pull_request_template.md
index 1dd1a1d45d..405059b2ab 100644
--- a/pull_request_template.md
+++ b/pull_request_template.md
@@ -1,7 +1,7 @@
 ## Description
 _Describe the problem or feature in addition to a link to the relevant github issues._
 
-### Addresses:
+## Addresses
 - `<Enter the Link to the issue(s) this PR addresses>`
 - ...more if required
 

From 86cfd76b5a7edc02614431200a6640fc43b5421d Mon Sep 17 00:00:00 2001
From: Budibase Staging Release Bot <>
Date: Tue, 7 Nov 2023 16:37:31 +0000
Subject: [PATCH 45/47] Bump version to 2.13.3

---
 lerna.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lerna.json b/lerna.json
index bfcac5633c..25ec556c56 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,5 +1,5 @@
 {
-  "version": "2.13.2",
+  "version": "2.13.3",
   "npmClient": "yarn",
   "packages": [
     "packages/*"

From 9d29e3dddab4daeb3b656e71a7fe1d5b7c726d74 Mon Sep 17 00:00:00 2001
From: Adria Navarro <adria@budibase.com>
Date: Tue, 7 Nov 2023 18:31:44 +0100
Subject: [PATCH 46/47] Update pro ref

---
 packages/pro | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/pro b/packages/pro
index 3820c0c93a..cffa01ac67 160000
--- a/packages/pro
+++ b/packages/pro
@@ -1 +1 @@
-Subproject commit 3820c0c93a3e448e10a60a9feb5396844b537ca8
+Subproject commit cffa01ac67a2ca7401e848593f95f881e103a33d

From 4d5d9a0685a9c340c9859414363bb8654b6e0ef6 Mon Sep 17 00:00:00 2001
From: Adria Navarro <adria@budibase.com>
Date: Tue, 7 Nov 2023 18:42:41 +0100
Subject: [PATCH 47/47] Update pro ref

---
 packages/pro | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/pro b/packages/pro
index cffa01ac67..ad9a0085be 160000
--- a/packages/pro
+++ b/packages/pro
@@ -1 +1 @@
-Subproject commit cffa01ac67a2ca7401e848593f95f881e103a33d
+Subproject commit ad9a0085bee0c4f3184acd86cadd872ea9917e88