From ce36a7f40dec2b3aa25b0c606aac086b2af834dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E4=B8=AB=E8=AE=B2=E6=A2=B5?= Date: Sat, 5 Sep 2026 01:03:38 +0800 Subject: [PATCH] feat(grid): resizable column filter panel --- apps/desktop/src/components/grid/DataGrid.vue | 92 ++++++++++++++++++- .../stores/__tests__/settingsStore.spec.ts | 8 ++ apps/desktop/src/stores/settingsStore.ts | 4 + 3 files changed, 103 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 2eb2ac2222..5da7b2da21 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -1071,6 +1071,33 @@ const headerSortMenuOpenColumn = ref(null); const headerPanelDismissGuardUntil = ref(0); const localFilterSearch = ref(""); const localFilterDraft = ref(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() { - + +