Merge pull request #665 from mjashanks/fixes

Bugfixes
This commit is contained in:
Michael Shanks 2020-10-06 21:50:56 +01:00 committed by GitHub
commit e9ecafb3e1
23 changed files with 7384 additions and 47 deletions

View File

@ -47,8 +47,9 @@ context('Create a View', () => {
it('creates a stats calculation view based on age', () => { it('creates a stats calculation view based on age', () => {
cy.contains("Calculate").click() cy.contains("Calculate").click()
cy.get(".menu-container").find("select").first().select("Statistics") // we may reinstate this - have commented this dropdown for now as there is only one option
cy.get(".menu-container").find("select").eq(1).select("age") //cy.get(".menu-container").find("select").first().select("Statistics")
cy.get(".menu-container").find("select").eq(0).select("age")
cy.contains("Save").click() cy.contains("Save").click()
cy.get("thead th").should(($headers) => { cy.get("thead th").should(($headers) => {
expect($headers).to.have.length(7) expect($headers).to.have.length(7)

View File

@ -10,7 +10,6 @@
import AttachmentList from "./AttachmentList.svelte" import AttachmentList from "./AttachmentList.svelte"
import TablePagination from "./TablePagination.svelte" import TablePagination from "./TablePagination.svelte"
import Spinner from "components/common/Spinner.svelte" import Spinner from "components/common/Spinner.svelte"
import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
import RowPopover from "./popovers/Row.svelte" import RowPopover from "./popovers/Row.svelte"
import ColumnPopover from "./popovers/Column.svelte" import ColumnPopover from "./popovers/Column.svelte"
import ViewPopover from "./popovers/View.svelte" import ViewPopover from "./popovers/View.svelte"

View File

@ -8,7 +8,6 @@
import ActionButton from "components/common/ActionButton.svelte" import ActionButton from "components/common/ActionButton.svelte"
import AttachmentList from "./AttachmentList.svelte" import AttachmentList from "./AttachmentList.svelte"
import TablePagination from "./TablePagination.svelte" import TablePagination from "./TablePagination.svelte"
import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
import RowPopover from "./popovers/Row.svelte" import RowPopover from "./popovers/Row.svelte"
import ColumnPopover from "./popovers/Column.svelte" import ColumnPopover from "./popovers/Column.svelte"
import ViewPopover from "./popovers/View.svelte" import ViewPopover from "./popovers/View.svelte"

View File

@ -9,7 +9,6 @@
import ActionButton from "components/common/ActionButton.svelte" import ActionButton from "components/common/ActionButton.svelte"
import LinkedRecord from "./LinkedRecord.svelte" import LinkedRecord from "./LinkedRecord.svelte"
import TablePagination from "./TablePagination.svelte" import TablePagination from "./TablePagination.svelte"
import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
import RowPopover from "./popovers/Row.svelte" import RowPopover from "./popovers/Row.svelte"
import ColumnPopover from "./popovers/Column.svelte" import ColumnPopover from "./popovers/Column.svelte"
import ViewPopover from "./popovers/View.svelte" import ViewPopover from "./popovers/View.svelte"

View File

@ -24,11 +24,7 @@
} }
let originalName = field.name let originalName = field.name
$: required = field && field.constraints && field.constraints.presence
$: required =
field.constraints &&
field.constraints.presence &&
!field.constraints.presence.allowEmpty
async function saveColumn() { async function saveColumn() {
backendUiStore.update(state => { backendUiStore.update(state => {
@ -50,6 +46,14 @@
field.type = type field.type = type
field.constraints = constraints field.constraints = constraints
} }
const getPresence = required => (required ? { allowEmpty: false } : false)
const requiredChanged = ev => {
const req = ev.target.checked
field.constraints.presence = req ? { allowEmpty: false } : false
required = req
}
</script> </script>
<div class="actions"> <div class="actions">
@ -68,10 +72,7 @@
<div class="info"> <div class="info">
<div class="field"> <div class="field">
<label>Required</label> <label>Required</label>
<input <input type="checkbox" checked={required} on:change={requiredChanged} />
type="checkbox"
bind:checked={required}
on:change={() => (field.constraints.presence.allowEmpty = required)} />
</div> </div>
{#if field.type === 'string' && field.constraints} {#if field.type === 'string' && field.constraints}

View File

@ -9,7 +9,6 @@
} from "@budibase/bbui" } from "@budibase/bbui"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import CreateEditRecord from "../modals/CreateEditRecord.svelte"
import analytics from "analytics" import analytics from "analytics"
const CALCULATIONS = [ const CALCULATIONS = [
@ -34,6 +33,7 @@
) )
function saveView() { function saveView() {
if (!view.calculation) view.calculation = "stats"
backendUiStore.actions.views.save(view) backendUiStore.actions.views.save(view)
notifier.success(`View ${view.name} saved.`) notifier.success(`View ${view.name} saved.`)
analytics.captureEvent("Added View Calculate", { field: view.field }) analytics.captureEvent("Added View Calculate", { field: view.field })
@ -50,14 +50,15 @@
<Popover bind:this={dropdown} {anchor} align="left"> <Popover bind:this={dropdown} {anchor} align="left">
<h5>Calculate</h5> <h5>Calculate</h5>
<div class="input-group-row"> <div class="input-group-row">
<p>The</p> <!-- <p>The</p>
<Select secondary thin bind:value={view.calculation}> <Select secondary thin bind:value={view.calculation}>
<option value="">Choose an option</option> <option value="">Choose an option</option>
{#each CALCULATIONS as calculation} {#each CALCULATIONS as calculation}
<option value={calculation.key}>{calculation.name}</option> <option value={calculation.key}>{calculation.name}</option>
{/each} {/each}
</Select> </Select>
<p>of</p> <p>of</p> -->
<p>The statistics of</p>
<Select secondary thin bind:value={view.field}> <Select secondary thin bind:value={view.field}>
<option value="">Choose an option</option> <option value="">Choose an option</option>
{#each fields as field} {#each fields as field}
@ -86,7 +87,7 @@
.input-group-row { .input-group-row {
display: grid; display: grid;
grid-template-columns: 50px 1fr 20px 1fr; grid-template-columns: auto 1fr 20px 1fr;
gap: var(--spacing-s); gap: var(--spacing-s);
margin-bottom: var(--spacing-l); margin-bottom: var(--spacing-l);
align-items: center; align-items: center;

View File

@ -6,10 +6,10 @@
Icon, Icon,
Input, Input,
Select, Select,
DatePicker,
} from "@budibase/bbui" } from "@budibase/bbui"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import CreateEditRecord from "../modals/CreateEditRecord.svelte"
import analytics from "analytics" import analytics from "analytics"
const CONDITIONS = [ const CONDITIONS = [
@ -81,11 +81,38 @@
function isMultipleChoice(field) { function isMultipleChoice(field) {
return ( return (
viewModel.schema[field].constraints && (viewModel.schema[field].constraints &&
viewModel.schema[field].constraints.inclusion && viewModel.schema[field].constraints.inclusion &&
viewModel.schema[field].constraints.inclusion.length viewModel.schema[field].constraints.inclusion.length) ||
viewModel.schema[field].type === "boolean"
) )
} }
function fieldOptions(field) {
return viewModel.schema[field].type === "string"
? viewModel.schema[field].constraints.inclusion
: [true, false]
}
function isDate(field) {
return viewModel.schema[field].type === "datetime"
}
function isNumber(field) {
return viewModel.schema[field].type === "number"
}
const fieldChanged = filter => ev => {
// reset if type changed
if (
filter.key &&
ev.target.value &&
viewModel.schema[filter.key].type !==
viewModel.schema[ev.target.value].type
) {
filter.value = ""
}
}
</script> </script>
<div bind:this={anchor}> <div bind:this={anchor}>
@ -112,7 +139,11 @@
{/each} {/each}
</Select> </Select>
{/if} {/if}
<Select secondary thin bind:value={filter.key}> <Select
secondary
thin
bind:value={filter.key}
on:change={fieldChanged(filter)}>
<option value="">Choose an option</option> <option value="">Choose an option</option>
{#each fields as field} {#each fields as field}
<option value={field}>{field}</option> <option value={field}>{field}</option>
@ -126,10 +157,21 @@
</Select> </Select>
{#if filter.key && isMultipleChoice(filter.key)} {#if filter.key && isMultipleChoice(filter.key)}
<Select secondary thin bind:value={filter.value}> <Select secondary thin bind:value={filter.value}>
{#each viewModel.schema[filter.key].constraints.inclusion as option} <option value="">Choose an option</option>
<option value={option}>{option}</option> {#each fieldOptions(filter.key) as option}
<option value={option}>{option.toString()}</option>
{/each} {/each}
</Select> </Select>
{:else if filter.key && isDate(filter.key)}
<DatePicker
bind:value={filter.value}
placeholder={filter.key || fields[0]} />
{:else if filter.key && isNumber(filter.key)}
<Input
thin
bind:value={filter.value}
placeholder={filter.key || fields[0]}
type="number" />
{:else} {:else}
<Input <Input
thin thin

View File

@ -9,7 +9,6 @@
} from "@budibase/bbui" } from "@budibase/bbui"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import CreateEditRecord from "../modals/CreateEditRecord.svelte"
const CALCULATIONS = [ const CALCULATIONS = [
{ {

View File

@ -10,7 +10,6 @@
import { goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import CreateEditRecord from "../modals/CreateEditRecord.svelte"
import analytics from "analytics" import analytics from "analytics"
let anchor let anchor

View File

@ -33,9 +33,9 @@
}) })
notifier.success(`Table ${name} created successfully.`) notifier.success(`Table ${name} created successfully.`)
$goto(`./model/${model._id}`) $goto(`./model/${model._id}`)
analytics.captureEvent("Table Created", { name })
name = "" name = ""
dropdown.hide() dropdown.hide()
analytics.captureEvent("Table Created", { name })
loading = false loading = false
} }

View File

@ -147,7 +147,7 @@
}) })
const appJson = await appResp.json() const appJson = await appResp.json()
analytics.captureEvent("App Created", { analytics.captureEvent("App Created", {
name, name: $createAppStore.values.applicationName,
appId: appJson._id, appId: appJson._id,
template, template,
}) })

View File

@ -11,7 +11,7 @@
on:input={() => (blurred.api = true)} on:input={() => (blurred.api = true)}
label="API Key" label="API Key"
name="apiKey" name="apiKey"
placeholder="Enter your API Key" placeholder="Use command-V to paste your API Key"
type="password" type="password"
error={blurred.api && validationErrors.apiKey} /> error={blurred.api && validationErrors.apiKey} />
<a target="_blank" href="https://portal.budi.live/">Get API Key</a> <a target="_blank" href="https://portal.budi.live/">Get API Key</a>

View File

@ -19,7 +19,7 @@
label="Password" label="Password"
name="password" name="password"
placeholder="Password" placeholder="Password"
type="pasword" type="password"
error={blurred.password && validationErrors.password} /> error={blurred.password && validationErrors.password} />
<Select secondary name="accessLevelId"> <Select secondary name="accessLevelId">
<option value="ADMIN">Admin</option> <option value="ADMIN">Admin</option>

View File

@ -6,7 +6,7 @@ export const FIELDS = {
constraints: { constraints: {
type: "string", type: "string",
length: {}, length: {},
presence: { allowEmpty: true }, presence: false,
}, },
}, },
NUMBER: { NUMBER: {
@ -15,7 +15,7 @@ export const FIELDS = {
type: "number", type: "number",
constraints: { constraints: {
type: "number", type: "number",
presence: { allowEmpty: true }, presence: false,
numericality: { greaterThanOrEqualTo: "", lessThanOrEqualTo: "" }, numericality: { greaterThanOrEqualTo: "", lessThanOrEqualTo: "" },
}, },
}, },
@ -25,7 +25,7 @@ export const FIELDS = {
type: "boolean", type: "boolean",
constraints: { constraints: {
type: "boolean", type: "boolean",
presence: { allowEmpty: true }, presence: false,
}, },
}, },
// OPTIONS: { // OPTIONS: {
@ -44,7 +44,7 @@ export const FIELDS = {
constraints: { constraints: {
type: "string", type: "string",
length: {}, length: {},
presence: { allowEmpty: true }, presence: false,
datetime: { datetime: {
latest: "", latest: "",
earliest: "", earliest: "",
@ -57,7 +57,7 @@ export const FIELDS = {
type: "attachment", type: "attachment",
constraints: { constraints: {
type: "array", type: "array",
presence: { allowEmpty: true }, presence: false,
}, },
}, },
// LINKED_FIELDS: { // LINKED_FIELDS: {

View File

@ -5,7 +5,6 @@
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import ActionButton from "components/common/ActionButton.svelte" import ActionButton from "components/common/ActionButton.svelte"
import * as api from "components/database/DataTable/api" import * as api from "components/database/DataTable/api"
import { CreateEditRecordModal } from "components/database/DataTable/modals"
const { open, close } = getContext("simple-modal") const { open, close } = getContext("simple-modal")

View File

@ -5,7 +5,6 @@
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import ActionButton from "components/common/ActionButton.svelte" import ActionButton from "components/common/ActionButton.svelte"
import * as api from "components/database/DataTable/api" import * as api from "components/database/DataTable/api"
import { CreateEditRecordModal } from "components/database/DataTable/modals"
const { open, close } = getContext("simple-modal") const { open, close } = getContext("simple-modal")

View File

@ -1,6 +1,7 @@
const CouchDB = require("../../db") const CouchDB = require("../../db")
const validateJs = require("validate.js") const validateJs = require("validate.js")
const { getRecordParams, generateRecordID } = require("../../db/utils") const { getRecordParams, generateRecordID } = require("../../db/utils")
const { cloneDeep } = require("lodash")
const MODEL_VIEW_BEGINS_WITH = "all_model:" const MODEL_VIEW_BEGINS_WITH = "all_model:"
@ -31,10 +32,12 @@ validateJs.extend(validateJs.validators.datetime, {
exports.patch = async function(ctx) { exports.patch = async function(ctx) {
const db = new CouchDB(ctx.user.instanceId) const db = new CouchDB(ctx.user.instanceId)
const record = await db.get(ctx.params.id) let record = await db.get(ctx.params.id)
const model = await db.get(record.modelId) const model = await db.get(record.modelId)
const patchfields = ctx.request.body const patchfields = ctx.request.body
record = coerceRecordValues(record, model)
for (let key in patchfields) { for (let key in patchfields) {
if (!model.schema[key]) continue if (!model.schema[key]) continue
record[key] = patchfields[key] record[key] = patchfields[key]
@ -64,7 +67,7 @@ exports.patch = async function(ctx) {
exports.save = async function(ctx) { exports.save = async function(ctx) {
const db = new CouchDB(ctx.user.instanceId) const db = new CouchDB(ctx.user.instanceId)
const record = ctx.request.body let record = ctx.request.body
record.modelId = ctx.params.modelId record.modelId = ctx.params.modelId
if (!record._rev && !record._id) { if (!record._rev && !record._id) {
@ -73,6 +76,8 @@ exports.save = async function(ctx) {
const model = await db.get(record.modelId) const model = await db.get(record.modelId)
record = coerceRecordValues(record, model)
const validateResult = await validate({ const validateResult = await validate({
record, record,
model, model,
@ -231,3 +236,50 @@ async function validate({ instanceId, modelId, record, model }) {
} }
return { valid: Object.keys(errors).length === 0, errors } return { valid: Object.keys(errors).length === 0, errors }
} }
function coerceRecordValues(rec, model) {
const record = cloneDeep(rec)
for (let [key, value] of Object.entries(record)) {
const field = model.schema[key]
if (!field) continue
// eslint-disable-next-line no-prototype-builtins
if (TYPE_TRANSFORM_MAP[field.type].hasOwnProperty(value)) {
record[key] = TYPE_TRANSFORM_MAP[field.type][value]
} else if (TYPE_TRANSFORM_MAP[field.type].parse) {
record[key] = TYPE_TRANSFORM_MAP[field.type].parse(value)
}
}
return record
}
const TYPE_TRANSFORM_MAP = {
string: {
"": "",
[null]: "",
[undefined]: undefined,
},
number: {
"": null,
[null]: null,
[undefined]: undefined,
parse: n => parseFloat(n),
},
datetime: {
"": null,
[undefined]: undefined,
[null]: null,
},
attachment: {
"": [],
[null]: [],
[undefined]: undefined,
},
boolean: {
"": null,
[null]: null,
[undefined]: undefined,
true: true,
false: false,
},
}

View File

@ -61,8 +61,11 @@ function parseFilterExpression(filters) {
`doc["${filter.key}"].${TOKEN_MAP[filter.condition]}("${filter.value}")` `doc["${filter.key}"].${TOKEN_MAP[filter.condition]}("${filter.value}")`
) )
} else { } else {
const value =
typeof filter.value == "string" ? `"${filter.value}"` : filter.value
expression.push( expression.push(
`doc["${filter.key}"] ${TOKEN_MAP[filter.condition]} "${filter.value}"` `doc["${filter.key}"] ${TOKEN_MAP[filter.condition]} ${value}`
) )
} }
} }

View File

@ -46,13 +46,13 @@ exports.createModel = async (request, appId, instanceId, model) => {
key: "name", key: "name",
schema: { schema: {
name: { name: {
type: "text", type: "string",
constraints: { constraints: {
type: "string", type: "string",
}, },
}, },
description: { description: {
type: "text", type: "string",
constraints: { constraints: {
type: "string", type: "string",
}, },

View File

@ -180,7 +180,7 @@ describe("/models", () => {
key: "name", key: "name",
schema: { schema: {
name: { name: {
type: "text", type: "string",
constraints: { constraints: {
type: "string", type: "string",
}, },

View File

@ -38,7 +38,7 @@ describe("/records", () => {
const createRecord = async r => const createRecord = async r =>
await request await request
.post(`/api/${model._id}/records`) .post(`/api/${r ? r.modelId : record.modelId}/records`)
.send(r || record) .send(r || record)
.set(defaultHeaders(app._id, instance._id)) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
@ -152,6 +152,95 @@ describe("/records", () => {
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(404) .expect(404)
}) })
it("record values are coerced", async () => {
const str = {type:"string", constraints: { type: "string", presence: false }}
const attachment = {type:"attachment", constraints: { type: "array", presence: false }}
const bool = {type:"boolean", constraints: { type: "boolean", presence: false }}
const number = {type:"number", constraints: { type: "number", presence: false }}
const datetime = {type:"datetime", constraints: { type: "string", presence: false, datetime: {earliest:"", latest: ""} }}
model = await createModel(request, app._id, instance._id, {
name: "TestModel2",
type: "model",
key: "name",
schema: {
name: str,
stringUndefined: str,
stringNull: str,
stringString: str,
numberEmptyString: number,
numberNull: number,
numberUndefined: number,
numberString: number,
datetimeEmptyString: datetime,
datetimeNull: datetime,
datetimeUndefined: datetime,
datetimeString: datetime,
datetimeDate: datetime,
boolNull: bool,
boolEmpty: bool,
boolUndefined: bool,
boolString: bool,
boolBool: bool,
attachmentNull : attachment,
attachmentUndefined : attachment,
attachmentEmpty : attachment,
},
})
record = {
name: "Test Record",
stringUndefined: undefined,
stringNull: null,
stringString: "i am a string",
numberEmptyString: "",
numberNull: null,
numberUndefined: undefined,
numberString: "123",
numberNumber: 123,
datetimeEmptyString: "",
datetimeNull: null,
datetimeUndefined: undefined,
datetimeString: "1984-04-20T00:00:00.000Z",
datetimeDate: new Date("1984-04-20"),
boolNull: null,
boolEmpty: "",
boolUndefined: undefined,
boolString: "true",
boolBool: true,
modelId: model._id,
attachmentNull : null,
attachmentUndefined : undefined,
attachmentEmpty : "",
}
const id = (await createRecord(record)).body._id
const saved = (await loadRecord(id)).body
expect(saved.stringUndefined).toBe(undefined)
expect(saved.stringNull).toBe("")
expect(saved.stringString).toBe("i am a string")
expect(saved.numberEmptyString).toBe(null)
expect(saved.numberNull).toBe(null)
expect(saved.numberUndefined).toBe(undefined)
expect(saved.numberString).toBe(123)
expect(saved.numberNumber).toBe(123)
expect(saved.datetimeEmptyString).toBe(null)
expect(saved.datetimeNull).toBe(null)
expect(saved.datetimeUndefined).toBe(undefined)
expect(saved.datetimeString).toBe(new Date(record.datetimeString).toISOString())
expect(saved.datetimeDate).toBe(record.datetimeDate.toISOString())
expect(saved.boolNull).toBe(null)
expect(saved.boolEmpty).toBe(null)
expect(saved.boolUndefined).toBe(undefined)
expect(saved.boolString).toBe(true)
expect(saved.boolBool).toBe(true)
expect(saved.attachmentNull).toEqual([])
expect(saved.attachmentUndefined).toBe(undefined)
expect(saved.attachmentEmpty).toEqual([])
})
}) })
describe("patch", () => { describe("patch", () => {

View File

@ -69,13 +69,13 @@ describe("/views", () => {
filters: [], filters: [],
schema: { schema: {
name: { name: {
type: "text", type: "string",
constraints: { constraints: {
type: "string" type: "string"
}, },
}, },
description: { description: {
type: "text", type: "string",
constraints: { constraints: {
type: "string" type: "string"
}, },

File diff suppressed because it is too large Load Diff