From 16e58a38eaec8f483f6e08b0044bcd5dd13befa2 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 17 May 2024 10:51:40 +0200 Subject: [PATCH 01/25] Support pg time types --- packages/server/src/integrations/utils/utils.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/server/src/integrations/utils/utils.ts b/packages/server/src/integrations/utils/utils.ts index 892d8ae034..466f539ef3 100644 --- a/packages/server/src/integrations/utils/utils.ts +++ b/packages/server/src/integrations/utils/utils.ts @@ -71,7 +71,11 @@ const SQL_DATE_TYPE_MAP: Record = { } const SQL_DATE_ONLY_TYPES = ["date"] -const SQL_TIME_ONLY_TYPES = ["time"] +const SQL_TIME_ONLY_TYPES = [ + "time", + "time without time zone", + "time with time zone", +] const SQL_STRING_TYPE_MAP: Record = { varchar: FieldType.STRING, From a81626005c7b65c1321b462d861ca64dac977561 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 17 May 2024 15:55:27 +0200 Subject: [PATCH 02/25] Save timeonly on external db --- .../api/controllers/row/ExternalRequest.ts | 120 ++++++++++-------- 1 file changed, 64 insertions(+), 56 deletions(-) diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts index bd92413851..823330d601 100644 --- a/packages/server/src/api/controllers/row/ExternalRequest.ts +++ b/packages/server/src/api/controllers/row/ExternalRequest.ts @@ -1,3 +1,4 @@ +import dayjs from "dayjs" import { AutoFieldSubType, AutoReason, @@ -285,65 +286,72 @@ export class ExternalRequest { // parse floats/numbers if (field.type === FieldType.NUMBER && !isNaN(parseFloat(row[key]))) { newRow[key] = parseFloat(row[key]) - } - // if its not a link then just copy it over - if (field.type !== FieldType.LINK) { - newRow[key] = row[key] - continue - } - const { tableName: linkTableName } = breakExternalTableId(field?.tableId) - // table has to exist for many to many - if (!linkTableName || !this.tables[linkTableName]) { - continue - } - const linkTable = this.tables[linkTableName] - // @ts-ignore - const linkTablePrimary = linkTable.primary[0] - // one to many - if (isOneSide(field)) { - let id = row[key][0] - if (id) { - if (typeof row[key] === "string") { - id = decodeURIComponent(row[key]).match(/\[(.*?)\]/)?.[1] - } - newRow[field.foreignKey || linkTablePrimary] = breakRowIdField(id)[0] - } else { - // Removing from both new and row, as we don't know if it has already been processed - row[field.foreignKey || linkTablePrimary] = null - newRow[field.foreignKey || linkTablePrimary] = null + } else if (field.type === FieldType.LINK) { + const { tableName: linkTableName } = breakExternalTableId( + field?.tableId + ) + // table has to exist for many to many + if (!linkTableName || !this.tables[linkTableName]) { + continue } - } - // many to many - else if (isManyToMany(field)) { - // we're not inserting a doc, will be a bunch of update calls - const otherKey: string = field.throughFrom || linkTablePrimary - const thisKey: string = field.throughTo || tablePrimary - for (const relationship of row[key]) { - manyRelationships.push({ - tableId: field.through || field.tableId, - isUpdate: false, - key: otherKey, - [otherKey]: breakRowIdField(relationship)[0], - // leave the ID for enrichment later - [thisKey]: `{{ literal ${tablePrimary} }}`, - }) - } - } - // many to one - else { - const thisKey: string = "id" + const linkTable = this.tables[linkTableName] // @ts-ignore - const otherKey: string = field.fieldName - for (const relationship of row[key]) { - manyRelationships.push({ - tableId: field.tableId, - isUpdate: true, - key: otherKey, - [thisKey]: breakRowIdField(relationship)[0], - // leave the ID for enrichment later - [otherKey]: `{{ literal ${tablePrimary} }}`, - }) + const linkTablePrimary = linkTable.primary[0] + // one to many + if (isOneSide(field)) { + let id = row[key][0] + if (id) { + if (typeof row[key] === "string") { + id = decodeURIComponent(row[key]).match(/\[(.*?)\]/)?.[1] + } + newRow[field.foreignKey || linkTablePrimary] = + breakRowIdField(id)[0] + } else { + // Removing from both new and row, as we don't know if it has already been processed + row[field.foreignKey || linkTablePrimary] = null + newRow[field.foreignKey || linkTablePrimary] = null + } } + // many to many + else if (isManyToMany(field)) { + // we're not inserting a doc, will be a bunch of update calls + const otherKey: string = field.throughFrom || linkTablePrimary + const thisKey: string = field.throughTo || tablePrimary + for (const relationship of row[key]) { + manyRelationships.push({ + tableId: field.through || field.tableId, + isUpdate: false, + key: otherKey, + [otherKey]: breakRowIdField(relationship)[0], + // leave the ID for enrichment later + [thisKey]: `{{ literal ${tablePrimary} }}`, + }) + } + } + // many to one + else { + const thisKey: string = "id" + // @ts-ignore + const otherKey: string = field.fieldName + for (const relationship of row[key]) { + manyRelationships.push({ + tableId: field.tableId, + isUpdate: true, + key: otherKey, + [thisKey]: breakRowIdField(relationship)[0], + // leave the ID for enrichment later + [otherKey]: `{{ literal ${tablePrimary} }}`, + }) + } + } + } else if ( + field.type === FieldType.DATETIME && + field.timeOnly && + row[key] + ) { + newRow[key] = dayjs(row[key]).format("HH:mm") + } else { + newRow[key] = row[key] } } // we return the relationships that may need to be created in the through table From 76ac300cf004d947272cc95a9745e4a03c3021ba Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 17 May 2024 16:53:28 +0200 Subject: [PATCH 03/25] Save only if valid --- packages/server/src/api/controllers/row/ExternalRequest.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts index 823330d601..b30c97e289 100644 --- a/packages/server/src/api/controllers/row/ExternalRequest.ts +++ b/packages/server/src/api/controllers/row/ExternalRequest.ts @@ -347,7 +347,8 @@ export class ExternalRequest { } else if ( field.type === FieldType.DATETIME && field.timeOnly && - row[key] + row[key] && + dayjs(row[key]).isValid() ) { newRow[key] = dayjs(row[key]).format("HH:mm") } else { From a74f82a5359a01cd0ba02b67d0dcf8660c9ae79a Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 20 May 2024 13:06:45 +0200 Subject: [PATCH 04/25] Use native inputs --- .../Form/Core/DatePicker/NumberInput.svelte | 3 +- .../Form/Core/DatePicker/TimePicker.svelte | 37 +++++-------------- 2 files changed, 11 insertions(+), 29 deletions(-) diff --git a/packages/bbui/src/Form/Core/DatePicker/NumberInput.svelte b/packages/bbui/src/Form/Core/DatePicker/NumberInput.svelte index dc4886d28d..7e013341ac 100644 --- a/packages/bbui/src/Form/Core/DatePicker/NumberInput.svelte +++ b/packages/bbui/src/Form/Core/DatePicker/NumberInput.svelte @@ -4,13 +4,14 @@ export let max export let hideArrows = false export let width + export let type = "number" $: style = width ? `width:${width}px;` : "" - import { cleanInput } from "./utils" - import dayjs from "dayjs" import NumberInput from "./NumberInput.svelte" import { createEventDispatcher } from "svelte" @@ -8,39 +6,22 @@ const dispatch = createEventDispatcher() - $: displayValue = value || dayjs() + $: displayValue = value?.format("HH:mm") - const handleHourChange = e => { - dispatch("change", displayValue.hour(parseInt(e.target.value))) + const handleChange = e => { + const [hour, minute] = e.target.value.split(":").map(x => parseInt(x)) + dispatch("change", value.hour(hour).minute(minute)) } - - const handleMinuteChange = e => { - dispatch("change", displayValue.minute(parseInt(e.target.value))) - } - - const cleanHour = cleanInput({ max: 23, pad: 2, fallback: "00" }) - const cleanMinute = cleanInput({ max: 59, pad: 2, fallback: "00" })
- : -
From 5dc75582d9532042dc46db5acc76f08333a1bb1d Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 20 May 2024 13:09:28 +0200 Subject: [PATCH 05/25] Fix timezone issues --- packages/bbui/src/helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bbui/src/helpers.js b/packages/bbui/src/helpers.js index 90b447f3c1..66bc6551d8 100644 --- a/packages/bbui/src/helpers.js +++ b/packages/bbui/src/helpers.js @@ -166,7 +166,7 @@ export const stringifyDate = ( const offsetForTimezone = (enableTime && ignoreTimezones) || timeOnly if (offsetForTimezone) { // Ensure we use the correct offset for the date - const referenceDate = timeOnly ? new Date() : value.toDate() + const referenceDate = value.toDate() const offset = referenceDate.getTimezoneOffset() * 60000 return new Date(value.valueOf() - offset).toISOString().slice(0, -1) } From 9f759220118d603b7a16259ca81fba4d6d2ce933 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 20 May 2024 16:31:11 +0200 Subject: [PATCH 06/25] SQL time only column creation as time --- packages/server/src/integrations/base/sqlTable.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/server/src/integrations/base/sqlTable.ts b/packages/server/src/integrations/base/sqlTable.ts index a82a9fcea8..9871d57a3a 100644 --- a/packages/server/src/integrations/base/sqlTable.ts +++ b/packages/server/src/integrations/base/sqlTable.ts @@ -79,9 +79,13 @@ function generateSchema( schema.boolean(key) break case FieldType.DATETIME: - schema.datetime(key, { - useTz: !column.ignoreTimezones, - }) + if (!column.timeOnly) { + schema.datetime(key, { + useTz: !column.ignoreTimezones, + }) + } else { + schema.time(key) + } break case FieldType.ARRAY: case FieldType.BB_REFERENCE: From 57e73488b572a2834a01d717743acb046e51b7d0 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 20 May 2024 16:34:00 +0200 Subject: [PATCH 07/25] Add tests --- .../src/api/routes/tests/search.spec.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index 87d0aa72c7..9f7209129a 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -962,6 +962,36 @@ describe.each([ }) }) + describe("datetime - time only", () => { + const T_1000 = "10:00" + const T_1045 = "10:45" + const T_1200 = "12:00" + const T_1530 = "15:30" + const T_0000 = "00:00" + + beforeAll(async () => { + await createTable({ + time: { name: "time", type: FieldType.DATETIME, timeOnly: true }, + }) + + await createRows( + _.shuffle([T_1000, T_1045, T_1200, T_1530, T_0000]).map(time => ({ + time, + })) + ) + }) + + describe("equal", () => { + it("successfully finds a row", () => + expectQuery({ equal: { time: T_1000 } }).toContainExactly([ + { time: "10:00:00" }, + ])) + + it("fails to find nonexistent row", () => + expectQuery({ equal: { time: "10:01" } }).toFindNothing()) + }) + }) + describe.each([FieldType.ARRAY, FieldType.OPTIONS])("%s", () => { beforeAll(async () => { await createTable({ From 318dd5e6288d8fc70e5850c2805239112e51feab Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 20 May 2024 16:34:22 +0200 Subject: [PATCH 08/25] Fix time and mssql tests --- packages/server/src/integrations/base/sql.ts | 26 +++++++++++++++----- packages/server/src/utilities/schema.ts | 5 ++-- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/packages/server/src/integrations/base/sql.ts b/packages/server/src/integrations/base/sql.ts index 85db642e47..dd663dc918 100644 --- a/packages/server/src/integrations/base/sql.ts +++ b/packages/server/src/integrations/base/sql.ts @@ -122,11 +122,8 @@ function generateSelectStatement( const fieldNames = field.split(/\./g) const tableName = fieldNames[0] const columnName = fieldNames[1] - if ( - columnName && - schema?.[columnName] && - knex.client.config.client === SqlClient.POSTGRES - ) { + const columnSchema = schema?.[columnName] + if (columnSchema && knex.client.config.client === SqlClient.POSTGRES) { const externalType = schema[columnName].externalType if (externalType?.includes("money")) { return knex.raw( @@ -134,6 +131,13 @@ function generateSelectStatement( ) } } + if ( + knex.client.config.client === SqlClient.MS_SQL && + columnSchema.type === FieldType.DATETIME && + columnSchema.timeOnly + ) { + return knex.raw(`CONVERT(varchar, ${field}, 108) as "${field}"`) + } return `${field} as ${field}` }) } @@ -634,13 +638,23 @@ class SqlQueryBuilder extends SqlTableQueryBuilder { */ _query(json: QueryJson, opts: QueryOptions = {}): SqlQuery | SqlQuery[] { const sqlClient = this.getSqlClient() - const config: { client: string; useNullAsDefault?: boolean } = { + const config: Knex.Config = { client: sqlClient, } if (sqlClient === SqlClient.SQL_LITE) { config.useNullAsDefault = true } + + if (sqlClient === SqlClient.MS_SQL) { + // config.connection ??= {} + // config.connection.typeCast = (field: any, next: any): any => { + // if (field.type === "TIME") return field.string() + // return next() + // } + } + const client = knex(config) + let query: Knex.QueryBuilder const builder = new InternalBuilder(sqlClient) switch (this._operation(json)) { diff --git a/packages/server/src/utilities/schema.ts b/packages/server/src/utilities/schema.ts index 4f0feb3c93..a205bf8c11 100644 --- a/packages/server/src/utilities/schema.ts +++ b/packages/server/src/utilities/schema.ts @@ -129,11 +129,12 @@ export function parse(rows: Rows, schema: TableSchema): Rows { return } - const { type: columnType } = schema[columnName] + const columnSchema = schema[columnName] + const { type: columnType } = columnSchema if (columnType === FieldType.NUMBER) { // If provided must be a valid number parsedRow[columnName] = columnData ? Number(columnData) : columnData - } else if (columnType === FieldType.DATETIME) { + } else if (columnType === FieldType.DATETIME && !columnSchema.timeOnly) { // If provided must be a valid date parsedRow[columnName] = columnData ? new Date(columnData).toISOString() From 684e3607157c576a3ee0cdbb3bae03e43b14ddd5 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 20 May 2024 16:49:38 +0200 Subject: [PATCH 09/25] Add other tests --- .../src/api/routes/tests/search.spec.ts | 159 +++++++++++++++--- 1 file changed, 136 insertions(+), 23 deletions(-) diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index 9f7209129a..5e306739c9 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -962,35 +962,148 @@ describe.each([ }) }) - describe("datetime - time only", () => { - const T_1000 = "10:00" - const T_1045 = "10:45" - const T_1200 = "12:00" - const T_1530 = "15:30" - const T_0000 = "00:00" + !isInternal && + describe("datetime - time only", () => { + const T_1000 = "10:00" + const T_1045 = "10:45" + const T_1200 = "12:00" + const T_1530 = "15:30" + const T_0000 = "00:00" - beforeAll(async () => { - await createTable({ - time: { name: "time", type: FieldType.DATETIME, timeOnly: true }, + const UNEXISTING_TIME = "10:01" + + beforeAll(async () => { + await createTable({ + time: { name: "time", type: FieldType.DATETIME, timeOnly: true }, + }) + + await createRows( + _.shuffle([T_1000, T_1045, T_1200, T_1530, T_0000]).map(time => ({ + time, + })) + ) }) - await createRows( - _.shuffle([T_1000, T_1045, T_1200, T_1530, T_0000]).map(time => ({ - time, - })) - ) - }) + describe("equal", () => { + it("successfully finds a row", () => + expectQuery({ equal: { time: T_1000 } }).toContainExactly([ + { time: "10:00:00" }, + ])) - describe("equal", () => { - it("successfully finds a row", () => - expectQuery({ equal: { time: T_1000 } }).toContainExactly([ - { time: "10:00:00" }, - ])) + it("fails to find nonexistent row", () => + expectQuery({ equal: { time: UNEXISTING_TIME } }).toFindNothing()) + }) - it("fails to find nonexistent row", () => - expectQuery({ equal: { time: "10:01" } }).toFindNothing()) + describe("notEqual", () => { + it("successfully finds a row", () => + expectQuery({ notEqual: { time: T_1000 } }).toContainExactly([ + { time: "10:45:00" }, + { time: "12:00:00" }, + { time: "15:30:00" }, + { time: "00:00:00" }, + ])) + + it("return all when requesting non-existing", () => + expectQuery({ notEqual: { time: UNEXISTING_TIME } }).toContainExactly( + [ + { time: "10:00:00" }, + { time: "10:45:00" }, + { time: "12:00:00" }, + { time: "15:30:00" }, + { time: "00:00:00" }, + ] + )) + }) + + describe("oneOf", () => { + it("successfully finds a row", () => + expectQuery({ oneOf: { time: [T_1000] } }).toContainExactly([ + { time: "10:00:00" }, + ])) + + it("fails to find nonexistent row", () => + expectQuery({ oneOf: { time: [UNEXISTING_TIME] } }).toFindNothing()) + }) + + describe("range", () => { + it("successfully finds a row", () => + expectQuery({ + range: { time: { low: T_1045, high: T_1045 } }, + }).toContainExactly([{ time: "10:45:00" }])) + + it("successfully finds multiple rows", () => + expectQuery({ + range: { time: { low: T_1045, high: T_1530 } }, + }).toContainExactly([ + { time: "10:45:00" }, + { time: "12:00:00" }, + { time: "15:30:00" }, + ])) + + it("successfully finds no rows", () => + expectQuery({ + range: { time: { low: UNEXISTING_TIME, high: UNEXISTING_TIME } }, + }).toFindNothing()) + }) + + describe("sort", () => { + it("sorts ascending", () => + expectSearch({ + query: {}, + sort: "time", + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([ + { time: "00:00:00" }, + { time: "10:00:00" }, + { time: "10:45:00" }, + { time: "12:00:00" }, + { time: "15:30:00" }, + ])) + + it("sorts descending", () => + expectSearch({ + query: {}, + sort: "time", + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([ + { time: "15:30:00" }, + { time: "12:00:00" }, + { time: "10:45:00" }, + { time: "10:00:00" }, + { time: "00:00:00" }, + ])) + + describe("sortType STRING", () => { + it("sorts ascending", () => + expectSearch({ + query: {}, + sort: "time", + sortType: SortType.STRING, + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([ + { time: "00:00:00" }, + { time: "10:00:00" }, + { time: "10:45:00" }, + { time: "12:00:00" }, + { time: "15:30:00" }, + ])) + + it("sorts descending", () => + expectSearch({ + query: {}, + sort: "time", + sortType: SortType.STRING, + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([ + { time: "15:30:00" }, + { time: "12:00:00" }, + { time: "10:45:00" }, + { time: "10:00:00" }, + { time: "00:00:00" }, + ])) + }) + }) }) - }) describe.each([FieldType.ARRAY, FieldType.OPTIONS])("%s", () => { beforeAll(async () => { From 7d709d0d221963234c1c3e97c6f3440d1f4c11d0 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 20 May 2024 16:57:03 +0200 Subject: [PATCH 10/25] Fix flaky test --- packages/server/src/api/routes/tests/search.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index 5e306739c9..fec0c4738e 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -407,8 +407,7 @@ describe.each([ }) it("should parse the encoded js binding. Return rows with appointments 2 weeks in the past", async () => { - const jsBinding = - "const currentTime = new Date()\ncurrentTime.setDate(currentTime.getDate()-14);\nreturn currentTime.toISOString();" + const jsBinding = `const currentTime = new Date(${Date.now()})\ncurrentTime.setDate(currentTime.getDate()-14);\nreturn currentTime.toISOString();` const encodedBinding = encodeJSBinding(jsBinding) await expectQuery({ From 028aaa0bb40d1788bb4539b1c681a82c6bf03234 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 20 May 2024 17:00:12 +0200 Subject: [PATCH 11/25] Clean --- packages/server/src/integrations/base/sql.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/server/src/integrations/base/sql.ts b/packages/server/src/integrations/base/sql.ts index dd663dc918..1b8d2ea4ae 100644 --- a/packages/server/src/integrations/base/sql.ts +++ b/packages/server/src/integrations/base/sql.ts @@ -645,14 +645,6 @@ class SqlQueryBuilder extends SqlTableQueryBuilder { config.useNullAsDefault = true } - if (sqlClient === SqlClient.MS_SQL) { - // config.connection ??= {} - // config.connection.typeCast = (field: any, next: any): any => { - // if (field.type === "TIME") return field.string() - // return next() - // } - } - const client = knex(config) let query: Knex.QueryBuilder From 356da44b4b4133370fcd9b89e578d2ee144c9be2 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 20 May 2024 17:00:51 +0200 Subject: [PATCH 12/25] Clean code --- packages/server/src/integrations/base/sql.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/server/src/integrations/base/sql.ts b/packages/server/src/integrations/base/sql.ts index 1b8d2ea4ae..59efe9a9ba 100644 --- a/packages/server/src/integrations/base/sql.ts +++ b/packages/server/src/integrations/base/sql.ts @@ -646,7 +646,6 @@ class SqlQueryBuilder extends SqlTableQueryBuilder { } const client = knex(config) - let query: Knex.QueryBuilder const builder = new InternalBuilder(sqlClient) switch (this._operation(json)) { From 1d00604674d0c85fc8489f2015c34ef0a6557a8b Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 20 May 2024 17:06:34 +0200 Subject: [PATCH 13/25] Lint --- packages/bbui/src/Form/Core/DatePicker/TimePicker.svelte | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/bbui/src/Form/Core/DatePicker/TimePicker.svelte b/packages/bbui/src/Form/Core/DatePicker/TimePicker.svelte index af38eaef7a..6f1490a573 100644 --- a/packages/bbui/src/Form/Core/DatePicker/TimePicker.svelte +++ b/packages/bbui/src/Form/Core/DatePicker/TimePicker.svelte @@ -31,10 +31,4 @@ flex-direction: row; align-items: center; } - .time-picker span { - font-weight: bold; - font-size: 18px; - z-index: 0; - margin-bottom: 1px; - } From 2b1df81649dde09fd9d8974720818133e9a669c7 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 21 May 2024 11:24:04 +0200 Subject: [PATCH 14/25] Fix null references --- packages/server/src/integrations/base/sql.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/integrations/base/sql.ts b/packages/server/src/integrations/base/sql.ts index 59efe9a9ba..25f2d13a35 100644 --- a/packages/server/src/integrations/base/sql.ts +++ b/packages/server/src/integrations/base/sql.ts @@ -133,7 +133,7 @@ function generateSelectStatement( } if ( knex.client.config.client === SqlClient.MS_SQL && - columnSchema.type === FieldType.DATETIME && + columnSchema?.type === FieldType.DATETIME && columnSchema.timeOnly ) { return knex.raw(`CONVERT(varchar, ${field}, 108) as "${field}"`) From d97f3b03781e796c2eff7288de823bb6bea9baa4 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 21 May 2024 11:54:42 +0200 Subject: [PATCH 15/25] Handle undefineds --- packages/bbui/src/Form/Core/DatePicker/TimePicker.svelte | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/bbui/src/Form/Core/DatePicker/TimePicker.svelte b/packages/bbui/src/Form/Core/DatePicker/TimePicker.svelte index 6f1490a573..14f3559e9a 100644 --- a/packages/bbui/src/Form/Core/DatePicker/TimePicker.svelte +++ b/packages/bbui/src/Form/Core/DatePicker/TimePicker.svelte @@ -9,6 +9,11 @@ $: displayValue = value?.format("HH:mm") const handleChange = e => { + if (!e.target.value) { + dispatch("change", undefined) + return + } + const [hour, minute] = e.target.value.split(":").map(x => parseInt(x)) dispatch("change", value.hour(hour).minute(minute)) } From fc99fad3d2e85a0090724150ea9cb541ddd65bcb Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 21 May 2024 11:54:55 +0200 Subject: [PATCH 16/25] Fix display in chromium --- packages/bbui/src/Form/Core/DatePicker/NumberInput.svelte | 3 +++ packages/bbui/src/Form/Core/DatePicker/TimePicker.svelte | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/bbui/src/Form/Core/DatePicker/NumberInput.svelte b/packages/bbui/src/Form/Core/DatePicker/NumberInput.svelte index 7e013341ac..6c06ce4e79 100644 --- a/packages/bbui/src/Form/Core/DatePicker/NumberInput.svelte +++ b/packages/bbui/src/Form/Core/DatePicker/NumberInput.svelte @@ -52,4 +52,7 @@ input.hide-arrows { -moz-appearance: textfield; } + input[type="time"]::-webkit-calendar-picker-indicator { + display: none; + } diff --git a/packages/bbui/src/Form/Core/DatePicker/TimePicker.svelte b/packages/bbui/src/Form/Core/DatePicker/TimePicker.svelte index 14f3559e9a..e1ea4d625b 100644 --- a/packages/bbui/src/Form/Core/DatePicker/TimePicker.svelte +++ b/packages/bbui/src/Form/Core/DatePicker/TimePicker.svelte @@ -24,7 +24,6 @@ hideArrows type={"time"} value={displayValue} - width={60} on:input={handleChange} on:change={handleChange} /> From 221c8a3f0a7734bd53446855a23e0226cc5eb666 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 21 May 2024 13:31:15 +0200 Subject: [PATCH 17/25] Shuffle all test createRows --- .../server/src/api/routes/tests/search.spec.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index 2a5890decf..62d55e7e32 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -78,7 +78,7 @@ describe.each([ } async function createRows(rows: Record[]) { - await config.api.row.bulkImport(table._id!, { rows }) + await config.api.row.bulkImport(table._id!, { rows: _.shuffle(rows) }) } class SearchAssertion { @@ -981,11 +981,13 @@ describe.each([ time: { name: "time", type: FieldType.DATETIME, timeOnly: true }, }) - await createRows( - _.shuffle([T_1000, T_1045, T_1200, T_1530, T_0000]).map(time => ({ - time, - })) - ) + await createRows([ + { time: T_1000 }, + { time: T_1045 }, + { time: T_1200 }, + { time: T_1530 }, + { time: T_0000 }, + ]) }) describe("equal", () => { From 7c7f88bd47a3b9b776a3efcef40ad946e54fb8a8 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 21 May 2024 13:41:46 +0200 Subject: [PATCH 18/25] Pop on asserts --- .../server/src/api/routes/tests/search.spec.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index 62d55e7e32..74eca789cb 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -84,7 +84,7 @@ describe.each([ class SearchAssertion { constructor(private readonly query: RowSearchParams) {} - private findRow(expectedRow: any, foundRows: any[]) { + private popRow(expectedRow: any, foundRows: any[]) { const row = foundRows.find(foundRow => _.isMatch(foundRow, expectedRow)) if (!row) { const fields = Object.keys(expectedRow) @@ -97,6 +97,9 @@ describe.each([ )} in ${JSON.stringify(searchedObjects)}` ) } + + // Ensuring the same row is not matched twice + foundRows.splice(foundRows.indexOf(row), 1) return row } @@ -113,9 +116,9 @@ describe.each([ // eslint-disable-next-line jest/no-standalone-expect expect(foundRows).toHaveLength(expectedRows.length) // eslint-disable-next-line jest/no-standalone-expect - expect(foundRows).toEqual( + expect([...foundRows]).toEqual( expectedRows.map((expectedRow: any) => - expect.objectContaining(this.findRow(expectedRow, foundRows)) + expect.objectContaining(this.popRow(expectedRow, foundRows)) ) ) } @@ -132,10 +135,10 @@ describe.each([ // eslint-disable-next-line jest/no-standalone-expect expect(foundRows).toHaveLength(expectedRows.length) // eslint-disable-next-line jest/no-standalone-expect - expect(foundRows).toEqual( + expect([...foundRows]).toEqual( expect.arrayContaining( expectedRows.map((expectedRow: any) => - expect.objectContaining(this.findRow(expectedRow, foundRows)) + expect.objectContaining(this.popRow(expectedRow, foundRows)) ) ) ) @@ -151,10 +154,10 @@ describe.each([ }) // eslint-disable-next-line jest/no-standalone-expect - expect(foundRows).toEqual( + expect([...foundRows]).toEqual( expect.arrayContaining( expectedRows.map((expectedRow: any) => - expect.objectContaining(this.findRow(expectedRow, foundRows)) + expect.objectContaining(this.popRow(expectedRow, foundRows)) ) ) ) From 6eefa1afe81d4ebd503dffdd25d00909f3cb7aa6 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 21 May 2024 14:30:03 +0200 Subject: [PATCH 19/25] Add comment --- packages/server/src/api/routes/tests/search.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index 74eca789cb..00864e7454 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -78,6 +78,7 @@ describe.each([ } async function createRows(rows: Record[]) { + // Shuffling to avoid false positives given a fixed order await config.api.row.bulkImport(table._id!, { rows: _.shuffle(rows) }) } From 8cf021f254f8144b0ffa14624871ecd3fb3c4505 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 21 May 2024 14:30:37 +0200 Subject: [PATCH 20/25] Add null row --- packages/server/src/api/routes/tests/search.spec.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index 00864e7454..15220a9c1a 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -980,12 +980,16 @@ describe.each([ const UNEXISTING_TIME = "10:01" + const NULL_TIME__ID = `null_time__id` + beforeAll(async () => { await createTable({ + timeid: { name: "timeid", type: FieldType.STRING }, time: { name: "time", type: FieldType.DATETIME, timeOnly: true }, }) await createRows([ + { timeid: NULL_TIME__ID, time: null }, { time: T_1000 }, { time: T_1045 }, { time: T_1200 }, From 53605ec8ed68c06c5baab65e98706e2642bec2ef Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 21 May 2024 16:08:22 +0200 Subject: [PATCH 21/25] Fix sorting in pg --- packages/server/src/integrations/base/sql.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/server/src/integrations/base/sql.ts b/packages/server/src/integrations/base/sql.ts index 25f2d13a35..1d0d909829 100644 --- a/packages/server/src/integrations/base/sql.ts +++ b/packages/server/src/integrations/base/sql.ts @@ -387,7 +387,13 @@ class InternalBuilder { for (let [key, value] of Object.entries(sort)) { const direction = value.direction === SortDirection.ASCENDING ? "asc" : "desc" - query = query.orderBy(`${aliased}.${key}`, direction) + let nulls + if (this.client === SqlClient.POSTGRES) { + // All other clients already sort this as expected by default, and adding this to the rest of the clients is causing issues + nulls = value.direction === SortDirection.ASCENDING ? "first" : "last" + } + + query = query.orderBy(`${aliased}.${key}`, direction, nulls) } } else if (this.client === SqlClient.MS_SQL && paginate?.limit) { // @ts-ignore From a55f975489abebad2c020c497aac32f1f85f862b Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 21 May 2024 16:11:36 +0200 Subject: [PATCH 22/25] Fix tests --- packages/server/src/api/routes/tests/search.spec.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index 15220a9c1a..709f8a7597 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -1067,6 +1067,7 @@ describe.each([ sort: "time", sortOrder: SortOrder.ASCENDING, }).toMatchExactly([ + { timeid: NULL_TIME__ID }, { time: "00:00:00" }, { time: "10:00:00" }, { time: "10:45:00" }, @@ -1085,6 +1086,7 @@ describe.each([ { time: "10:45:00" }, { time: "10:00:00" }, { time: "00:00:00" }, + { timeid: NULL_TIME__ID }, ])) describe("sortType STRING", () => { @@ -1095,6 +1097,7 @@ describe.each([ sortType: SortType.STRING, sortOrder: SortOrder.ASCENDING, }).toMatchExactly([ + { timeid: NULL_TIME__ID }, { time: "00:00:00" }, { time: "10:00:00" }, { time: "10:45:00" }, @@ -1114,6 +1117,7 @@ describe.each([ { time: "10:45:00" }, { time: "10:00:00" }, { time: "00:00:00" }, + { timeid: NULL_TIME__ID }, ])) }) }) From 4bf0a43c64ed73d9d213d24aa91ed9c6be2adbd5 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 21 May 2024 16:35:11 +0200 Subject: [PATCH 23/25] Add comment --- packages/server/src/integrations/base/sql.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/server/src/integrations/base/sql.ts b/packages/server/src/integrations/base/sql.ts index 1d0d909829..c3dca2a39c 100644 --- a/packages/server/src/integrations/base/sql.ts +++ b/packages/server/src/integrations/base/sql.ts @@ -136,6 +136,7 @@ function generateSelectStatement( columnSchema?.type === FieldType.DATETIME && columnSchema.timeOnly ) { + // Time gets returned as timestamp from mssql, not matching the expected HH:mm format return knex.raw(`CONVERT(varchar, ${field}, 108) as "${field}"`) } return `${field} as ${field}` From d93a9e2c4f6d7cb531a6dfd84582d1dc776ef682 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 21 May 2024 16:52:40 +0200 Subject: [PATCH 24/25] Fix value from null --- packages/bbui/src/Form/Core/DatePicker/TimePicker.svelte | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/bbui/src/Form/Core/DatePicker/TimePicker.svelte b/packages/bbui/src/Form/Core/DatePicker/TimePicker.svelte index e1ea4d625b..4f070bdcfb 100644 --- a/packages/bbui/src/Form/Core/DatePicker/TimePicker.svelte +++ b/packages/bbui/src/Form/Core/DatePicker/TimePicker.svelte @@ -1,4 +1,5 @@ From a920161e92e5aaf283733c6e17681d77bb952d64 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 21 May 2024 22:02:47 +0200 Subject: [PATCH 25/25] Fix tests --- .../src/integrations/tests/sqlAlias.spec.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/server/src/integrations/tests/sqlAlias.spec.ts b/packages/server/src/integrations/tests/sqlAlias.spec.ts index fda2a091fa..0de4d0a151 100644 --- a/packages/server/src/integrations/tests/sqlAlias.spec.ts +++ b/packages/server/src/integrations/tests/sqlAlias.spec.ts @@ -61,9 +61,9 @@ describe("Captures of real examples", () => { "b"."taskid" as "b.taskid", "b"."completed" as "b.completed", "b"."qaid" as "b.qaid", "b"."executorid" as "b.executorid", "b"."taskname" as "b.taskname", "b"."taskid" as "b.taskid", "b"."completed" as "b.completed", "b"."qaid" as "b.qaid" - from (select * from "persons" as "a" order by "a"."firstname" asc limit $1) as "a" + from (select * from "persons" as "a" order by "a"."firstname" asc nulls first limit $1) as "a" left join "tasks" as "b" on "a"."personid" = "b"."qaid" or "a"."personid" = "b"."executorid" - order by "a"."firstname" asc limit $2`), + order by "a"."firstname" asc nulls first limit $2`), }) }) @@ -75,10 +75,10 @@ describe("Captures of real examples", () => { sql: multiline(`select "a"."productname" as "a.productname", "a"."productid" as "a.productid", "b"."executorid" as "b.executorid", "b"."taskname" as "b.taskname", "b"."taskid" as "b.taskid", "b"."completed" as "b.completed", "b"."qaid" as "b.qaid" - from (select * from "products" as "a" order by "a"."productname" asc limit $1) as "a" + from (select * from "products" as "a" order by "a"."productname" asc nulls first limit $1) as "a" left join "products_tasks" as "c" on "a"."productid" = "c"."productid" left join "tasks" as "b" on "b"."taskid" = "c"."taskid" where "b"."taskname" = $2 - order by "a"."productname" asc limit $3`), + order by "a"."productname" asc nulls first limit $3`), }) }) @@ -90,10 +90,10 @@ describe("Captures of real examples", () => { sql: multiline(`select "a"."productname" as "a.productname", "a"."productid" as "a.productid", "b"."executorid" as "b.executorid", "b"."taskname" as "b.taskname", "b"."taskid" as "b.taskid", "b"."completed" as "b.completed", "b"."qaid" as "b.qaid" - from (select * from "products" as "a" order by "a"."productname" asc limit $1) as "a" + from (select * from "products" as "a" order by "a"."productname" asc nulls first limit $1) as "a" left join "products_tasks" as "c" on "a"."productid" = "c"."productid" left join "tasks" as "b" on "b"."taskid" = "c"."taskid" - order by "a"."productname" asc limit $2`), + order by "a"."productname" asc nulls first limit $2`), }) }) @@ -138,11 +138,11 @@ describe("Captures of real examples", () => { "c"."personid" as "c.personid", "c"."address" as "c.address", "c"."age" as "c.age", "c"."type" as "c.type", "c"."city" as "c.city", "c"."lastname" as "c.lastname" from (select * from "tasks" as "a" where not "a"."completed" = $1 - order by "a"."taskname" asc limit $2) as "a" + order by "a"."taskname" asc nulls first limit $2) as "a" left join "products_tasks" as "d" on "a"."taskid" = "d"."taskid" left join "products" as "b" on "b"."productid" = "d"."productid" left join "persons" as "c" on "a"."executorid" = "c"."personid" or "a"."qaid" = "c"."personid" - where "c"."year" between $3 and $4 and "b"."productname" = $5 order by "a"."taskname" asc limit $6`), + where "c"."year" between $3 and $4 and "b"."productname" = $5 order by "a"."taskname" asc nulls first limit $6`), }) }) })