From 66d7b965dc07acfd4e8edf231981cc3e9e5c3055 Mon Sep 17 00:00:00 2001 From: onenewcode Date: Mon, 13 Jul 2026 18:14:11 +0800 Subject: [PATCH] feat(grid): add list and range filters --- apps/desktop/src/components/grid/DataGrid.vue | 93 +++- apps/desktop/src/i18n/locales/en.ts | 7 + apps/desktop/src/i18n/locales/es.ts | 7 + apps/desktop/src/i18n/locales/it.ts | 7 + apps/desktop/src/i18n/locales/ja.ts | 7 + apps/desktop/src/i18n/locales/pt-BR.ts | 7 + apps/desktop/src/i18n/locales/zh-CN.ts | 7 + apps/desktop/src/i18n/locales/zh-TW.ts | 7 + .../src/lib/dataGrid/dataGridColumnFilter.ts | 106 +++- apps/desktop/src/lib/dataGrid/dataGridSql.ts | 4 +- crates/dbx-core/src/data_grid_sql.rs | 515 +++++++++++++++++- .../app-tests/dataGridColumnFilter.test.ts | 79 ++- 12 files changed, 802 insertions(+), 44 deletions(-) diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 7908e89ecb..d71f421266 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -2,8 +2,9 @@ type CachedStructuredFilterRule = { id: string; columnName: string; - mode: "equals" | "not-equals" | "is-null" | "is-not-null" | "like" | "not-like" | "less-than" | "greater-than"; + mode: "equals" | "not-equals" | "is-null" | "is-not-null" | "like" | "not-like" | "less-than" | "greater-than" | "in" | "not-in" | "between" | "not-between"; rawValue: string; + rawEndValue: string; conjunction: "AND" | "OR"; disabled?: boolean; }; @@ -164,7 +165,19 @@ import { dataGridSaveActionMode, dataGridSaveToolbarState } from "@/lib/dataGrid import type { QueryEditabilityReason } from "@/lib/sql/sqlAnalysis"; import { EDITOR_FONT_FAMILY_CSS_VAR } from "@/lib/editor/editorThemes"; import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage"; -import { appendColumnValueFilterCondition, buildColumnValueFilterCondition, buildColumnValuesFilterCondition, combineWhereInputs, filterModeNeedsValue, parseFilterValue } from "@/lib/dataGrid/dataGridColumnFilter"; +import { + appendColumnValueFilterCondition, + buildColumnValueFilterCondition, + buildColumnValuesFilterCondition, + combineWhereInputs, + filterModeHasCompleteValue, + filterModeIsSupportedForDatabase, + filterModeNeedsValue, + filterModeUsesList, + filterModeUsesRange, + parseFilterValue, + parseFilterValues, +} from "@/lib/dataGrid/dataGridColumnFilter"; import { clampSearchSplitWidth } from "@/lib/dataGrid/dataGridSearchSplit"; import { MAX_RESULT_PAGE_SIZE, MIN_RESULT_PAGE_SIZE, normalizeResultPageSize, resultPageSizeMenuOptions } from "@/lib/dataGrid/paginationPageSize"; import { allNullColumnIndexes, filterColumnVisibilityOptions, hiddenColumnIndexesWithAllNullColumns, invertedHiddenColumnIndexes, nextHiddenColumnIndexes, removeAutoHiddenColumnIndexes, visibleColumnIndexesForFilter } from "@/lib/dataGrid/dataGridColumnVisibility"; @@ -1136,6 +1149,7 @@ type StructuredFilterRule = { columnName: string; mode: FilterMode; rawValue: string; + rawEndValue: string; conjunction: "AND" | "OR"; disabled?: boolean; }; @@ -1158,16 +1172,21 @@ let serverFilterRequestId = 0; let serverFilterSearchTimer: ReturnType | undefined; const filterBuilderOpen = ref(false); const filterBuilderColumnSearch = ref(""); -const filterModeOptions: Array<{ value: FilterMode; labelKey: string }> = [ +const allFilterModeOptions: Array<{ value: FilterMode; labelKey: string }> = [ { value: "equals", labelKey: "grid.filterBuilderEquals" }, { value: "not-equals", labelKey: "grid.filterBuilderNotEquals" }, { value: "like", labelKey: "grid.filterBuilderContains" }, { value: "not-like", labelKey: "grid.filterBuilderNotContains" }, { value: "greater-than", labelKey: "grid.filterBuilderGreaterThan" }, { value: "less-than", labelKey: "grid.filterBuilderLessThan" }, + { value: "in", labelKey: "grid.filterBuilderIn" }, + { value: "not-in", labelKey: "grid.filterBuilderNotIn" }, + { value: "between", labelKey: "grid.filterBuilderBetween" }, + { value: "not-between", labelKey: "grid.filterBuilderNotBetween" }, { value: "is-null", labelKey: "grid.filterBuilderIsNull" }, { value: "is-not-null", labelKey: "grid.filterBuilderIsNotNull" }, ]; +const filterModeOptions = computed(() => allFilterModeOptions.filter((option) => filterModeIsSupportedForDatabase(option.value, resolvedDatabaseType.value))); const filterBuilderColumns = computed(() => props.tableMeta?.columns ?? []); const filterBuilderColumnOptions = computed(() => filterBuilderColumns.value.map((column) => column.name)); const filteredFilterBuilderColumnOptions = computed(() => { @@ -1180,7 +1199,7 @@ const structuredFilterCacheKey = computed(() => props.cacheKey || [props.connect const structuredFilterScopeKey = computed(() => [props.connectionId ?? "", props.database ?? "", props.schema ?? "", props.context ?? "", props.tableMeta?.schema ?? "", props.tableMeta?.tableName ?? "", filterBuilderColumnOptions.value.join("\0")].join("\u0001")); const structuredFilterRules = ref([]); const appliedStructuredWhereInput = ref(""); -const structuredFilterCount = computed(() => structuredFilterRules.value.filter((rule) => !rule.disabled && !!rule.columnName && (!filterModeNeedsValue(rule.mode) || rule.rawValue.trim().length > 0)).length); +const structuredFilterCount = computed(() => structuredFilterRules.value.filter((rule) => !rule.disabled && !!rule.columnName && filterModeHasCompleteValue(rule.mode, rule.rawValue, rule.rawEndValue)).length); const hasStructuredFilters = computed(() => !!combineWhereInputs(undefined, appliedStructuredWhereInput.value)); const formatterOpenColumn = ref(null); type FormatterDraftKind = Exclude; @@ -1796,10 +1815,15 @@ function defaultStructuredFilterRule(): StructuredFilterRule { columnName: filterBuilderColumnOptions.value[0] ?? "", mode: "equals", rawValue: "", + rawEndValue: "", conjunction: "AND", }; } +function filterModeUsesExpandedLayout(mode: FilterMode): boolean { + return filterModeUsesList(mode) || filterModeUsesRange(mode); +} + function cloneStructuredFilterRules(rules: StructuredFilterRule[]): StructuredFilterRule[] { return rules.map((rule) => ({ ...rule })); } @@ -1815,8 +1839,11 @@ async function buildStructuredWhereFromRules(rules: StructuredFilterRule[]): Pro rules.map(async (rule) => { if (rule.disabled) return { rule, condition: null }; if (!rule.columnName) return { rule, condition: null }; - if (filterModeNeedsValue(rule.mode) && !rule.rawValue.trim()) return { rule, condition: null }; + if (!filterModeIsSupportedForDatabase(rule.mode, resolvedDatabaseType.value)) return { rule, condition: null }; + if (!filterModeHasCompleteValue(rule.mode, rule.rawValue, rule.rawEndValue)) return { rule, condition: null }; const columnInfo = filterBuilderColumns.value.find((column) => column.name === rule.columnName); + const usesList = filterModeUsesList(rule.mode); + const usesRange = filterModeUsesRange(rule.mode); return { rule, condition: @@ -1825,7 +1852,9 @@ async function buildStructuredWhereFromRules(rules: StructuredFilterRule[]): Pro columnName: rule.columnName, columnInfo, mode: rule.mode, - value: filterModeNeedsValue(rule.mode) ? parseFilterValue(rule.rawValue, columnInfo) : null, + value: !usesList && filterModeNeedsValue(rule.mode) ? parseFilterValue(rule.rawValue, columnInfo, resolvedDatabaseType.value) : null, + values: usesList ? parseFilterValues(rule.rawValue, columnInfo, resolvedDatabaseType.value) : undefined, + endValue: usesRange ? parseFilterValue(rule.rawEndValue, columnInfo, resolvedDatabaseType.value) : undefined, })) ?? null, }; }), @@ -1895,7 +1924,12 @@ function updateStructuredFilterRule(ruleId: string, patch: Partial { if (rule.id !== ruleId) return rule; const next = { ...rule, ...patch }; - if (!filterModeNeedsValue(next.mode)) next.rawValue = ""; + if (!filterModeNeedsValue(next.mode)) { + next.rawValue = ""; + next.rawEndValue = ""; + } else if (!filterModeUsesRange(next.mode)) { + next.rawEndValue = ""; + } return next; }); } @@ -9002,7 +9036,7 @@ const gridContextMenuItems = computed(() => { {{ rule.conjunction }} -
+
+
+ + +
+ (() => { {{ t("grid.filterBuilderNoValue") }}
-
+