Merge pull request #7270 from Budibase/cheeks-fixes

Various fixes
This commit is contained in:
Andrew Kingston 2022-08-19 15:07:26 +01:00 committed by GitHub
commit 078d327cd0
8 changed files with 203 additions and 78 deletions

View File

@ -59,6 +59,13 @@
// If time only set date component to 2000-01-01 // If time only set date component to 2000-01-01
if (timeOnly) { if (timeOnly) {
// Classic flackpickr causing issues.
// 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) {
newValue = new Date(dates[0].getTime() + 60 * 60 * 1000).toISOString()
}
newValue = `2000-01-01T${newValue.split("T")[1]}` newValue = `2000-01-01T${newValue.split("T")[1]}`
} }

View File

@ -133,7 +133,13 @@
<div class="title"> <div class="title">
<div class="filename"> <div class="filename">
{#if selectedUrl} {#if selectedUrl}
<Link href={selectedUrl}>{selectedImage.name}</Link> <Link
target="_blank"
download={selectedImage.name}
href={selectedUrl}
>
{selectedImage.name}
</Link>
{:else} {:else}
{selectedImage.name} {selectedImage.name}
{/if} {/if}

View File

@ -8,12 +8,14 @@
export let secondary = false export let secondary = false
export let overBackground = false export let overBackground = false
export let target export let target
export let download
</script> </script>
<a <a
on:click on:click
{href} {href}
{target} {target}
{download}
class:spectrum-Link--primary={primary} class:spectrum-Link--primary={primary}
class:spectrum-Link--secondary={secondary} class:spectrum-Link--secondary={secondary}
class:spectrum-Link--overBackground={overBackground} class:spectrum-Link--overBackground={overBackground}

View File

@ -15,14 +15,24 @@
{#each attachments as attachment} {#each attachments as attachment}
{#if isImage(attachment.extension)} {#if isImage(attachment.extension)}
<Link quiet target="_blank" href={attachment.url}> <Link
quiet
target="_blank"
download={attachment.name}
href={attachment.url}
>
<div class="center" title={attachment.name}> <div class="center" title={attachment.name}>
<img src={attachment.url} alt={attachment.extension} /> <img src={attachment.url} alt={attachment.extension} />
</div> </div>
</Link> </Link>
{:else} {:else}
<div class="file" title={attachment.name}> <div class="file" title={attachment.name}>
<Link quiet target="_blank" href={attachment.url}> <Link
quiet
target="_blank"
download={attachment.name}
href={attachment.url}
>
{attachment.extension} {attachment.extension}
</Link> </Link>
</div> </div>

View File

@ -186,7 +186,7 @@
$goto("./navigation") $goto("./navigation")
} }
} else if (type === "request-add-component") { } else if (type === "request-add-component") {
$goto(`./components/${$selectedComponent?._id}/new`) toggleAddComponent()
} else if (type === "highlight-setting") { } else if (type === "highlight-setting") {
store.actions.settings.highlight(data.setting) store.actions.settings.highlight(data.setting)
@ -230,9 +230,8 @@
if (isAddingComponent) { if (isAddingComponent) {
$goto(`../${$selectedScreen._id}/components/${$selectedComponent?._id}`) $goto(`../${$selectedScreen._id}/components/${$selectedComponent?._id}`)
} else { } else {
$goto( const id = $selectedComponent?._id || $selectedScreen?.props?._id
`../${$selectedScreen._id}/components/${$selectedComponent?._id}/new` $goto(`../${$selectedScreen._id}/components/${id}/new`)
)
} }
} }

View File

