From 62e01a299e1f05227bc6fee4dfc79f9c0f675ad5 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Sat, 13 Aug 2022 15:22:54 +0100 Subject: [PATCH 1/9] Fix add component not working on first click when no component is selected --- .../design/[screenId]/_components/AppPreview.svelte | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 3c99c90d49..814930d636 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 @@ -186,7 +186,7 @@ $goto("./navigation") } } else if (type === "request-add-component") { - $goto(`./components/${$selectedComponent?._id}/new`) + toggleAddComponent() } else if (type === "highlight-setting") { store.actions.settings.highlight(data.setting) @@ -230,9 +230,8 @@ if (isAddingComponent) { $goto(`../${$selectedScreen._id}/components/${$selectedComponent?._id}`) } else { - $goto( - `../${$selectedScreen._id}/components/${$selectedComponent?._id}/new` - ) + const id = $selectedComponent?._id || $selectedScreen?.props?._id + $goto(`../${$selectedScreen._id}/components/${id}/new`) } } From 3a7c92e20248d6837f259a87a8e5a3b8ea60ac48 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Sat, 13 Aug 2022 15:38:21 +0100 Subject: [PATCH 2/9] Support filtering data exports to only certain columns with internal tables --- .../server/src/api/controllers/row/internal.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/server/src/api/controllers/row/internal.js b/packages/server/src/api/controllers/row/internal.js index 086e1d9ce4..0c92959db8 100644 --- a/packages/server/src/api/controllers/row/internal.js +++ b/packages/server/src/api/controllers/row/internal.js @@ -375,6 +375,7 @@ exports.exportRows = async ctx => { const table = await db.get(ctx.params.tableId) const rowIds = ctx.request.body.rows let format = ctx.query.format + const { columns } = ctx.request.body let response = ( await db.allDocs({ include_docs: true, @@ -382,7 +383,20 @@ exports.exportRows = async ctx => { }) ).rows.map(row => row.doc) - let rows = await outputProcessing(table, response) + let result = await outputProcessing(table, response) + let rows = [] + + // Filter data to only specified columns if required + if (columns && columns.length) { + for (let i = 0; i < result.length; i++) { + rows[i] = {} + for (let column of columns) { + rows[i][column] = result[i][column] + } + } + } else { + rows = result + } let headers = Object.keys(rows[0]) const exporter = exporters[format] From ee5085f57fd3e929be9982ca6606918bbb67a3a1 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Sat, 13 Aug 2022 16:25:01 +0100 Subject: [PATCH 3/9] Fix flatpickr offsetting date by one hour on initial selection of time-only fields --- packages/bbui/src/Form/Core/DatePicker.svelte | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/bbui/src/Form/Core/DatePicker.svelte b/packages/bbui/src/Form/Core/DatePicker.svelte index 39a7d9d626..c3eee7505f 100644 --- a/packages/bbui/src/Form/Core/DatePicker.svelte +++ b/packages/bbui/src/Form/Core/DatePicker.svelte @@ -59,6 +59,13 @@ // If time only set date component to 2000-01-01 if (timeOnly) { + // Classic flackpickr causing issues. + // When selecting a time first the first time for a "time only" field, + // the time is always offset by 1 hour for some reason (regardless of time + // zone) so we need to correct it. + if (!value && newValue) { + newValue = new Date(dates[0].getTime() + 60 * 60 * 1000).toISOString() + } newValue = `2000-01-01T${newValue.split("T")[1]}` } From 03e379bafef8d0eea80347ed0d9f4d26059f0cf3 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 15 Aug 2022 11:24:25 +0100 Subject: [PATCH 4/9] Fix typo --- packages/bbui/src/Form/Core/DatePicker.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bbui/src/Form/Core/DatePicker.svelte b/packages/bbui/src/Form/Core/DatePicker.svelte index c3eee7505f..c6230c5212 100644 --- a/packages/bbui/src/Form/Core/DatePicker.svelte +++ b/packages/bbui/src/Form/Core/DatePicker.svelte @@ -60,7 +60,7 @@ // If time only set date component to 2000-01-01 if (timeOnly) { // Classic flackpickr causing issues. - // When selecting a time first the first time for a "time only" field, + // When selecting a value for the first time for a "time only" field, // the time is always offset by 1 hour for some reason (regardless of time // zone) so we need to correct it. if (!value && newValue) { From 4045337bb7034862d7d91c51c5e3725caedefcf3 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 15 Aug 2022 11:37:04 +0100 Subject: [PATCH 5/9] Add download setting to links --- packages/bbui/src/Link/Link.svelte | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/bbui/src/Link/Link.svelte b/packages/bbui/src/Link/Link.svelte index f66554bd75..3bbfdd8282 100644 --- a/packages/bbui/src/Link/Link.svelte +++ b/packages/bbui/src/Link/Link.svelte @@ -8,12 +8,14 @@ export let secondary = false export let overBackground = false export let target + export let download Date: Mon, 15 Aug 2022 11:37:30 +0100 Subject: [PATCH 6/9] Use real file names when download files from dropzones --- packages/bbui/src/Form/Core/Dropzone.svelte | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/bbui/src/Form/Core/Dropzone.svelte b/packages/bbui/src/Form/Core/Dropzone.svelte index 36515acbc5..e98b2ad964 100644 --- a/packages/bbui/src/Form/Core/Dropzone.svelte +++ b/packages/bbui/src/Form/Core/Dropzone.svelte @@ -133,7 +133,13 @@
{#if selectedUrl} - {selectedImage.name} + + {selectedImage.name} + {:else} {selectedImage.name} {/if} From 35528ee17e563c5c84c90b95f2bf67a7a9b69d0f Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 15 Aug 2022 11:37:40 +0100 Subject: [PATCH 7/9] Use real file names when download files from tables --- packages/bbui/src/Table/AttachmentRenderer.svelte | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/bbui/src/Table/AttachmentRenderer.svelte b/packages/bbui/src/Table/AttachmentRenderer.svelte index 4dff22aef8..3017aac9b7 100644 --- a/packages/bbui/src/Table/AttachmentRenderer.svelte +++ b/packages/bbui/src/Table/AttachmentRenderer.svelte @@ -15,14 +15,24 @@ {#each attachments as attachment} {#if isImage(attachment.extension)} - +
{attachment.extension}
{:else}
- + {attachment.extension}
From abd732fa52537453100242530db7f200067e2fc3 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 19 Aug 2022 13:54:08 +0100 Subject: [PATCH 8/9] Add tests for exporting data --- .../server/src/api/routes/tests/row.spec.js | 216 ++++++++++++------ 1 file changed, 149 insertions(+), 67 deletions(-) diff --git a/packages/server/src/api/routes/tests/row.spec.js b/packages/server/src/api/routes/tests/row.spec.js index 86e47924d8..5cd282bb34 100644 --- a/packages/server/src/api/routes/tests/row.spec.js +++ b/packages/server/src/api/routes/tests/row.spec.js @@ -3,7 +3,12 @@ const setup = require("./utilities") const { basicRow } = setup.structures const { doInAppContext } = require("@budibase/backend-core/context") const { doInTenant } = require("@budibase/backend-core/tenancy") -const { quotas, QuotaUsageType, StaticQuotaName, MonthlyQuotaName } = require("@budibase/pro") +const { + quotas, + QuotaUsageType, + StaticQuotaName, + MonthlyQuotaName, +} = require("@budibase/pro") describe("/rows", () => { let request = setup.getRequest() @@ -23,23 +28,30 @@ describe("/rows", () => { await request .get(`/api/${table._id}/rows/${id}`) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(status) const getRowUsage = async () => { - return config.doInContext(null, () => quotas.getCurrentUsageValue(QuotaUsageType.STATIC, StaticQuotaName.ROWS)) + return config.doInContext(null, () => + quotas.getCurrentUsageValue(QuotaUsageType.STATIC, StaticQuotaName.ROWS) + ) } const getQueryUsage = async () => { - return config.doInContext(null, () => quotas.getCurrentUsageValue(QuotaUsageType.MONTHLY, MonthlyQuotaName.QUERIES)) + return config.doInContext(null, () => + quotas.getCurrentUsageValue( + QuotaUsageType.MONTHLY, + MonthlyQuotaName.QUERIES + ) + ) } - const assertRowUsage = async (expected) => { + const assertRowUsage = async expected => { const usage = await getRowUsage() expect(usage).toBe(expected) } - const assertQueryUsage = async (expected) => { + const assertQueryUsage = async expected => { const usage = await getQueryUsage() expect(usage).toBe(expected) } @@ -76,10 +88,12 @@ describe("/rows", () => { name: "Updated Name", }) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) - expect(res.res.statusMessage).toEqual(`${table.name} updated successfully.`) + expect(res.res.statusMessage).toEqual( + `${table.name} updated successfully.` + ) expect(res.body.name).toEqual("Updated Name") // await assertRowUsage(rowUsage) // await assertQueryUsage(queryUsage + 1) @@ -92,7 +106,7 @@ describe("/rows", () => { const res = await request .get(`/api/${table._id}/rows/${existing._id}`) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) expect(res.body).toEqual({ @@ -110,7 +124,7 @@ describe("/rows", () => { const newRow = { tableId: table._id, name: "Second Contact", - status: "new" + status: "new", } await config.createRow() await config.createRow(newRow) @@ -119,7 +133,7 @@ describe("/rows", () => { const res = await request .get(`/api/${table._id}/rows`) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) expect(res.body.length).toBe(2) @@ -135,17 +149,36 @@ describe("/rows", () => { await request .get(`/api/${table._id}/rows/not-a-valid-id`) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(404) await assertQueryUsage(queryUsage) // no change }) it("row 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: ""} }} + 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: "" }, + }, + } table = await config.createTable({ name: "TestTable2", @@ -171,9 +204,9 @@ describe("/rows", () => { boolUndefined: bool, boolString: bool, boolBool: bool, - attachmentNull : attachment, - attachmentUndefined : attachment, - attachmentEmpty : attachment, + attachmentNull: attachment, + attachmentUndefined: attachment, + attachmentEmpty: attachment, }, }) @@ -198,9 +231,9 @@ describe("/rows", () => { boolString: "true", boolBool: true, tableId: table._id, - attachmentNull : null, - attachmentUndefined : undefined, - attachmentEmpty : "", + attachmentNull: null, + attachmentUndefined: undefined, + attachmentEmpty: "", } const id = (await config.createRow(row))._id @@ -218,7 +251,9 @@ describe("/rows", () => { expect(saved.datetimeEmptyString).toBe(null) expect(saved.datetimeNull).toBe(null) expect(saved.datetimeUndefined).toBe(undefined) - expect(saved.datetimeString).toBe(new Date(row.datetimeString).toISOString()) + expect(saved.datetimeString).toBe( + new Date(row.datetimeString).toISOString() + ) expect(saved.datetimeDate).toBe(row.datetimeDate.toISOString()) expect(saved.boolNull).toBe(null) expect(saved.boolEmpty).toBe(null) @@ -247,10 +282,12 @@ describe("/rows", () => { name: "Updated Name", }) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) - - expect(res.res.statusMessage).toEqual(`${table.name} updated successfully.`) + + expect(res.res.statusMessage).toEqual( + `${table.name} updated successfully.` + ) expect(res.body.name).toEqual("Updated Name") expect(res.body.description).toEqual(existing.description) @@ -292,16 +329,14 @@ describe("/rows", () => { const res = await request .delete(`/api/${table._id}/rows`) .send({ - rows: [ - createdRow - ] + rows: [createdRow], }) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) expect(res.body[0]._id).toEqual(createdRow._id) - await assertRowUsage(rowUsage -1) - await assertQueryUsage(queryUsage +1) + await assertRowUsage(rowUsage - 1) + await assertQueryUsage(queryUsage + 1) }) }) @@ -314,9 +349,9 @@ describe("/rows", () => { .post(`/api/${table._id}/rows/validate`) .send({ name: "ivan" }) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) - + expect(res.body.valid).toBe(true) expect(Object.keys(res.body.errors)).toEqual([]) await assertRowUsage(rowUsage) @@ -331,9 +366,9 @@ describe("/rows", () => { .post(`/api/${table._id}/rows/validate`) .send({ name: 1 }) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) - + expect(res.body.valid).toBe(false) expect(Object.keys(res.body.errors)).toEqual(["name"]) await assertRowUsage(rowUsage) @@ -351,19 +386,16 @@ describe("/rows", () => { const res = await request .delete(`/api/${table._id}/rows`) .send({ - rows: [ - row1, - row2, - ] + rows: [row1, row2], }) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) expect(res.body.length).toEqual(2) await loadRow(row1._id, 404) await assertRowUsage(rowUsage - 2) - await assertQueryUsage(queryUsage +1) + await assertQueryUsage(queryUsage + 1) }) }) @@ -376,12 +408,12 @@ describe("/rows", () => { const res = await request .get(`/api/views/${table._id}`) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) expect(res.body.length).toEqual(1) expect(res.body[0]._id).toEqual(row._id) await assertRowUsage(rowUsage) - await assertQueryUsage(queryUsage +1) + await assertQueryUsage(queryUsage + 1) }) it("should throw an error if view doesn't exist", async () => { @@ -406,7 +438,7 @@ describe("/rows", () => { const res = await request .get(`/api/views/${view.name}`) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) expect(res.body.length).toEqual(1) expect(res.body[0]._id).toEqual(row._id) @@ -418,21 +450,24 @@ describe("/rows", () => { describe("fetchEnrichedRows", () => { it("should allow enriching some linked rows", async () => { - const { table, firstRow, secondRow } = await doInTenant(setup.structures.TENANT_ID, async () => { - const table = await config.createLinkedTable() - const firstRow = await config.createRow({ - name: "Test Contact", - description: "original description", - tableId: table._id - }) - const secondRow = await config.createRow({ - name: "Test 2", - description: "og desc", - link: [{_id: firstRow._id}], - tableId: table._id, - }) - return { table, firstRow, secondRow } - }) + const { table, firstRow, secondRow } = await doInTenant( + setup.structures.TENANT_ID, + async () => { + const table = await config.createLinkedTable() + const firstRow = await config.createRow({ + name: "Test Contact", + description: "original description", + tableId: table._id, + }) + const secondRow = await config.createRow({ + name: "Test 2", + description: "og desc", + link: [{ _id: firstRow._id }], + tableId: table._id, + }) + return { table, firstRow, secondRow } + } + ) const rowUsage = await getRowUsage() const queryUsage = await getQueryUsage() @@ -440,7 +475,7 @@ describe("/rows", () => { const resBasic = await request .get(`/api/${table._id}/rows/${secondRow._id}`) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) expect(resBasic.body.link[0]._id).toBe(firstRow._id) expect(resBasic.body.link[0].primaryDisplay).toBe("Test Contact") @@ -449,14 +484,14 @@ describe("/rows", () => { const resEnriched = await request .get(`/api/${table._id}/${secondRow._id}/enrich`) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) expect(resEnriched.body.link.length).toBe(1) expect(resEnriched.body.link[0]._id).toBe(firstRow._id) expect(resEnriched.body.link[0].name).toBe("Test Contact") expect(resEnriched.body.link[0].description).toBe("original description") await assertRowUsage(rowUsage) - await assertQueryUsage(queryUsage +2) + await assertQueryUsage(queryUsage + 2) }) }) @@ -466,9 +501,11 @@ describe("/rows", () => { const row = await config.createRow({ name: "test", description: "test", - attachment: [{ - key: `${config.getAppId()}/attachments/test/thing.csv`, - }], + attachment: [ + { + key: `${config.getAppId()}/attachments/test/thing.csv`, + }, + ], tableId: table._id, }) // the environment needs configured for this @@ -482,4 +519,49 @@ describe("/rows", () => { }) }) }) + + describe("exportData", () => { + it("should allow exporting all columns", async () => { + const existing = await config.createRow() + const res = await request + .post(`/api/${table._id}/rows/exportRows?format=json`) + .set(config.defaultHeaders()) + .send({ + rows: [existing._id], + }) + .expect("Content-Type", /json/) + .expect(200) + const results = JSON.parse(res.text) + expect(results.length).toEqual(1) + const row = results[0] + + // Ensure all original columns were exported + expect(Object.keys(row).length).toBeGreaterThanOrEqual( + Object.keys(existing).length + ) + Object.keys(existing).forEach(key => { + expect(row[key]).toEqual(existing[key]) + }) + }) + + it("should allow exporting only certain columns", async () => { + const existing = await config.createRow() + const res = await request + .post(`/api/${table._id}/rows/exportRows?format=json`) + .set(config.defaultHeaders()) + .send({ + rows: [existing._id], + columns: ["_id"], + }) + .expect("Content-Type", /json/) + .expect(200) + const results = JSON.parse(res.text) + expect(results.length).toEqual(1) + const row = results[0] + + // Ensure only the _id column was exported + expect(Object.keys(row).length).toEqual(1) + expect(row._id).toEqual(existing._id) + }) + }) }) From 5c5e4bcccbb6363da08edd36ad9bc3fcbbce341b Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 19 Aug 2022 14:11:58 +0100 Subject: [PATCH 9/9] Fix issue with falsey lucene values being ignored --- packages/frontend-core/src/utils/lucene.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/frontend-core/src/utils/lucene.js b/packages/frontend-core/src/utils/lucene.js index b9f673668e..243b7ba646 100644 --- a/packages/frontend-core/src/utils/lucene.js +++ b/packages/frontend-core/src/utils/lucene.js @@ -72,7 +72,7 @@ const cleanupQuery = query => { continue } for (let [key, value] of Object.entries(query[filterField])) { - if (!value || value === "") { + if (value == null || value === "") { delete query[filterField][key] } } @@ -174,7 +174,7 @@ export const runLuceneQuery = (docs, query) => { return docs } - // make query consistent first + // Make query consistent first query = cleanupQuery(query) // Iterates over a set of filters and evaluates a fail function against a doc @@ -206,7 +206,12 @@ export const runLuceneQuery = (docs, query) => { // Process a range match const rangeMatch = match("range", (docValue, testValue) => { - return !docValue || docValue < testValue.low || docValue > testValue.high + return ( + docValue == null || + docValue === "" || + docValue < testValue.low || + docValue > testValue.high + ) }) // Process an equal match (fails if the value is different)