Merge branch 'develop' of github.com:Budibase/budibase into bugs/various-bugs

This commit is contained in:
mike12345567 2021-02-23 16:31:12 +00:00
commit 4ce179c213
16 changed files with 165 additions and 56 deletions

View File

@ -17,23 +17,33 @@ context("Create a automation", () => {
cy.get("[data-cy=new-automation]").click() cy.get("[data-cy=new-automation]").click()
cy.get(".modal").within(() => { cy.get(".modal").within(() => {
cy.get("input").type("Add Row") cy.get("input").type("Add Row")
cy.get(".buttons").contains("Create").click() cy.get(".buttons")
.contains("Create")
.click()
}) })
// Add trigger // Add trigger
cy.contains("Trigger").click() cy.contains("Trigger").click()
cy.contains("Row Saved").click() cy.contains("Row Created").click()
cy.get(".setup").within(() => { cy.get(".setup").within(() => {
cy.get("select").first().select("dog") cy.get("select")
.first()
.select("dog")
}) })
// Create action // Create action
cy.contains("Action").click() cy.contains("Action").click()
cy.contains("Create Row").click() cy.contains("Create Row").click()
cy.get(".setup").within(() => { cy.get(".setup").within(() => {
cy.get("select").first().select("dog") cy.get("select")
cy.get("input").first().type("goodboy") .first()
cy.get("input").eq(1).type("11") .select("dog")
cy.get("input")
.first()
.type("goodboy")
cy.get("input")
.eq(1)
.type("11")
}) })
// Save // Save

View File

@ -7,6 +7,7 @@ import { TableNames } from "../constants"
// Regex to match all instances of template strings // Regex to match all instances of template strings
const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
const CAPTURE_HBS_TEMPLATE = /{{[\S\s]*?}}/g
/** /**
* Gets all bindable data context fields and instance fields. * Gets all bindable data context fields and instance fields.
@ -282,6 +283,20 @@ const buildFormSchema = component => {
return schema 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. * utility function for the readableToRuntimeBinding and runtimeToReadableBinding.
*/ */

View File

@ -135,6 +135,9 @@ export const getBackendUiStore = () => {
} }
query.datasourceId = datasourceId query.datasourceId = datasourceId
const response = await api.post(`/api/queries`, query) const response = await api.post(`/api/queries`, query)
if (response.status !== 200) {
throw new Error("Failed saving query.")
}
const json = await response.json() const json = await response.json()
store.update(state => { store.update(state => {
const currentIdx = state.queries.findIndex( const currentIdx = state.queries.findIndex(

View File

@ -15,6 +15,7 @@ import { FrontendTypes } from "constants"
import analytics from "analytics" import analytics from "analytics"
import { findComponentType, findComponentParent } from "../storeUtils" import { findComponentType, findComponentParent } from "../storeUtils"
import { uuid } from "../uuid" import { uuid } from "../uuid"
import { removeBindings } from "../dataBinding"
const INITIAL_FRONTEND_STATE = { const INITIAL_FRONTEND_STATE = {
apps: [], apps: [],
@ -408,9 +409,16 @@ export const getFrontendStore = () => {
return state 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 // Clone the component to paste
// Retain the same ID if cutting as things may be referencing this component // Retain the same ID if cutting as things may be referencing this component
const cut = state.componentToPaste.isCut
delete state.componentToPaste.isCut delete state.componentToPaste.isCut
let componentToPaste = cloneDeep(state.componentToPaste) let componentToPaste = cloneDeep(state.componentToPaste)
if (cut) { if (cut) {

View File

@ -34,11 +34,26 @@
} }
const saveRow = async () => { 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( const rowResponse = await backendApi.saveRow(
{ ...row, tableId: table._id }, { ...row, tableId: table._id },
table._id table._id
) )
if (rowResponse.errors) { if (rowResponse.errors) {
if (Array.isArray(rowResponse.errors)) { if (Array.isArray(rowResponse.errors)) {
errors = rowResponse.errors.map(error => ({ message: error })) errors = rowResponse.errors.map(error => ({ message: error }))
@ -48,6 +63,9 @@
.flat() .flat()
} }
return false return false
} else if (rowResponse.status === 400 && rowResponse.message) {
errors = [{ message: rowResponse.message }]
return false
} }
notifier.success("User saved successfully.") notifier.success("User saved successfully.")

View File

@ -204,7 +204,9 @@
{#if data} {#if data}
<Switcher headings={PREVIEW_HEADINGS} bind:value={tab}> <Switcher headings={PREVIEW_HEADINGS} bind:value={tab}>
{#if tab === 'JSON'} {#if tab === 'JSON'}
<pre class="preview"> <pre
class="preview">
<!-- prettier-ignore -->
{#if !data[0]} {#if !data[0]}
Please run your query to fetch some data. Please run your query to fetch some data.

View File

@ -1,5 +1,10 @@
const PouchDB = require("../../../db") 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) { exports.getAppQuota = async function(appId) {
const db = new PouchDB(appId) const db = new PouchDB(appId)
@ -19,9 +24,16 @@ exports.getAppQuota = async function(appId) {
const designDoc = await db.get("_design/database") 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 { return {
rows: existingRows, rows: existingRows,
users: existingUsers, users: existingUsers,
views: Object.keys(designDoc.views).length, views: views,
} }
} }

View File

@ -111,7 +111,6 @@ exports.patch = async function(ctx) {
} }
row._rev = response.rev row._rev = response.rev
row.type = "row" row.type = "row"
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:update`, appId, row, table) ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:update`, appId, row, table)
ctx.body = row ctx.body = row
ctx.status = 200 ctx.status = 200

View File

@ -27,7 +27,7 @@ function generateQueryValidation() {
readable: Joi.boolean(), readable: Joi.boolean(),
parameters: Joi.array().items(Joi.object({ parameters: Joi.array().items(Joi.object({
name: Joi.string(), name: Joi.string(),
default: Joi.string() default: Joi.string().allow(""),
})), })),
queryVerb: Joi.string().allow().required(), queryVerb: Joi.string().allow().required(),
schema: Joi.object({}).required().unknown(true) schema: Joi.object({}).required().unknown(true)

View File

@ -13,11 +13,11 @@ const FAKE_DATETIME = "1970-01-01T00:00:00.000Z"
const BUILTIN_DEFINITIONS = { const BUILTIN_DEFINITIONS = {
ROW_SAVED: { ROW_SAVED: {
name: "Row Saved", name: "Row Created",
event: "row:save", event: "row:save",
icon: "ri-save-line", icon: "ri-save-line",
tagline: "Row is added to {{inputs.enriched.table.name}}", 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", stepId: "ROW_SAVED",
inputs: {}, inputs: {},
schema: { schema: {
@ -36,7 +36,47 @@ const BUILTIN_DEFINITIONS = {
row: { row: {
type: "object", type: "object",
customType: "row", 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: { id: {
type: "string", type: "string",
@ -79,7 +119,7 @@ const BUILTIN_DEFINITIONS = {
description: "The row that was deleted", description: "The row that was deleted",
}, },
}, },
required: ["row", "id"], required: ["row"],
}, },
}, },
type: "TRIGGER", type: "TRIGGER",
@ -191,6 +231,13 @@ emitter.on("row:save", async function(event) {
await queueRelevantRowAutomations(event, "row:save") 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) { emitter.on("row:delete", async function(event) {
if (!event || !event.row || !event.row.tableId) { if (!event || !event.row || !event.row.tableId) {
return return

View File

@ -40,7 +40,7 @@
<a <a
use:linkable use:linkable
style="--linkColor: {linkColor}; --linkHoverColor: {linkHoverColor}" style="--linkColor: {linkColor}; --linkHoverColor: {linkHoverColor}"
href={linkUrl}> href={linkUrl || '/'}>
{linkText} {linkText}
</a> </a>
</div> </div>
@ -71,6 +71,7 @@
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 700; font-weight: 700;
margin: 0; margin: 0;
white-space: pre-wrap;
} }
.text { .text {
@ -78,6 +79,7 @@
margin: 0; margin: 0;
font-weight: 400; font-weight: 400;
line-height: 1.5rem; line-height: 1.5rem;
white-space: pre-wrap;
} }
a { a {
@ -85,6 +87,7 @@
text-decoration: none; text-decoration: none;
color: var(--linkColor); color: var(--linkColor);
font-weight: 600; font-weight: 600;
white-space: pre-wrap;
} }
a:hover { a:hover {

View File

@ -40,7 +40,7 @@
<p class="subtext">{subtext}</p> <p class="subtext">{subtext}</p>
<a <a
style="--linkColor: {linkColor}; --linkHoverColor: {linkHoverColor}" style="--linkColor: {linkColor}; --linkHoverColor: {linkHoverColor}"
href={linkUrl}>{linkText}</a> href={linkUrl || '/'}>{linkText}</a>
</footer> </footer>
</div> </div>
</div> </div>
@ -71,6 +71,7 @@
font-size: 1rem; font-size: 1rem;
font-weight: 700; font-weight: 700;
margin: 0; margin: 0;
white-space: pre-wrap;
} }
.text { .text {
@ -78,6 +79,7 @@
margin: 0.5rem 0 0 0; margin: 0.5rem 0 0 0;
font-weight: 400; font-weight: 400;
line-height: 1.25rem; line-height: 1.25rem;
white-space: pre-wrap;
} }
footer { footer {
@ -91,6 +93,7 @@
margin: 0; margin: 0;
font-weight: 400; font-weight: 400;
color: #757575; color: #757575;
white-space: pre-wrap;
} }
a { a {
@ -99,7 +102,7 @@
color: var(--linkColor); color: var(--linkColor);
font-weight: 600; font-weight: 600;
font-size: 0.85rem; font-size: 0.85rem;
margin: 0; white-space: pre-wrap;
} }
a:hover { a:hover {

View File

@ -20,7 +20,6 @@
.container { .container {
min-width: 260px; min-width: 260px;
width: max-content; width: max-content;
max-height: 170px;
border: 1px solid var(--grey-3); border: 1px solid var(--grey-3);
border-radius: 0.3rem; border-radius: 0.3rem;
color: var(--blue); color: var(--blue);
@ -31,6 +30,7 @@
color: #9e9e9e; color: #9e9e9e;
font-weight: 500; font-weight: 500;
margin: 1rem 1.5rem 0.5rem 1.5rem; margin: 1rem 1.5rem 0.5rem 1.5rem;
white-space: pre-wrap;
} }
.value { .value {
@ -38,6 +38,7 @@
font-weight: 500; font-weight: 500;
margin: 0 1.5rem 1.5rem 1.5rem; margin: 0 1.5rem 1.5rem 1.5rem;
color: inherit; color: inherit;
white-space: pre-wrap;
} }
.label { .label {
@ -45,5 +46,6 @@
font-weight: 400; font-weight: 400;
color: #9e9e9e; color: #9e9e9e;
margin: 1rem 1.5rem; margin: 1rem 1.5rem;
white-space: pre-wrap;
} }
</style> </style>

View File

@ -4,21 +4,31 @@
const { styleable } = getContext("sdk") const { styleable } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
export let className = ""
export let type export let type
export let text = "" export let text = ""
</script> </script>
{#if type === 'h1'} {#if type === 'h1'}
<h1 class={className} use:styleable={$component.styles}>{text}</h1> <h1 use:styleable={$component.styles}>{text}</h1>
{:else if type === 'h2'} {:else if type === 'h2'}
<h2 class={className} use:styleable={$component.styles}>{text}</h2> <h2 use:styleable={$component.styles}>{text}</h2>
{:else if type === 'h3'} {:else if type === 'h3'}
<h3 class={className} use:styleable={$component.styles}>{text}</h3> <h3 use:styleable={$component.styles}>{text}</h3>
{:else if type === 'h4'} {:else if type === 'h4'}
<h4 class={className} use:styleable={$component.styles}>{text}</h4> <h4 use:styleable={$component.styles}>{text}</h4>
{:else if type === 'h5'} {:else if type === 'h5'}
<h5 class={className} use:styleable={$component.styles}>{text}</h5> <h5 use:styleable={$component.styles}>{text}</h5>
{:else if type === 'h6'} {:else if type === 'h6'}
<h6 class={className} use:styleable={$component.styles}>{text}</h6> <h6 use:styleable={$component.styles}>{text}</h6>
{/if} {/if}
<style>
h1,
h2,
h3,
h4,
h5,
h6 {
white-space: pre-wrap;
}
</style>

View File

@ -50,6 +50,7 @@
.subheading { .subheading {
opacity: 0.6; opacity: 0.6;
white-space: pre-wrap;
} }
.content { .content {
@ -60,6 +61,7 @@
.heading { .heading {
font-weight: 600; font-weight: 600;
white-space: pre-wrap;
} }
.image-block { .image-block {

View File

@ -5,38 +5,13 @@
const component = getContext("component") const component = getContext("component")
export let text = "" export let text = ""
export let className = ""
export let type = ""
const isTag = tag => type === tag
</script> </script>
{#if isTag('none')} <p use:styleable={$component.styles}>{text}</p>
<span use:styleable={$component.styles}>{text}</span>
{:else if isTag('bold')}
<b class={className} use:styleable={$component.styles}>{text}</b>
{:else if isTag('strong')}
<strong class={className} use:styleable={$component.styles}>{text}</strong>
{:else if isTag('italic')}
<i class={className} use:styleable={$component.styles}>{text}</i>
{:else if isTag('emphasis')}
<em class={className} use:styleable={$component.styles}>{text}</em>
{:else if isTag('mark')}
<mark class={className} use:styleable={$component.styles}>{text}</mark>
{:else if isTag('small')}
<small class={className} use:styleable={$component.styles}>{text}</small>
{:else if isTag('del')}
<del class={className} use:styleable={$component.styles}>{text}</del>
{:else if isTag('ins')}
<ins class={className} use:styleable={$component.styles}>{text}</ins>
{:else if isTag('sub')}
<sub class={className} use:styleable={$component.styles}>{text}</sub>
{:else if isTag('sup')}
<sup class={className} use:styleable={$component.styles}>{text}</sup>
{:else}<span use:styleable={$component.styles}>{text}</span>{/if}
<style> <style>
span { p {
display: inline-block; display: inline-block;
white-space: pre-wrap;
} }
</style> </style>