@ -72,7 +72,7 @@ const cleanupQuery = query => {
continue continue
} }
for (let [key, value] of Object.entries(query[filterField])) { for (let [key, value] of Object.entries(query[filterField])) {
if (!value || value === "") { if (value == null || value === "") {
delete query[filterField][key] delete query[filterField][key]
} }
} }
@ -174,7 +174,7 @@ export const runLuceneQuery = (docs, query) => {
return docs return docs
} }
// make query consistent first // Make query consistent first
query = cleanupQuery(query) query = cleanupQuery(query)
// Iterates over a set of filters and evaluates a fail function against a doc // 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 // Process a range match
const rangeMatch = match("range", (docValue, testValue) => { 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) // Process an equal match (fails if the value is different)

View File

@ -375,6 +375,7 @@ exports.exportRows = async ctx => {
const table = await db.get(ctx.params.tableId) const table = await db.get(ctx.params.tableId)
const rowIds = ctx.request.body.rows const rowIds = ctx.request.body.rows
let format = ctx.query.format let format = ctx.query.format
const { columns } = ctx.request.body
let response = ( let response = (
await db.allDocs({ await db.allDocs({
include_docs: true, include_docs: true,
@ -382,7 +383,20 @@ exports.exportRows = async ctx => {
}) })
).rows.map(row => row.doc) ).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]) let headers = Object.keys(rows[0])
const exporter = exporters[format] const exporter = exporters[format]

View File

@ -3,7 +3,12 @@ const setup = require("./utilities")
const { basicRow } = setup.structures const { basicRow } = setup.structures
const { doInAppContext } = require("@budibase/backend-core/context") const { doInAppContext } = require("@budibase/backend-core/context")
const { doInTenant } = require("@budibase/backend-core/tenancy") 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", () => { describe("/rows", () => {
let request = setup.getRequest() let request = setup.getRequest()
@ -23,23 +28,30 @@ describe("/rows", () => {
await request await request
.get(`/api/${table._id}/rows/${id}`) .get(`/api/${table._id}/rows/${id}`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect('Content-Type', /json/) .expect("Content-Type", /json/)
.expect(status) .expect(status)
const getRowUsage = async () => { 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 () => { 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() const usage = await getRowUsage()
expect(usage).toBe(expected) expect(usage).toBe(expected)
} }
const assertQueryUsage = async (expected) => { const assertQueryUsage = async expected => {
const usage = await getQueryUsage() const usage = await getQueryUsage()
expect(usage).toBe(expected) expect(usage).toBe(expected)
} }
@ -76,10 +88,12 @@ describe("/rows", () => {
name: "Updated Name", name: "Updated Name",
}) })
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect('Content-Type', /json/) .expect("Content-Type", /json/)
.expect(200) .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.name).toEqual("Updated Name")
// await assertRowUsage(rowUsage) // await assertRowUsage(rowUsage)
// await assertQueryUsage(queryUsage + 1) // await assertQueryUsage(queryUsage + 1)
@ -92,7 +106,7 @@ describe("/rows", () => {
const res = await request const res = await request
.get(`/api/${table._id}/rows/${existing._id}`) .get(`/api/${table._id}/rows/${existing._id}`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect('Content-Type', /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
expect(res.body).toEqual({ expect(res.body).toEqual({
@ -110,7 +124,7 @@ describe("/rows", () => {
const newRow = { const newRow = {
tableId: table._id, tableId: table._id,
name: "Second Contact", name: "Second Contact",
status: "new" status: "new",
} }
await config.createRow() await config.createRow()
await config.createRow(newRow) await config.createRow(newRow)
@ -119,7 +133,7 @@ describe("/rows", () => {
const res = await request const res = await request
.get(`/api/${table._id}/rows`) .get(`/api/${table._id}/rows`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect('Content-Type', /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
expect(res.body.length).toBe(2) expect(res.body.length).toBe(2)
@ -135,17 +149,36 @@ describe("/rows", () => {
await request await request
.get(`/api/${table._id}/rows/not-a-valid-id`) .get(`/api/${table._id}/rows/not-a-valid-id`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect('Content-Type', /json/) .expect("Content-Type", /json/)
.expect(404) .expect(404)
await assertQueryUsage(queryUsage) // no change await assertQueryUsage(queryUsage) // no change
}) })
it("row values are coerced", async () => { it("row values are coerced", async () => {
const str = {type:"string", constraints: { type: "string", presence: false }} const str = {
const attachment = {type:"attachment", constraints: { type: "array", presence: false }} type: "string",
const bool = {type:"boolean", constraints: { type: "boolean", presence: false }} constraints: { type: "string", presence: false },
const number = {type:"number", constraints: { type: "number", presence: false }} }
const datetime = {type:"datetime", constraints: { type: "string", presence: false, datetime: {earliest:"", latest: ""} }} 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({ table = await config.createTable({
name: "TestTable2", name: "TestTable2",
@ -218,7 +251,9 @@ describe("/rows", () => {
expect(saved.datetimeEmptyString).toBe(null) expect(saved.datetimeEmptyString).toBe(null)
expect(saved.datetimeNull).toBe(null) expect(saved.datetimeNull).toBe(null)
expect(saved.datetimeUndefined).toBe(undefined) 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.datetimeDate).toBe(row.datetimeDate.toISOString())
expect(saved.boolNull).toBe(null) expect(saved.boolNull).toBe(null)
expect(saved.boolEmpty).toBe(null) expect(saved.boolEmpty).toBe(null)
@ -247,10 +282,12 @@ describe("/rows", () => {
name: "Updated Name", name: "Updated Name",
}) })
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect('Content-Type', /json/) .expect("Content-Type", /json/)
.expect(200) .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.name).toEqual("Updated Name")
expect(res.body.description).toEqual(existing.description) expect(res.body.description).toEqual(existing.description)
@ -292,12 +329,10 @@ describe("/rows", () => {
const res = await request const res = await request
.delete(`/api/${table._id}/rows`) .delete(`/api/${table._id}/rows`)
.send({ .send({
rows: [ rows: [createdRow],
createdRow
]
}) })
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect('Content-Type', /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
expect(res.body[0]._id).toEqual(createdRow._id) expect(res.body[0]._id).toEqual(createdRow._id)
await assertRowUsage(rowUsage - 1) await assertRowUsage(rowUsage - 1)
@ -314,7 +349,7 @@ describe("/rows", () => {
.post(`/api/${table._id}/rows/validate`) .post(`/api/${table._id}/rows/validate`)
.send({ name: "ivan" }) .send({ name: "ivan" })
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect('Content-Type', /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
expect(res.body.valid).toBe(true) expect(res.body.valid).toBe(true)
@ -331,7 +366,7 @@ describe("/rows", () => {
.post(`/api/${table._id}/rows/validate`) .post(`/api/${table._id}/rows/validate`)
.send({ name: 1 }) .send({ name: 1 })
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect('Content-Type', /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
expect(res.body.valid).toBe(false) expect(res.body.valid).toBe(false)
@ -351,13 +386,10 @@ describe("/rows", () => {
const res = await request const res = await request
.delete(`/api/${table._id}/rows`) .delete(`/api/${table._id}/rows`)
.send({ .send({
rows: [ rows: [row1, row2],
row1,
row2,
]
}) })
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect('Content-Type', /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
expect(res.body.length).toEqual(2) expect(res.body.length).toEqual(2)
@ -376,7 +408,7 @@ describe("/rows", () => {
const res = await request const res = await request
.get(`/api/views/${table._id}`) .get(`/api/views/${table._id}`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect('Content-Type', /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
expect(res.body.length).toEqual(1) expect(res.body.length).toEqual(1)
expect(res.body[0]._id).toEqual(row._id) expect(res.body[0]._id).toEqual(row._id)
@ -406,7 +438,7 @@ describe("/rows", () => {
const res = await request const res = await request
.get(`/api/views/${view.name}`) .get(`/api/views/${view.name}`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect('Content-Type', /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
expect(res.body.length).toEqual(1) expect(res.body.length).toEqual(1)
expect(res.body[0]._id).toEqual(row._id) expect(res.body[0]._id).toEqual(row._id)
@ -418,12 +450,14 @@ describe("/rows", () => {
describe("fetchEnrichedRows", () => { describe("fetchEnrichedRows", () => {
it("should allow enriching some linked rows", async () => { it("should allow enriching some linked rows", async () => {
const { table, firstRow, secondRow } = await doInTenant(setup.structures.TENANT_ID, async () => { const { table, firstRow, secondRow } = await doInTenant(
setup.structures.TENANT_ID,
async () => {
const table = await config.createLinkedTable() const table = await config.createLinkedTable()
const firstRow = await config.createRow({ const firstRow = await config.createRow({
name: "Test Contact", name: "Test Contact",
description: "original description", description: "original description",
tableId: table._id tableId: table._id,
}) })
const secondRow = await config.createRow({ const secondRow = await config.createRow({
name: "Test 2", name: "Test 2",
@ -432,7 +466,8 @@ describe("/rows", () => {
tableId: table._id, tableId: table._id,
}) })
return { table, firstRow, secondRow } return { table, firstRow, secondRow }
}) }
)
const rowUsage = await getRowUsage() const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage() const queryUsage = await getQueryUsage()
@ -440,7 +475,7 @@ describe("/rows", () => {
const resBasic = await request const resBasic = await request
.get(`/api/${table._id}/rows/${secondRow._id}`) .get(`/api/${table._id}/rows/${secondRow._id}`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect('Content-Type', /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
expect(resBasic.body.link[0]._id).toBe(firstRow._id) expect(resBasic.body.link[0]._id).toBe(firstRow._id)
expect(resBasic.body.link[0].primaryDisplay).toBe("Test Contact") expect(resBasic.body.link[0].primaryDisplay).toBe("Test Contact")
@ -449,7 +484,7 @@ describe("/rows", () => {
const resEnriched = await request const resEnriched = await request
.get(`/api/${table._id}/${secondRow._id}/enrich`) .get(`/api/${table._id}/${secondRow._id}/enrich`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect('Content-Type', /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
expect(resEnriched.body.link.length).toBe(1) expect(resEnriched.body.link.length).toBe(1)
expect(resEnriched.body.link[0]._id).toBe(firstRow._id) expect(resEnriched.body.link[0]._id).toBe(firstRow._id)
@ -466,9 +501,11 @@ describe("/rows", () => {
const row = await config.createRow({ const row = await config.createRow({
name: "test", name: "test",
description: "test", description: "test",
attachment: [{ attachment: [
{
key: `${config.getAppId()}/attachments/test/thing.csv`, key: `${config.getAppId()}/attachments/test/thing.csv`,
}], },
],
tableId: table._id, tableId: table._id,
}) })
// the environment needs configured for this // 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)
})
})
}) })