diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 4f784334f..02822a996 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -111,6 +111,7 @@ import { isCancelSearchShortcut, isFocusSearchShortcut } from "@/lib/keyboardSho import { dataGridHeaderContentWidth, scrollbarGutterWidth } from "@/lib/dataGridScrollGutter"; import { dataGridSaveActionMode, dataGridSaveToolbarState } from "@/lib/dataGridSaveUi"; import { appendColumnValueFilterCondition, buildColumnValueFilterCondition } from "@/lib/dataGridColumnFilter"; +import { clampSearchSplitWidth } from "@/lib/dataGridSearchSplit"; import { MAX_RESULT_PAGE_SIZE, MIN_RESULT_PAGE_SIZE, @@ -312,6 +313,18 @@ const orderByInput = ref(""); const hasOrderByInput = computed(() => orderByInput.value.trim().length > 0); const whereFilterInput = ref(props.initialWhereInput ?? ""); const hasWhereFilterInput = computed(() => whereFilterInput.value.trim().length > 0); +const searchSplitContainerRef = ref(); +const searchSplitWhereWidth = ref(null); +const isResizingSearchSplit = ref(false); +let searchSplitStartX = 0; +let searchSplitStartWidth = 0; + +const whereSearchPaneStyle = computed(() => { + if (searchSplitWhereWidth.value == null) return {}; + return { + flex: `0 0 ${searchSplitWhereWidth.value}px`, + }; +}); type LocalColumnFilterDraft = { columnIndex: number; @@ -2578,6 +2591,48 @@ function toggleDdlWrap() { ddlWrap.value = !ddlWrap.value; } +function searchSplitContainerWidth(): number { + return searchSplitContainerRef.value?.getBoundingClientRect().width ?? 0; +} + +function onSearchSplitResizeStart(event: MouseEvent) { + const containerWidth = searchSplitContainerWidth(); + if (containerWidth <= 0) return; + event.preventDefault(); + isResizingSearchSplit.value = true; + searchSplitStartX = event.clientX; + searchSplitStartWidth = clampSearchSplitWidth({ + containerWidth, + desiredWidth: searchSplitWhereWidth.value ?? undefined, + }); + searchSplitWhereWidth.value = searchSplitStartWidth; + document.body.classList.add("select-none", "cursor-col-resize"); + window.addEventListener("mousemove", onSearchSplitResizeMove); + window.addEventListener("mouseup", onSearchSplitResizeEnd); +} + +function onSearchSplitResizeMove(event: MouseEvent) { + if (!isResizingSearchSplit.value) return; + const containerWidth = searchSplitContainerWidth(); + if (containerWidth <= 0) return; + searchSplitWhereWidth.value = clampSearchSplitWidth({ + containerWidth, + desiredWidth: searchSplitStartWidth + event.clientX - searchSplitStartX, + }); +} + +function onSearchSplitResizeEnd() { + isResizingSearchSplit.value = false; + document.body.classList.remove("select-none", "cursor-col-resize"); + window.removeEventListener("mousemove", onSearchSplitResizeMove); + window.removeEventListener("mouseup", onSearchSplitResizeEnd); +} + +function resetSearchSplitWidth() { + const containerWidth = searchSplitContainerWidth(); + searchSplitWhereWidth.value = containerWidth > 0 ? clampSearchSplitWidth({ containerWidth }) : null; +} + function onDdlResizeStart(event: MouseEvent) { isResizingDdl.value = true; ddlResizeStartX = event.clientX; @@ -2642,6 +2697,7 @@ watch( onUnmounted(() => { cleanupFrames(); + onSearchSplitResizeEnd(); onDdlResizeEnd(); onDetailResizeEnd(); finishCellSelection(); @@ -2752,109 +2808,125 @@ defineExpose({ diff --git a/apps/desktop/src/lib/dataGridSearchSplit.ts b/apps/desktop/src/lib/dataGridSearchSplit.ts new file mode 100644 index 000000000..3236eaa5e --- /dev/null +++ b/apps/desktop/src/lib/dataGridSearchSplit.ts @@ -0,0 +1,17 @@ +export const DEFAULT_SEARCH_SPLIT_RATIO = 0.5; +export const MIN_SEARCH_SPLIT_PANE_WIDTH = 180; + +export function clampSearchSplitWidth({ + containerWidth, + desiredWidth, +}: { + containerWidth: number; + desiredWidth?: number; +}): number { + const fallback = containerWidth * DEFAULT_SEARCH_SPLIT_RATIO; + if (containerWidth <= MIN_SEARCH_SPLIT_PANE_WIDTH * 2) return fallback; + + const min = MIN_SEARCH_SPLIT_PANE_WIDTH; + const max = containerWidth - MIN_SEARCH_SPLIT_PANE_WIDTH; + return Math.min(Math.max(desiredWidth ?? fallback, min), max); +} diff --git a/packages/app-tests/dataGridSearchSplit.test.ts b/packages/app-tests/dataGridSearchSplit.test.ts new file mode 100644 index 000000000..a3cdc5bfd --- /dev/null +++ b/packages/app-tests/dataGridSearchSplit.test.ts @@ -0,0 +1,20 @@ +import { strict as assert } from "node:assert"; +import test from "node:test"; +import { + clampSearchSplitWidth, + DEFAULT_SEARCH_SPLIT_RATIO, + MIN_SEARCH_SPLIT_PANE_WIDTH, +} from "../../apps/desktop/src/lib/dataGridSearchSplit.ts"; + +test("defaults the search split to an even width", () => { + assert.equal(clampSearchSplitWidth({ containerWidth: 800 }), 400); +}); + +test("clamps the search split to keep both panes usable", () => { + assert.equal(clampSearchSplitWidth({ containerWidth: 800, desiredWidth: 80 }), MIN_SEARCH_SPLIT_PANE_WIDTH); + assert.equal(clampSearchSplitWidth({ containerWidth: 800, desiredWidth: 700 }), 800 - MIN_SEARCH_SPLIT_PANE_WIDTH); +}); + +test("keeps the search split centered when the toolbar is narrow", () => { + assert.equal(clampSearchSplitWidth({ containerWidth: 300, desiredWidth: 40 }), 300 * DEFAULT_SEARCH_SPLIT_RATIO); +});