feat(grid): support searching filter columns

This commit is contained in:
zipg 2026-07-07 16:19:29 +08:00 committed by GitHub
parent d50f1761d6
commit 3497df033f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 70 additions and 6 deletions

View File

@ -82,6 +82,7 @@ const queryStore = useQueryStore();
const { openTableTarget } = useNavigationTargets({
showFieldLineageDialog: ref(false),
showDatabaseSearchDialog: ref(false),
showDiagramDialog: ref(false),
});
const { toast } = useToast();
const { isDark } = useTheme();

View File

@ -1080,6 +1080,7 @@ const serverFilterValueByKey = ref<Map<string, CellValue>>(new Map());
let serverFilterRequestId = 0;
let serverFilterSearchTimer: ReturnType<typeof window.setTimeout> | undefined;
const filterBuilderOpen = ref(false);
const filterBuilderColumnSearch = ref("");
const filterModeOptions: Array<{ value: FilterMode; labelKey: string }> = [
{ value: "equals", labelKey: "grid.filterBuilderEquals" },
{ value: "not-equals", labelKey: "grid.filterBuilderNotEquals" },
@ -1092,6 +1093,12 @@ const filterModeOptions: Array<{ value: FilterMode; labelKey: string }> = [
];
const filterBuilderColumns = computed(() => props.tableMeta?.columns ?? []);
const filterBuilderColumnOptions = computed(() => filterBuilderColumns.value.map((column) => column.name));
const filteredFilterBuilderColumnOptions = computed(() => {
const query = filterBuilderColumnSearch.value.trim().toLowerCase();
if (!query) return filterBuilderColumnOptions.value;
return filterBuilderColumnOptions.value.filter((columnName) => columnName.toLowerCase().includes(query));
});
const filterBuilderColumnSearchNavigationKeys = new Set(["Escape", "Tab", "ArrowUp", "ArrowDown", "Home", "End", "PageUp", "PageDown"]);
const structuredFilterCacheKey = computed(() => props.cacheKey || [props.connectionId ?? "", props.database ?? "", props.context ?? "", props.tableMeta?.schema ?? "", props.tableMeta?.tableName ?? ""].join("\u0001"));
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<StructuredFilterRule[]>([]);
@ -1815,6 +1822,24 @@ function updateStructuredFilterRule(ruleId: string, patch: Partial<StructuredFil
});
}
function updateStructuredFilterRuleColumn(ruleId: string, columnName: string) {
updateStructuredFilterRule(ruleId, { columnName });
filterBuilderColumnSearch.value = "";
}
function handleFilterBuilderColumnSearchKeydown(event: KeyboardEvent) {
if (event.isComposing || event.key === "Process") {
event.stopPropagation();
return;
}
if (filterBuilderColumnSearchNavigationKeys.has(event.key)) return;
if (event.ctrlKey || event.metaKey || event.altKey) return;
if (event.key.length === 1 || event.key === "Backspace" || event.key === "Delete") {
// Let text editing stay in the search box without breaking Select navigation keys.
event.stopPropagation();
}
}
function resetStructuredFilters() {
appliedStructuredWhereInput.value = "";
structuredFilterRules.value = filterBuilderColumnOptions.value.length > 0 ? [defaultStructuredFilterRule()] : [];
@ -1871,6 +1896,10 @@ async function applyStructuredFilters() {
watch([structuredFilterCacheKey, structuredFilterScopeKey], loadStructuredFilterStateForScope, { immediate: true });
watch(filterBuilderOpen, (open) => {
if (!open) filterBuilderColumnSearch.value = "";
});
watch(
[structuredFilterRules, appliedStructuredWhereInput],
() => {
@ -8243,14 +8272,32 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
</Button>
</div>
<div class="grid grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)_minmax(0,1.2fr)_auto] items-center gap-2">
<Select :model-value="rule.columnName" :disabled="rule.disabled" :class="rule.disabled ? 'opacity-45' : ''" @update:model-value="(value: any) => updateStructuredFilterRule(rule.id, { columnName: String(value) })">
<Select :model-value="rule.columnName" :disabled="rule.disabled" :class="rule.disabled ? 'opacity-45' : ''" @update:model-value="(value: any) => updateStructuredFilterRuleColumn(rule.id, String(value))">
<SelectTrigger class="h-8 w-full min-w-0 overflow-hidden text-xs [&_[data-slot=select-value]]:min-w-0 [&_[data-slot=select-value]]:truncate">
<SelectValue :placeholder="t('grid.filterBuilderColumn')" />
</SelectTrigger>
<SelectContent position="popper">
<SelectItem v-for="columnName in filterBuilderColumnOptions" :key="columnName" :value="columnName">
<SelectContent position="popper" class="max-h-72" :hide-scroll-buttons="true">
<SelectItem v-for="columnName in filteredFilterBuilderColumnOptions" :key="columnName" :value="columnName">
{{ columnName }}
</SelectItem>
<div v-if="filteredFilterBuilderColumnOptions.length === 0" class="px-2 py-2 text-xs text-muted-foreground">
{{ t("grid.filterBuilderNoMatchingColumns") }}
</div>
<div class="sticky bottom-0 mt-1 flex items-center gap-1.5 border-t bg-popover px-2 py-1.5">
<Search class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<input
v-model="filterBuilderColumnSearch"
autocapitalize="off"
autocomplete="off"
autocorrect="off"
spellcheck="false"
class="h-7 min-w-0 flex-1 bg-transparent text-xs outline-none placeholder:text-muted-foreground"
:placeholder="t('grid.filterBuilderSearchColumns')"
@click.stop
@keydown="handleFilterBuilderColumnSearchKeydown"
@pointerdown.stop
/>
</div>
</SelectContent>
</Select>

View File

@ -16,17 +16,19 @@ const props = withDefaults(
class?: HTMLAttributes["class"];
disablePortal?: boolean;
disableOutsidePointerEvents?: boolean;
hideScrollButtons?: boolean;
}
>(),
{
position: "popper",
disablePortal: false,
disableOutsidePointerEvents: true,
hideScrollButtons: false,
},
);
const emits = defineEmits<SelectContentEmits>();
const delegatedProps = reactiveOmit(props, "class", "disablePortal");
const delegatedProps = reactiveOmit(props, "class", "disablePortal", "hideScrollButtons");
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
@ -45,11 +47,11 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
)
"
>
<SelectScrollUpButton />
<SelectScrollUpButton v-if="!hideScrollButtons" />
<SelectViewport :data-position="position" :class="cn('data-[position=popper]:h-[var(--reka-select-trigger-height)] data-[position=popper]:w-full')">
<slot />
</SelectViewport>
<SelectScrollDownButton />
<SelectScrollDownButton v-if="!hideScrollButtons" />
</SelectContent>
</SelectPortal>
</template>

View File

@ -719,6 +719,8 @@ export default {
filterBuilderSummary: "{count} rules",
filterBuilderAddRule: "Add rule",
filterBuilderColumn: "Column",
filterBuilderSearchColumns: "Search columns...",
filterBuilderNoMatchingColumns: "No matching columns",
filterBuilderValue: "Value",
filterBuilderNoValue: "No value needed",
filterBuilderEmpty: "Add a rule to start filtering rows.",

View File

@ -919,6 +919,8 @@ export default withEnglishFallback({
},
sortUnsupported: "Este SQL no admite ordenamiento del resultado completo. Intenta con una consulta SELECT simple.",
truncatedHint: "Los resultados están limitados a 10.000 filas. Usa LIMIT/OFFSET en tu consulta para paginar.",
filterBuilderSearchColumns: "Buscar columnas...",
filterBuilderNoMatchingColumns: "Sin columnas coincidentes",
},
exportProgress: {
title: "Exportando datos de la tabla",

View File

@ -917,6 +917,8 @@ export default withEnglishFallback({
},
sortUnsupported: "Questo SQL non supporta l'ordinamento sull'intero risultato. Riprova con una query SELECT semplice.",
truncatedHint: "Risultati troncati a {count} righe. Usa la paginazione a piè di pagina o regola le righe per pagina.",
filterBuilderSearchColumns: "Cerca campi...",
filterBuilderNoMatchingColumns: "Nessun campo corrispondente",
},
exportProgress: {
title: "Esportazione Dati Tabella",

View File

@ -917,6 +917,8 @@ export default withEnglishFallback({
renderMode: "レンダーモード",
undoChange: "変更を元に戻す",
redoChange: "変更をやり直す",
filterBuilderSearchColumns: "フィールドを検索...",
filterBuilderNoMatchingColumns: "一致するフィールドがありません",
},
exportProgress: {
title: "テーブルデータをエクスポート中",

View File

@ -918,6 +918,8 @@ export default withEnglishFallback({
},
sortUnsupported: "Este SQL não suporta a ordenação do resultado completo. Tente novamente com uma única consulta SELECT.",
truncatedHint: "Resultados truncados em {count} linhas. Use a paginação no rodapé ou ajuste as linhas por página.",
filterBuilderSearchColumns: "Pesquisar campos...",
filterBuilderNoMatchingColumns: "Nenhum campo correspondente",
},
exportProgress: {
title: "Exportando Dados da Tabela",

View File

@ -721,6 +721,8 @@ export default withEnglishFallback({
filterBuilderSummary: "{count} 条规则",
filterBuilderAddRule: "新增条件",
filterBuilderColumn: "字段",
filterBuilderSearchColumns: "搜索字段...",
filterBuilderNoMatchingColumns: "没有匹配的字段",
filterBuilderValue: "值",
filterBuilderNoValue: "无需输入值",
filterBuilderEmpty: "新增一条条件后即可开始筛选。",

View File

@ -918,6 +918,8 @@ export default withEnglishFallback({
},
sortUnsupported: "目前 SQL 不支援完整排序,請改為單條 SELECT 查詢後再嘗試。",
truncatedHint: "結果已截斷,僅顯示前 {count} 行。可經由底部分頁繼續載入,或調整每頁行數。",
filterBuilderSearchColumns: "搜尋欄位...",
filterBuilderNoMatchingColumns: "沒有匹配的欄位",
},
exportProgress: {
title: "匯出資料表資料",