+ {selectedImage.name}
+
{:else}
{selectedImage.name}
{/if}
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
+
{:else}
-
+
{attachment.extension}
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`)
}
}
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)
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]
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)
+ })
+ })
})