feat(grid): distinguish blank and null filters

Closes #5006
This commit is contained in:
t8y2 committed 2026-08-24 22:33:21 +00:00
1 parent f621e40655
commit e2fa810f2e
16 files changed
+146 -10

No files matched your search

@@ -1036,6 +1036,8 @@ const allFilterModeOptions: Array<{ value: FilterMode; labelKey: string }> = [
{ value: "not-between", labelKey: "grid.filterBuilderNotBetween" },
{ value: "is-null", labelKey: "grid.filterBuilderIsNull" },
{ value: "is-not-null", labelKey: "grid.filterBuilderIsNotNull" },
{ value: "is-blank", labelKey: "grid.filterBuilderIsBlank" },
{ value: "is-not-blank", labelKey: "grid.filterBuilderIsNotBlank" },
];
const filterModeOptions = computed(() => allFilterModeOptions.filter((option) => filterModeIsSupportedForDatabase(option.value, resolvedDatabaseType.value)));
const filterBuilderColumnOptions = computed(() => filterBuilderColumns.value.map((column) => column.name));
@@ -6372,7 +6374,7 @@ async function contextFilterCondition(target: ContextFilterTarget, mode: FilterM
const { columnName, sourceResult, sourceIndex, sourceValue, requiresHydration } = target;
// CustomContextMenu closes before invoking its action. Keep the target
// stable across hydration after the close lifecycle clears contextCell.
if (mode !== "is-null" && mode !== "is-not-null") {
if (filterModeNeedsValue(mode)) {
if (!(await hydrateLargeValueCell(target.rowId, target.col))) return null;
}
if (props.result !== sourceResult) return null;
+2
View File
@@ -1711,6 +1711,8 @@ export default {
filterBuilderNotBetween: "Outside range",
filterBuilderIsNull: "Is NULL",
filterBuilderIsNotNull: "Is not NULL",
filterBuilderIsBlank: "Is blank (NULL or empty)",
filterBuilderIsNotBlank: "Is not blank (not NULL and not empty)",
columnActions: "Column actions",
localFilter: "Local value filter",
localFilterFor: "Local Values For '{column}'",
+2
View File
@@ -1594,6 +1594,8 @@ export default withEnglishFallback({
filterBuilderNotBetween: "Fuera de rango",
filterBuilderIsNull: "Es NULL",
filterBuilderIsNotNull: "No es NULL",
filterBuilderIsBlank: "Está vacío (NULL o cadena vacía)",
filterBuilderIsNotBlank: "No está vacío (ni NULL ni cadena vacía)",
columnActions: "Acciones de columna",
localFilter: "Filtro local de valores",
localFilterFor: "Valores locales para '{column}'",
+2
View File
@@ -1592,6 +1592,8 @@ export default withEnglishFallback({
filterBuilderNotBetween: "Fuori intervallo",
filterBuilderIsNull: "È NULL",
filterBuilderIsNotNull: "Non è NULL",
filterBuilderIsBlank: "È vuoto (NULL o stringa vuota)",
filterBuilderIsNotBlank: "Non è vuoto (né NULL né stringa vuota)",
columnActions: "Azioni colonna",
localFilter: "Filtro valori locale",
localFilterFor: "Valori locali per '{column}'",
+2
View File
@@ -1610,6 +1610,8 @@ export default withEnglishFallback({
filterBuilderNotBetween: "範囲外",
filterBuilderIsNull: "NULLである",
filterBuilderIsNotNull: "NULLでない",
filterBuilderIsBlank: "空(NULLまたは空文字)",
filterBuilderIsNotBlank: "空でない(NULLでも空文字でもない)",
columnActions: "列の操作",
localFilter: "ローカル値フィルター",
localFilterFor: "'{column}'のローカル値",
+2
View File
@@ -1587,6 +1587,8 @@ export default withEnglishFallback({
filterBuilderNotBetween: "범위 외",
filterBuilderIsNull: "NULL임",
filterBuilderIsNotNull: "NULL이 아님",
filterBuilderIsBlank: "비어 있음(NULL 또는 빈 문자열)",
filterBuilderIsNotBlank: "비어 있지 않음(NULL도 빈 문자열도 아님)",
columnActions: "컬럼 작업",
localFilter: "로컬 값 필터",
localFilterFor: "'{column}' 로컬 값",
+2
View File
@@ -1594,6 +1594,8 @@ export default withEnglishFallback({
filterBuilderNotBetween: "Fora do intervalo",
filterBuilderIsNull: "É NULL",
filterBuilderIsNotNull: "Não é NULL",
filterBuilderIsBlank: "Está em branco (NULL ou string vazia)",
filterBuilderIsNotBlank: "Não está em branco (nem NULL nem string vazia)",
columnActions: "Ações da coluna",
localFilter: "Filtro local de valores",
localFilterFor: "Valores locais para '{column}'",
+4 -2
View File
@@ -1634,8 +1634,10 @@ export default withEnglishFallback({
filterBuilderNotIn: "列表外",
filterBuilderBetween: "介于",
filterBuilderNotBetween: "不介于",
filterBuilderIsNull: "为空",
filterBuilderIsNotNull: "不为空",
filterBuilderIsNull: "为 NULL",
filterBuilderIsNotNull: "不为 NULL",
filterBuilderIsBlank: "为空(NULL 或空字符串)",
filterBuilderIsNotBlank: "不为空(非 NULL 且非空字符串)",
columnActions: "字段操作",
localFilter: "本地值筛选",
localFilterFor: "本地值筛选“{column}”",
+4 -2
View File
@@ -1591,8 +1591,10 @@ export default withEnglishFallback({
filterBuilderNotIn: "清單外",
filterBuilderBetween: "介於",
filterBuilderNotBetween: "不介於",
filterBuilderIsNull: "為空",
filterBuilderIsNotNull: "不為空",
filterBuilderIsNull: "為 NULL",
filterBuilderIsNotNull: "不為 NULL",
filterBuilderIsBlank: "為空(NULL 或空字串)",
filterBuilderIsNotBlank: "不為空(非 NULL 且非空字串)",
columnActions: "欄位操作",
localFilter: "本機值篩選",
localFilterFor: "本機值篩選「{column}」",
@@ -36,6 +36,19 @@ describe("normalizeColumnFormatter", () => {
expect(normalizeColumnFormatter({ ...base, filter: { column: "dict_type", mode: "between", value: "A" } })).toBeUndefined();
expect(normalizeColumnFormatter({ ...base, filter: { column: "dict_type", mode: "raw-sql", value: "1=1" } })).toBeUndefined();
});
it.each(["is-blank", "is-not-blank"] as const)("preserves the value-less %s manual reference filter", (mode) => {
expect(
normalizeColumnFormatter({
kind: "foreign-key-display",
referenceMode: "manual",
refTable: "dictionary",
refColumn: "dict_key",
displayColumn: "dict_value",
filter: { column: "dict_type", mode },
}),
).toMatchObject({ filter: { column: "dict_type", mode, value: undefined, endValue: undefined } });
});
});
describe("normalizeSupportedDateTimePattern", () => {
@@ -4,7 +4,7 @@ import type { DataGridContextFilterMode } from "@/lib/dataGrid/dataGridSql";
describe("VictoriaMetrics data grid filters", () => {
it("does not expose SQL filter modes for MetricsQL results", () => {
const modes: DataGridContextFilterMode[] = ["equals", "not-equals", "is-null", "is-not-null", "like", "not-like", "less-than", "greater-than", "in", "not-in", "between", "not-between"];
const modes: DataGridContextFilterMode[] = ["equals", "not-equals", "is-null", "is-not-null", "is-blank", "is-not-blank", "like", "not-like", "less-than", "greater-than", "in", "not-in", "between", "not-between"];
expect(modes.every((mode) => !filterModeIsSupportedForDatabase(mode, "victoriametrics"))).toBe(true);
expect(filterModeIsSupportedForDatabase("equals", "postgres")).toBe(true);
@@ -190,7 +190,7 @@ export function normalizeColumnFormatter(value: unknown): ColumnFormatterConfig
return undefined;
}
const FOREIGN_KEY_DISPLAY_FILTER_MODES = new Set<DataGridContextFilterMode>(["equals", "not-equals", "is-null", "is-not-null", "like", "not-like", "less-than", "less-than-or-equal", "greater-than", "greater-than-or-equal", "in", "not-in", "between", "not-between"]);
const FOREIGN_KEY_DISPLAY_FILTER_MODES = new Set<DataGridContextFilterMode>(["equals", "not-equals", "is-null", "is-not-null", "is-blank", "is-not-blank", "like", "not-like", "less-than", "less-than-or-equal", "greater-than", "greater-than-or-equal", "in", "not-in", "between", "not-between"]);
function normalizeForeignKeyDisplayFilter(value: unknown): ForeignKeyDisplayFilterConfig | undefined {
if (!value || typeof value !== "object") return undefined;
@@ -200,7 +200,7 @@ function normalizeForeignKeyDisplayFilter(value: unknown): ForeignKeyDisplayFilt
const mode = filter.mode as DataGridContextFilterMode;
const rawValue = typeof filter.value === "string" ? filter.value.trim() : "";
const rawEndValue = typeof filter.endValue === "string" ? filter.endValue.trim() : "";
if (mode !== "is-null" && mode !== "is-not-null" && !rawValue) return undefined;
if (mode !== "is-null" && mode !== "is-not-null" && mode !== "is-blank" && mode !== "is-not-blank" && !rawValue) return undefined;
if ((mode === "between" || mode === "not-between") && !rawEndValue) return undefined;
return {
column: filter.column.trim(),
@@ -172,7 +172,7 @@ export function combineWhereInputs(manualWhereInput?: string, structuredWhereInp
}
export function filterModeNeedsValue(mode: DataGridContextFilterMode): boolean {
return mode !== "is-null" && mode !== "is-not-null";
return mode !== "is-null" && mode !== "is-not-null" && mode !== "is-blank" && mode !== "is-not-blank";
}
export function filterModeUsesList(mode: DataGridContextFilterMode): boolean {
+1 -1
View File
@@ -58,7 +58,7 @@ export interface DataGridCopyInsertStatementOptions {
insertMode?: DataGridCopyInsertMode;
}
export type DataGridContextFilterMode = "equals" | "not-equals" | "is-null" | "is-not-null" | "like" | "not-like" | "less-than" | "less-than-or-equal" | "greater-than" | "greater-than-or-equal" | "in" | "not-in" | "between" | "not-between";
export type DataGridContextFilterMode = "equals" | "not-equals" | "is-null" | "is-not-null" | "is-blank" | "is-not-blank" | "like" | "not-like" | "less-than" | "less-than-or-equal" | "greater-than" | "greater-than-or-equal" | "in" | "not-in" | "between" | "not-between";
export interface DataGridContextFilterConditionOptions {
databaseType?: DatabaseType;