diff --git a/packages/builder/cypress/integration/createAutomation.spec.js b/packages/builder/cypress/integration/createAutomation.spec.js index 92d1f907bd..efc185be7c 100644 --- a/packages/builder/cypress/integration/createAutomation.spec.js +++ b/packages/builder/cypress/integration/createAutomation.spec.js @@ -17,23 +17,33 @@ context("Create a automation", () => { cy.get("[data-cy=new-automation]").click() cy.get(".modal").within(() => { cy.get("input").type("Add Row") - cy.get(".buttons").contains("Create").click() + cy.get(".buttons") + .contains("Create") + .click() }) // Add trigger cy.contains("Trigger").click() - cy.contains("Row Saved").click() + cy.contains("Row Created").click() cy.get(".setup").within(() => { - cy.get("select").first().select("dog") + cy.get("select") + .first() + .select("dog") }) // Create action cy.contains("Action").click() cy.contains("Create Row").click() cy.get(".setup").within(() => { - cy.get("select").first().select("dog") - cy.get("input").first().type("goodboy") - cy.get("input").eq(1).type("11") + cy.get("select") + .first() + .select("dog") + cy.get("input") + .first() + .type("goodboy") + cy.get("input") + .eq(1) + .type("11") }) // Save diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index eaf64b23b9..429ba1c683 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -7,6 +7,7 @@ import { TableNames } from "../constants" // Regex to match all instances of template strings const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g +const CAPTURE_HBS_TEMPLATE = /{{[\S\s]*?}}/g /** * Gets all bindable data context fields and instance fields. @@ -282,6 +283,20 @@ const buildFormSchema = component => { return schema } +/** + * Recurses the input object to remove any instances of bindings. + */ +export function removeBindings(obj) { + for (let [key, value] of Object.entries(obj)) { + if (typeof value === "object") { + obj[key] = removeBindings(value) + } else if (typeof value === "string") { + obj[key] = value.replace(CAPTURE_HBS_TEMPLATE, "Invalid binding") + } + } + return obj +} + /** * utility function for the readableToRuntimeBinding and runtimeToReadableBinding. */ diff --git a/packages/builder/src/builderStore/store/backend.js b/packages/builder/src/builderStore/store/backend.js index 6731aea51c..0a5b9f0461 100644 --- a/packages/builder/src/builderStore/store/backend.js +++ b/packages/builder/src/builderStore/store/backend.js @@ -135,6 +135,9 @@ export const getBackendUiStore = () => { } query.datasourceId = datasourceId const response = await api.post(`/api/queries`, query) + if (response.status !== 200) { + throw new Error("Failed saving query.") + } const json = await response.json() store.update(state => { const currentIdx = state.queries.findIndex( diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index d0d1f69793..19987764e5 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -15,6 +15,7 @@ import { FrontendTypes } from "constants" import analytics from "analytics" import { findComponentType, findComponentParent } from "../storeUtils" import { uuid } from "../uuid" +import { removeBindings } from "../dataBinding" const INITIAL_FRONTEND_STATE = { apps: [], @@ -408,9 +409,16 @@ export const getFrontendStore = () => { return state } + // defines if this is a copy or a cut + const cut = state.componentToPaste.isCut + + // immediately need to remove bindings, currently these aren't valid when pasted + if (!cut) { + state.componentToPaste = removeBindings(state.componentToPaste) + } + // Clone the component to paste // Retain the same ID if cutting as things may be referencing this component - const cut = state.componentToPaste.isCut delete state.componentToPaste.isCut let componentToPaste = cloneDeep(state.componentToPaste) if (cut) { diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditUser.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditUser.svelte index 5753b7d5bb..6467b8e953 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditUser.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditUser.svelte @@ -34,11 +34,26 @@ } const saveRow = async () => { + errors = [] + + // Do some basic front end validation first + if (!row.email) { + errors = [...errors, { message: "Email is required" }] + } + if (!row.password) { + errors = [...errors, { message: "Password is required" }] + } + if (!row.roleId) { + errors = [...errors, { message: "Role is required" }] + } + if (errors.length) { + return false + } + const rowResponse = await backendApi.saveRow( { ...row, tableId: table._id }, table._id ) - if (rowResponse.errors) { if (Array.isArray(rowResponse.errors)) { errors = rowResponse.errors.map(error => ({ message: error })) @@ -48,6 +63,9 @@ .flat() } return false + } else if (rowResponse.status === 400 && rowResponse.message) { + errors = [{ message: rowResponse.message }] + return false } notifier.success("User saved successfully.") diff --git a/packages/builder/src/components/integration/QueryViewer.svelte b/packages/builder/src/components/integration/QueryViewer.svelte index 8ab7167636..44537afff3 100644 --- a/packages/builder/src/components/integration/QueryViewer.svelte +++ b/packages/builder/src/components/integration/QueryViewer.svelte @@ -204,7 +204,9 @@ {#if data} {#if tab === 'JSON'} -
+              
+                
                 {#if !data[0]}
                   
                   Please run your query to fetch some data.
diff --git a/packages/server/src/api/controllers/deploy/quota.js b/packages/server/src/api/controllers/deploy/quota.js
index 4e4c869670..5e8880c7a9 100644
--- a/packages/server/src/api/controllers/deploy/quota.js
+++ b/packages/server/src/api/controllers/deploy/quota.js
@@ -1,5 +1,10 @@
 const PouchDB = require("../../../db")
-const { DocumentTypes, SEPARATOR, UNICODE_MAX } = require("../../../db/utils")
+const {
+  DocumentTypes,
+  SEPARATOR,
+  UNICODE_MAX,
+  ViewNames,
+} = require("../../../db/utils")
 
 exports.getAppQuota = async function(appId) {
   const db = new PouchDB(appId)
@@ -19,9 +24,16 @@ exports.getAppQuota = async function(appId) {
 
   const designDoc = await db.get("_design/database")
 
+  let views = 0
+  for (let viewName of Object.keys(designDoc.views)) {
+    if (Object.values(ViewNames).indexOf(viewName) === -1) {
+      views++
+    }
+  }
+
   return {
     rows: existingRows,
     users: existingUsers,
-    views: Object.keys(designDoc.views).length,
+    views: views,
   }
 }
diff --git a/packages/server/src/api/controllers/row.js b/packages/server/src/api/controllers/row.js
index 43d831dc60..54f4e4508e 100644
--- a/packages/server/src/api/controllers/row.js
+++ b/packages/server/src/api/controllers/row.js
@@ -111,7 +111,6 @@ exports.patch = async function(ctx) {
   }
   row._rev = response.rev
   row.type = "row"
-
   ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:update`, appId, row, table)
   ctx.body = row
   ctx.status = 200
diff --git a/packages/server/src/api/routes/query.js b/packages/server/src/api/routes/query.js
index 4266cf6bdc..55223e4e9e 100644
--- a/packages/server/src/api/routes/query.js
+++ b/packages/server/src/api/routes/query.js
@@ -27,7 +27,7 @@ function generateQueryValidation() {
     readable: Joi.boolean(),
     parameters: Joi.array().items(Joi.object({
       name: Joi.string(),
-      default: Joi.string()
+      default: Joi.string().allow(""),
     })),
     queryVerb: Joi.string().allow().required(),
     schema: Joi.object({}).required().unknown(true)
diff --git a/packages/server/src/automations/triggers.js b/packages/server/src/automations/triggers.js
index e4c91e5610..73ce9edeed 100644
--- a/packages/server/src/automations/triggers.js
+++ b/packages/server/src/automations/triggers.js
@@ -13,11 +13,11 @@ const FAKE_DATETIME = "1970-01-01T00:00:00.000Z"
 
 const BUILTIN_DEFINITIONS = {
   ROW_SAVED: {
-    name: "Row Saved",
+    name: "Row Created",
     event: "row:save",
     icon: "ri-save-line",
     tagline: "Row is added to {{inputs.enriched.table.name}}",
-    description: "Fired when a row is saved to your database",
+    description: "Fired when a row is added to your database",
     stepId: "ROW_SAVED",
     inputs: {},
     schema: {
@@ -36,7 +36,47 @@ const BUILTIN_DEFINITIONS = {
           row: {
             type: "object",
             customType: "row",
-            description: "The new row that was saved",
+            description: "The new row that was created",
+          },
+          id: {
+            type: "string",
+            description: "Row ID - can be used for updating",
+          },
+          revision: {
+            type: "string",
+            description: "Revision of row",
+          },
+        },
+        required: ["row", "id"],
+      },
+    },
+    type: "TRIGGER",
+  },
+  ROW_UPDATED: {
+    name: "Row Updated",
+    event: "row:update",
+    icon: "ri-refresh-line",
+    tagline: "Row is updated in {{inputs.enriched.table.name}}",
+    description: "Fired when a row is updated in your database",
+    stepId: "ROW_UPDATED",
+    inputs: {},
+    schema: {
+      inputs: {
+        properties: {
+          tableId: {
+            type: "string",
+            customType: "table",
+            title: "Table",
+          },
+        },
+        required: ["tableId"],
+      },
+      outputs: {
+        properties: {
+          row: {
+            type: "object",
+            customType: "row",
+            description: "The row that was updated",
           },
           id: {
             type: "string",
@@ -79,7 +119,7 @@ const BUILTIN_DEFINITIONS = {
             description: "The row that was deleted",
           },
         },
-        required: ["row", "id"],
+        required: ["row"],
       },
     },
     type: "TRIGGER",
@@ -191,6 +231,13 @@ emitter.on("row:save", async function(event) {
   await queueRelevantRowAutomations(event, "row:save")
 })
 
+emitter.on("row:update", async function(event) {
+  if (!event || !event.row || !event.row.tableId) {
+    return
+  }
+  await queueRelevantRowAutomations(event, "row:update")
+})
+
 emitter.on("row:delete", async function(event) {
   if (!event || !event.row || !event.row.tableId) {
     return
diff --git a/packages/standard-components/src/Card.svelte b/packages/standard-components/src/Card.svelte
index 33be4185f7..85f30c5a09 100644
--- a/packages/standard-components/src/Card.svelte
+++ b/packages/standard-components/src/Card.svelte
@@ -40,7 +40,7 @@
     
+      href={linkUrl || '/'}>
       {linkText}
     
   
@@ -71,6 +71,7 @@
     font-size: 1.25rem;
     font-weight: 700;
     margin: 0;
+    white-space: pre-wrap;
   }
 
   .text {
@@ -78,6 +79,7 @@
     margin: 0;
     font-weight: 400;
     line-height: 1.5rem;
+    white-space: pre-wrap;
   }
 
   a {
@@ -85,6 +87,7 @@
     text-decoration: none;
     color: var(--linkColor);
     font-weight: 600;
+    white-space: pre-wrap;
   }
 
   a:hover {
diff --git a/packages/standard-components/src/CardHorizontal.svelte b/packages/standard-components/src/CardHorizontal.svelte
index 5d7596e296..523d796cf5 100644
--- a/packages/standard-components/src/CardHorizontal.svelte
+++ b/packages/standard-components/src/CardHorizontal.svelte
@@ -40,7 +40,7 @@
       

{subtext}

{linkText} + href={linkUrl || '/'}>{linkText} @@ -71,6 +71,7 @@ font-size: 1rem; font-weight: 700; margin: 0; + white-space: pre-wrap; } .text { @@ -78,6 +79,7 @@ margin: 0.5rem 0 0 0; font-weight: 400; line-height: 1.25rem; + white-space: pre-wrap; } footer { @@ -91,6 +93,7 @@ margin: 0; font-weight: 400; color: #757575; + white-space: pre-wrap; } a { @@ -99,7 +102,7 @@ color: var(--linkColor); font-weight: 600; font-size: 0.85rem; - margin: 0; + white-space: pre-wrap; } a:hover { diff --git a/packages/standard-components/src/CardStat.svelte b/packages/standard-components/src/CardStat.svelte index e5e40ad862..86291db915 100644 --- a/packages/standard-components/src/CardStat.svelte +++ b/packages/standard-components/src/CardStat.svelte @@ -20,7 +20,6 @@ .container { min-width: 260px; width: max-content; - max-height: 170px; border: 1px solid var(--grey-3); border-radius: 0.3rem; color: var(--blue); @@ -31,6 +30,7 @@ color: #9e9e9e; font-weight: 500; margin: 1rem 1.5rem 0.5rem 1.5rem; + white-space: pre-wrap; } .value { @@ -38,6 +38,7 @@ font-weight: 500; margin: 0 1.5rem 1.5rem 1.5rem; color: inherit; + white-space: pre-wrap; } .label { @@ -45,5 +46,6 @@ font-weight: 400; color: #9e9e9e; margin: 1rem 1.5rem; + white-space: pre-wrap; } diff --git a/packages/standard-components/src/Heading.svelte b/packages/standard-components/src/Heading.svelte index 66775dc666..f06ac86afd 100644 --- a/packages/standard-components/src/Heading.svelte +++ b/packages/standard-components/src/Heading.svelte @@ -4,21 +4,31 @@ const { styleable } = getContext("sdk") const component = getContext("component") - export let className = "" export let type export let text = "" {#if type === 'h1'} -

{text}

+

{text}

{:else if type === 'h2'} -

{text}

+

{text}

{:else if type === 'h3'} -

{text}

+

{text}

{:else if type === 'h4'} -

{text}

+

{text}

{:else if type === 'h5'} -
{text}
+
{text}
{:else if type === 'h6'} -
{text}
+
{text}
{/if} + + diff --git a/packages/standard-components/src/StackedList.svelte b/packages/standard-components/src/StackedList.svelte index b06ce53ebe..3cf70b2ec1 100644 --- a/packages/standard-components/src/StackedList.svelte +++ b/packages/standard-components/src/StackedList.svelte @@ -50,6 +50,7 @@ .subheading { opacity: 0.6; + white-space: pre-wrap; } .content { @@ -60,6 +61,7 @@ .heading { font-weight: 600; + white-space: pre-wrap; } .image-block { diff --git a/packages/standard-components/src/Text.svelte b/packages/standard-components/src/Text.svelte index 887bdec29d..ab62352694 100644 --- a/packages/standard-components/src/Text.svelte +++ b/packages/standard-components/src/Text.svelte @@ -5,38 +5,13 @@ const component = getContext("component") export let text = "" - export let className = "" - export let type = "" - - const isTag = tag => type === tag -{#if isTag('none')} - {text} -{:else if isTag('bold')} - {text} -{:else if isTag('strong')} - {text} -{:else if isTag('italic')} - {text} -{:else if isTag('emphasis')} - {text} -{:else if isTag('mark')} - {text} -{:else if isTag('small')} - {text} -{:else if isTag('del')} - {text} -{:else if isTag('ins')} - {text} -{:else if isTag('sub')} - {text} -{:else if isTag('sup')} - {text} -{:else}{text}{/if} +

{text}