feat(grid): resizable column filter panel

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-09-05 01:03:38 +08:00
1 parent 61aa00bf8f
commit ce36a7f40d
3 files changed
+103 -1

No files matched your search

+91 -1
View File
@@ -1071,6 +1071,33 @@ const headerSortMenuOpenColumn = ref<number | null>(null);
const headerPanelDismissGuardUntil = ref(0);
const localFilterSearch = ref("");
const localFilterDraft = ref<LocalColumnFilterDraft | null>(null);
// Default sized to fully render a uuidv4 value (36 chars in text-xs mono) next to the
// checkbox and count columns; persisted per-editor as settings.localFilterPopoverWidth.
const LOCAL_FILTER_POPOVER_DEFAULT_WIDTH = 360;
const LOCAL_FILTER_POPOVER_MIN_WIDTH = 240;
const LOCAL_FILTER_POPOVER_VIEWPORT_PADDING = 16;
function localFilterPopoverMaxWidth() {
if (typeof window === "undefined") return LOCAL_FILTER_POPOVER_DEFAULT_WIDTH;
return Math.max(0, window.innerWidth - 32);
}
function clampLocalFilterPopoverWidth(width: number, maximumWidth = localFilterPopoverMaxWidth()) {
const normalizedWidth = Number.isFinite(width) ? width : LOCAL_FILTER_POPOVER_DEFAULT_WIDTH;
const maxWidth = Math.max(0, Math.min(localFilterPopoverMaxWidth(), maximumWidth));
const minWidth = Math.min(LOCAL_FILTER_POPOVER_MIN_WIDTH, maxWidth);
return Math.round(Math.max(minWidth, Math.min(maxWidth, normalizedWidth)));
}
const localFilterPopoverWidth = ref(clampLocalFilterPopoverWidth(settingsStore.editorSettings.localFilterPopoverWidth));
const localFilterPopoverOffsetX = ref(0);
const isResizingLocalFilter = ref(false);
let localFilterResizeDirection: "left" | "right" = "right";
let localFilterResizeStartX = 0;
let localFilterResizeStartWidth = LOCAL_FILTER_POPOVER_DEFAULT_WIDTH;
let localFilterResizeStartOffsetX = 0;
let localFilterResizeStartLeft = 0;
let localFilterResizeStartRight = 0;
const SERVER_COLUMN_FILTER_LIMIT = 1000;
const SERVER_COLUMN_FILTER_DEBOUNCE_MS = 300;
const serverFilterLoading = ref(false);
@@ -1395,6 +1422,7 @@ const canApplyTypedLocalFilterValue = computed(() => {
function openLocalFilter(colIdx: number, requestedMode: LocalFilterMode = "local") {
localFilterSearch.value = "";
localFilterPopoverOffsetX.value = 0;
const mode: LocalFilterMode = requestedMode === "server" && canUseServerColumnFilter.value ? "server" : "local";
const allKeys = mode === "server" ? [] : buildLocalFilterOptions(colIdx).map((option) => option.key);
localFilterDraft.value = {
@@ -1573,12 +1601,64 @@ function handleLocalFilterOpenChange(value: boolean, columnIndex: number) {
}
function closeLocalFilter() {
onLocalFilterResizeEnd();
localFilterOpenColumn.value = null;
localFilterDraft.value = null;
localFilterSearch.value = "";
localFilterPopoverOffsetX.value = 0;
resetServerFilterState();
}
function onLocalFilterResizeStart(event: MouseEvent, direction: "left" | "right") {
event.preventDefault();
isResizingLocalFilter.value = true;
localFilterResizeDirection = direction;
localFilterResizeStartX = event.clientX;
const handle = event.currentTarget as HTMLElement | null;
const popoverRect = handle?.parentElement?.getBoundingClientRect();
const renderedWidth = popoverRect?.width;
localFilterResizeStartWidth = clampLocalFilterPopoverWidth(renderedWidth ?? localFilterPopoverWidth.value);
localFilterResizeStartOffsetX = localFilterPopoverOffsetX.value;
localFilterResizeStartLeft = popoverRect?.left ?? LOCAL_FILTER_POPOVER_VIEWPORT_PADDING;
localFilterResizeStartRight = popoverRect?.right ?? localFilterResizeStartWidth;
localFilterPopoverWidth.value = localFilterResizeStartWidth;
document.body.classList.add("select-none", "cursor-col-resize");
window.addEventListener("mousemove", onLocalFilterResizeMove);
window.addEventListener("mouseup", onLocalFilterResizeEnd);
}
function onLocalFilterResizeMove(event: MouseEvent) {
if (!isResizingLocalFilter.value) return;
const deltaX = event.clientX - localFilterResizeStartX;
if (localFilterResizeDirection === "right") {
const maxWidth = typeof window === "undefined" ? LOCAL_FILTER_POPOVER_DEFAULT_WIDTH : window.innerWidth - LOCAL_FILTER_POPOVER_VIEWPORT_PADDING - localFilterResizeStartLeft;
localFilterPopoverWidth.value = clampLocalFilterPopoverWidth(localFilterResizeStartWidth + deltaX, maxWidth);
return;
}
const maxWidth = localFilterResizeStartRight - LOCAL_FILTER_POPOVER_VIEWPORT_PADDING;
const nextWidth = clampLocalFilterPopoverWidth(localFilterResizeStartWidth - deltaX, maxWidth);
localFilterPopoverWidth.value = nextWidth;
localFilterPopoverOffsetX.value = localFilterResizeStartOffsetX + localFilterResizeStartWidth - nextWidth;
}
function onLocalFilterResizeEnd() {
if (!isResizingLocalFilter.value) return;
isResizingLocalFilter.value = false;
settingsStore.updateEditorSettings({
localFilterPopoverWidth: localFilterPopoverWidth.value,
});
document.body.classList.remove("select-none", "cursor-col-resize");
window.removeEventListener("mousemove", onLocalFilterResizeMove);
window.removeEventListener("mouseup", onLocalFilterResizeEnd);
}
watch(
() => settingsStore.editorSettings.localFilterPopoverWidth,
(width) => {
if (!isResizingLocalFilter.value) localFilterPopoverWidth.value = clampLocalFilterPopoverWidth(width);
},
);
function formatterKeysForColumn(columnIndex: number): string[] {
const resultColumn = props.result.columns[columnIndex];
if (!props.connectionId || !resultColumn) return [];
@@ -8153,6 +8233,7 @@ onMounted(() => {
});
onDeactivated(pauseCanvasGridWork);
onUnmounted(() => {
onLocalFilterResizeEnd();
dataGridRuntimeScope.dispose();
foreignKeyDisplayRequests.dispose();
clearCellFormatCache();
@@ -13636,7 +13717,16 @@ function openGridSnapshot() {
<Filter class="h-3.5 w-3.5" />
</button>
</PopoverTrigger>
<PopoverContent align="start" side="bottom" class="w-[300px] max-w-[calc(100vw-2rem)] gap-0 overflow-hidden rounded-md border bg-popover p-0 text-popover-foreground shadow-xl" @click.stop @keydown.stop>
<PopoverContent
align="start"
side="bottom"
class="relative max-w-[calc(100vw-2rem)] gap-0 overflow-hidden rounded-md border bg-popover p-0 text-popover-foreground shadow-xl"
:style="{ width: `${localFilterPopoverWidth}px`, marginLeft: `${localFilterPopoverOffsetX}px` }"
@click.stop
@keydown.stop
>
<div role="separator" aria-orientation="vertical" aria-label="Resize filter panel" class="absolute left-0 top-0 z-10 h-full w-1.5 cursor-col-resize hover:bg-primary/30" @mousedown.stop="onLocalFilterResizeStart($event, 'left')" />
<div role="separator" aria-orientation="vertical" aria-label="Resize filter panel" class="absolute right-0 top-0 z-10 h-full w-1.5 cursor-col-resize hover:bg-primary/30" @mousedown.stop="onLocalFilterResizeStart($event, 'right')" />
<div class="border-b bg-muted/40 px-2 py-1.5 text-center text-xs font-semibold">
{{ columnFilterPanelTitle(col.name) }}
</div>
@@ -60,6 +60,14 @@ describe("normalizeEditorSettings", () => {
expect(normalizeEditorSettings({ dataGridTextFilterPanelHeight: 900 }).dataGridTextFilterPanelHeight).toBe(420);
});
it("defaults and bounds the persisted local filter popover width", () => {
expect(normalizeEditorSettings({}).localFilterPopoverWidth).toBe(360);
expect(normalizeEditorSettings({ localFilterPopoverWidth: 412.6 }).localFilterPopoverWidth).toBe(413);
expect(normalizeEditorSettings({ localFilterPopoverWidth: 120 }).localFilterPopoverWidth).toBe(240);
expect(normalizeEditorSettings({ localFilterPopoverWidth: 5000 }).localFilterPopoverWidth).toBe(900);
expect(normalizeEditorSettings({ localFilterPopoverWidth: "wide" }).localFilterPopoverWidth).toBe(360);
});
it("keeps data type colors disabled by default and preserves an explicit opt-in", () => {
expect(normalizeEditorSettings({}).colorizeDataGridCellTypes).toBe(false);
expect(normalizeEditorSettings({ colorizeDataGridCellTypes: true }).colorizeDataGridCellTypes).toBe(true);
+4
View File
@@ -741,6 +741,7 @@ export interface EditorSettings {
dataGridQuickEntry: boolean;
dataGridFilterEditorView: DataGridFilterEditorView;
dataGridTextFilterPanelHeight: number;
localFilterPopoverWidth: number;
dataGridRenderMode: DataGridRenderMode;
dataGridSearchMode: DataGridSearchMode;
dataGridCopyExtractor: DataGridCopyPreference;
@@ -968,6 +969,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
dataGridQuickEntry: false,
dataGridFilterEditorView: "quick",
dataGridTextFilterPanelHeight: DATA_GRID_TEXT_FILTER_PANEL_HEIGHT_DEFAULT,
localFilterPopoverWidth: 360,
dataGridRenderMode: "canvas",
dataGridSearchMode: "filter",
dataGridCopyExtractor: "smart",
@@ -1411,6 +1413,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
dataGridQuickEntry: settings.dataGridQuickEntry ?? DEFAULT_EDITOR_SETTINGS.dataGridQuickEntry,
dataGridFilterEditorView: normalizeDataGridFilterEditorView(settings.dataGridFilterEditorView),
dataGridTextFilterPanelHeight: normalizeDataGridTextFilterPanelHeight(settings.dataGridTextFilterPanelHeight),
localFilterPopoverWidth: normalizeDrawerWidth(settings.localFilterPopoverWidth, 240, DEFAULT_EDITOR_SETTINGS.localFilterPopoverWidth),
dataGridRenderMode: normalizeDataGridRenderMode(settings.dataGridRenderMode),
dataGridSearchMode: normalizeDataGridSearchMode(settings.dataGridSearchMode),
dataGridCopyExtractor: normalizeDataGridCopyPreference(settings.dataGridCopyExtractor),
@@ -2123,6 +2126,7 @@ export const useSettingsStore = defineStore("settings", () => {
if (partial.dataGridQuickEntry !== undefined) editorSettings.value.dataGridQuickEntry = partial.dataGridQuickEntry;
if (partial.dataGridFilterEditorView !== undefined) editorSettings.value.dataGridFilterEditorView = normalizeDataGridFilterEditorView(partial.dataGridFilterEditorView);
if (partial.dataGridTextFilterPanelHeight !== undefined) editorSettings.value.dataGridTextFilterPanelHeight = normalizeDataGridTextFilterPanelHeight(partial.dataGridTextFilterPanelHeight);
if (partial.localFilterPopoverWidth !== undefined) editorSettings.value.localFilterPopoverWidth = normalizeDrawerWidth(partial.localFilterPopoverWidth, 240, DEFAULT_EDITOR_SETTINGS.localFilterPopoverWidth);
if (partial.dataGridRenderMode !== undefined) editorSettings.value.dataGridRenderMode = normalizeDataGridRenderMode(partial.dataGridRenderMode);
if (partial.dataGridSearchMode !== undefined) editorSettings.value.dataGridSearchMode = normalizeDataGridSearchMode(partial.dataGridSearchMode);
if (partial.dataGridCopyExtractor !== undefined) editorSettings.value.dataGridCopyExtractor = normalizeDataGridCopyPreference(partial.dataGridCopyExtractor);