From 36d0b0f7d7a6ebc061673f6144355279cb7455f4 Mon Sep 17 00:00:00 2001 From: tanin47 Date: Fri, 23 Jan 2026 19:29:01 +0700 Subject: [PATCH] Trim and not null --- .github/workflows/ci.yml | 6 +- .../backdoor/core/BackdoorCoreServer.java | 20 +++-- .../main/java/tanin/backdoor/core/Filter.java | 13 ++- .../tanin/backdoor/core/engine/Engine.java | 17 +++- .../tanin/backdoor/postgres/TableTest.java | 39 ++++++++- .../test/java/tanin/backdoor/ux/EditTest.java | 20 +++++ frontend/svelte/_edit_modal.svelte | 40 +++++++--- frontend/svelte/_filter_modal.svelte | 80 +++++++++++++++---- frontend/svelte/common/models.ts | 5 +- 9 files changed, 196 insertions(+), 44 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5719eaa..c6cb2cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,7 @@ jobs: node-version: '22' - uses: browser-actions/setup-chrome@v2 with: - chrome-version: '142' + chrome-version: '144' install-chromedriver: true - name: Setup Gradle uses: gradle/actions/setup-gradle@v3 @@ -138,7 +138,7 @@ jobs: node-version: '22' - uses: browser-actions/setup-chrome@v2 with: - chrome-version: '142' + chrome-version: '143' install-chromedriver: true - name: Setup Gradle uses: gradle/actions/setup-gradle@v3 @@ -186,7 +186,7 @@ jobs: node-version: '22' - uses: browser-actions/setup-chrome@v2 with: - chrome-version: '142' + chrome-version: '144' install-chromedriver: true - name: Setup Gradle uses: gradle/actions/setup-gradle@v3 diff --git a/core/src/main/java/tanin/backdoor/core/BackdoorCoreServer.java b/core/src/main/java/tanin/backdoor/core/BackdoorCoreServer.java index 0791898..0dc5234 100644 --- a/core/src/main/java/tanin/backdoor/core/BackdoorCoreServer.java +++ b/core/src/main/java/tanin/backdoor/core/BackdoorCoreServer.java @@ -274,8 +274,9 @@ public FullSystem start() throws Exception { var primaryKeyFilters = json.asObject().get("primaryKeys").asArray().values().stream().map(s -> { var o = s.asObject(); var value = o.get("value"); + var operator = Filter.Operator.valueOf(o.get("operator").asString()); - return new Filter(o.get("name").asString(), value.isNull() ? null : value.asString()); + return new Filter(o.get("name").asString(), value.asString(), operator); }).toArray(Filter[]::new); try (var engine = makeEngine(database)) { @@ -339,16 +340,17 @@ public FullSystem start() throws Exception { var primaryKeyFilters = json.asObject().get("primaryKeys").asArray().values().stream().map(s -> { var o = s.asObject(); var value = o.get("value"); + var operator = Filter.Operator.valueOf(o.get("operator").asString()); - return new Filter(o.get("name").asString(), value.isNull() ? null : value.asString()); + return new Filter(o.get("name").asString(), value.asString(), operator); }).toArray(Filter[]::new); try (var engine = makeEngine(database)) { var column = Arrays.stream(engine.getColumns(tableName)).filter(c -> c.name.equals(columnName)).findFirst().orElse(null); - - var newSantiziedValue = setToNull ? null : newValue; - engine.update(tableName, column, newSantiziedValue, primaryKeyFilters); assert column != null; + var newSanitized = setToNull ? null : newValue; + + engine.update(tableName, column, newSanitized, primaryKeyFilters); AtomicReference newFetchedValue = new AtomicReference<>(Json.NULL); engine.select( tableName, @@ -356,7 +358,7 @@ public FullSystem start() throws Exception { Arrays.stream(primaryKeyFilters) .peek(p -> { if (p.name.equals(columnName)) { - p.value = newSantiziedValue; + p.value = newSanitized; } }) .toArray(Filter[]::new), @@ -434,8 +436,9 @@ public FullSystem start() throws Exception { var filters = json.asObject().get("filters").asArray().values().stream().map(s -> { var o = s.asObject(); var value = o.get("value"); + var operator = Filter.Operator.valueOf(o.get("operator").asString()); - return new Filter(o.get("name").asString(), value.isNull() ? null : value.asString()); + return new Filter(o.get("name").asString(), value.asString(), operator); }).toArray(Filter[]::new); var sorts = json.asObject().get("sorts").asArray().values().stream().map(s -> { var o = s.asObject(); @@ -554,8 +557,9 @@ public FullSystem start() throws Exception { var filters = json.asObject().get("filters").asArray().values().stream().map(s -> { var o = s.asObject(); var value = o.get("value"); + var operator = Filter.Operator.valueOf(o.get("operator").asString()); - return new Filter(o.get("name").asString(), value.isNull() ? null : value.asString()); + return new Filter(o.get("name").asString(), value.asString(), operator); }).toArray(Filter[]::new); var sorts = json.asObject().get("sorts").asArray().values().stream().map(s -> { var o = s.asObject(); diff --git a/core/src/main/java/tanin/backdoor/core/Filter.java b/core/src/main/java/tanin/backdoor/core/Filter.java index b8f7ec0..197f341 100644 --- a/core/src/main/java/tanin/backdoor/core/Filter.java +++ b/core/src/main/java/tanin/backdoor/core/Filter.java @@ -4,18 +4,27 @@ import com.eclipsesource.json.JsonValue; public class Filter { + public enum Operator { + EQUAL, + IS_NULL, + IS_NOT_NULL + } + public String name; public String value; + public Operator operator; - Filter(String name, String value) { + Filter(String name, String value, Operator operator) { this.name = name; this.value = value; + this.operator = operator; } public JsonValue toJson() { return Json.object() .add("name", name) - .add("value", value); + .add("value", value) + .add("operator", operator.toString()); } } diff --git a/core/src/main/java/tanin/backdoor/core/engine/Engine.java b/core/src/main/java/tanin/backdoor/core/engine/Engine.java index b5385eb..d40aa96 100644 --- a/core/src/main/java/tanin/backdoor/core/engine/Engine.java +++ b/core/src/main/java/tanin/backdoor/core/engine/Engine.java @@ -154,10 +154,19 @@ public String makeWhereClause(Filter[] filters) { var clauses = Arrays .stream(filters) .map(s -> { - var value = s.value == null ? "NULL" : makeSqlLiteral(s.value); - var op = s.value == null ? "IS" : "="; - - return makeSqlName(s.name) + " " + op + " " + value; + String operatorAndValue; + + if (s.operator == Filter.Operator.IS_NULL) { + operatorAndValue = " IS NULL"; + } else if (s.operator == Filter.Operator.IS_NOT_NULL) { + operatorAndValue = " IS NOT NULL"; + } else if (s.operator == Filter.Operator.EQUAL) { + operatorAndValue = " = " + makeSqlLiteral(s.value); + } else { + throw new RuntimeException("Unknown operator: " + s.operator); + } + + return makeSqlName(s.name) + " " + operatorAndValue; }) .toArray(String[]::new); whereClause = " WHERE " + String.join(" AND ", clauses); diff --git a/core/src/test/java/tanin/backdoor/postgres/TableTest.java b/core/src/test/java/tanin/backdoor/postgres/TableTest.java index 4606b8d..254ad0a 100644 --- a/core/src/test/java/tanin/backdoor/postgres/TableTest.java +++ b/core/src/test/java/tanin/backdoor/postgres/TableTest.java @@ -305,7 +305,7 @@ void deleteRow() throws InterruptedException { } @Test - void filterRow() throws InterruptedException { + void filterRowSpecificValue() throws InterruptedException { go("/"); click(tid("database-item")); waitUntil(() -> assertEquals("loaded", elem(tid("database-item")).getDomAttribute("data-database-status"))); @@ -323,6 +323,43 @@ void filterRow() throws InterruptedException { waitUntil(() -> assertColumnValues("username", "test_user_2")); } + @Test + void filterRowNullAndNotNull() throws Exception { + try (var engine = server.engineProvider.createEngine(postgresConfig, null)) { + engine.connection.createStatement().execute( + """ + INSERT INTO "user" ( + id, + username, + password + ) VALUES ( + '100', + 'null-user', + NULL + ) + """ + ); + } + go("/"); + click(tid("database-item")); + waitUntil(() -> assertEquals("loaded", elem(tid("database-item")).getDomAttribute("data-database-status"))); + + click(tid("menu-items", "postgres", null, "menu-item-table", "user")); + + waitUntil(() -> assertTrue(hasElem(tid("sheet-tab", "user")))); + waitUntil(() -> assertColumnValues("username", "test_user_1", "test_user_2", "test_user_3", "test_user_4", "null-user")); + + click(tid("sheet-view-column-header", "password", null, "filter-button")); + click(tid("not-null-checkbox")); + click(tid("submit-button")); + waitUntil(() -> assertColumnValues("username", "test_user_1", "test_user_2", "test_user_3", "test_user_4")); + + click(tid("sheet-view-column-header", "password", null, "filter-button")); + click(tid("null-checkbox")); + click(tid("submit-button")); + waitUntil(() -> assertColumnValues("username", "null-user")); + } + @Test void sortRow() throws InterruptedException { go("/"); diff --git a/core/src/test/java/tanin/backdoor/ux/EditTest.java b/core/src/test/java/tanin/backdoor/ux/EditTest.java index 26bf369..b94cf6a 100644 --- a/core/src/test/java/tanin/backdoor/ux/EditTest.java +++ b/core/src/test/java/tanin/backdoor/ux/EditTest.java @@ -36,6 +36,26 @@ void editToNullAndRevertField() throws InterruptedException { waitUntil(() -> assertEquals("new-password", elem(tid("sheet-column-value", "password")).getText().trim())); } + @Test + void trimValue() throws InterruptedException { + go("/"); + click(tid("database-item")); + waitUntil(() -> assertEquals("loaded", elem(tid("database-item")).getDomAttribute("data-database-status"))); + + click(tid("menu-items", "postgres", null, "menu-item-table", "user")); + + click(tid("sheet-column-value", "password", null, "edit-field-button")); + fill(tid("new-value"), "\n \nnew-password\n \n"); + click(tid("trim-value-checkbox")); + click(tid("submit-button")); + waitUntil(() -> assertEquals(" \n \nnew-password\n ", elem(tid("sheet-column-value", "password")).getText())); + + click(tid("sheet-column-value", "password", null, "edit-field-button")); + click(tid("trim-value-checkbox")); + click(tid("submit-button")); + waitUntil(() -> assertEquals(" new-password", elem(tid("sheet-column-value", "password")).getText())); + } + @Test void useNowTimestamp() throws Exception { try (var engine = server.engineProvider.createEngine(postgresConfig, null)) { diff --git a/frontend/svelte/_edit_modal.svelte b/frontend/svelte/_edit_modal.svelte index 0c38ca2..ced0550 100644 --- a/frontend/svelte/_edit_modal.svelte +++ b/frontend/svelte/_edit_modal.svelte @@ -17,6 +17,7 @@ let rowIndex_: number | null = null let currentValue: any let currentColumn: Column | null let setToNull: boolean = false +let shouldTrim: boolean = true let isLoading = false let errors: string[] = [] @@ -45,7 +46,11 @@ export function open(value: any, column: Column, rowValues: any[], rowIndex: num .map((column, index) => { if (column.isPrimaryKey) { const value = rowValues[index]; - return {name: column.name, value: value === null ? null : ('' + value)} + return { + name: column.name, + value: ('' + value), + operator: value === null ? 'IS NULL' : 'EQUAL' + } } else { return null } @@ -86,7 +91,7 @@ async function submit() { table: sheet.name, primaryKeys, column: currentColumn!.name, - value: currentValue, + value: shouldTrim ? currentValue.trim() : currentValue, setToNull, }) @@ -165,15 +170,28 @@ async function submit() { autocorrect="off" > - {#if currentColumn && currentColumn.type === 'TIMESTAMP' && !setToNull} -
{ - currentValue = new Date().toISOString() - }} - >Use the current timestamp -
+ {#if !setToNull && currentColumn} + {#if currentColumn.type === 'TIMESTAMP'} +
{ + currentValue = new Date().toISOString() + }} + >Use the current timestamp +
+ {:else if currentColumn.type === 'STRING'} + + {/if} {/if}
diff --git a/frontend/svelte/_filter_modal.svelte b/frontend/svelte/_filter_modal.svelte index 692a07c..7c2d131 100644 --- a/frontend/svelte/_filter_modal.svelte +++ b/frontend/svelte/_filter_modal.svelte @@ -14,7 +14,8 @@ let textarea: HTMLTextAreaElement | null let specifiedValue: any let currentColumn: Column | null -let mode: 'no_filter' | 'null' | 'specified_value' = 'specified_value' +let mode: 'no_filter' | 'null' | 'not_null' | 'specified_value' = 'specified_value' +let shouldTrim = true let isLoading = false let errors: string[] = [] @@ -36,12 +37,16 @@ export function open(column: Column): void { mode = 'no_filter' specifiedValue = '' } else { - specifiedValue = sheet.filters[foundIndex].value ?? '' + const filter = sheet.filters[foundIndex] - if (specifiedValue === null) { + if (filter.operator === 'IS_NULL') { mode = 'null' - } else { + } else if (filter.operator === 'IS_NOT_NULL') { + mode = 'not_null' + } else if (filter.operator === 'EQUAL') { mode = 'specified_value' + } else { + throw new Error(`Unrecognized column '${filter.operator}'`) } } currentColumn = column @@ -82,9 +87,15 @@ async function submit() { if (mode === 'no_filter') { // do nothing } else if (mode === 'null') { - newFilters.push({name: currentColumn.name, value: null}) + newFilters.push({name: currentColumn.name, value: '', operator: 'IS_NULL'}) + } else if (mode === 'not_null') { + newFilters.push({name: currentColumn.name, value: '', operator: 'IS_NOT_NULL'}) } else if (mode === 'specified_value') { - newFilters.push({name: currentColumn.name, value: specifiedValue}) + newFilters.push({ + name: currentColumn.name, + value: shouldTrim ? specifiedValue.trim() : specifiedValue, + operator: 'EQUAL' + }) } try { @@ -132,6 +143,7 @@ async function submit() { {mode = 'null'}} @@ -139,6 +151,19 @@ async function submit() { Filter for null
+
+ +
{/if}
- +
+ {#if mode !== 'specified_value'} +
{ + if (mode !== 'specified_value') { + mode = 'specified_value' + } + }} + >
+ {/if} + +
+ {#if mode === 'specified_value' && currentColumn} + {#if currentColumn.type === 'STRING'} + + {/if} + {/if}