mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
improve(grid): adapt split toolbar collapse to measured overflow
This commit is contained in:
@@ -386,6 +386,7 @@ import {
|
||||
DATA_GRID_TOOLBAR_ACTION_COLLAPSE_ORDER,
|
||||
dataGridDeleteRowToolbarState,
|
||||
dataGridToolbarActionCollapseCount,
|
||||
shouldCompactDataGridConditionInput,
|
||||
isDataGridToolbarCompact,
|
||||
type DataGridReloadIntent,
|
||||
type DataGridToolbarActionCapability,
|
||||
@@ -832,6 +833,7 @@ const dataGridViewportWidth = ref(0);
|
||||
const dataGridTopbarOverflowCompact = ref(false);
|
||||
const dataGridTopbarOverflowActionCount = ref(0);
|
||||
const dataGridTopbarExpandedRequiredWidth = ref(0);
|
||||
const dataGridFilterExpandedRequiredWidth = ref(0);
|
||||
const showColumnCommentsInHeader = computed(() => settingsStore.editorSettings.showColumnCommentsInHeader);
|
||||
const showColumnTypesInHeader = computed(() => settingsStore.editorSettings.showColumnTypesInHeader);
|
||||
const showColumnHeaderTooltips = computed(() => settingsStore.editorSettings.showColumnHeaderTooltips !== false);
|
||||
@@ -853,10 +855,15 @@ const compactColumnHeaderActions = computed(() => settingsStore.editorSettings.c
|
||||
const dataGridRenderMode = computed(() => settingsStore.editorSettings.dataGridRenderMode);
|
||||
const dataGridSearchMode = computed(() => settingsStore.editorSettings.dataGridSearchMode);
|
||||
const dataGridRowNumberMode = computed(() => settingsStore.editorSettings.dataGridRowNumberMode);
|
||||
const compactDataGridToolbar = computed(() => dataGridTopbarOverflowCompact.value || isDataGridToolbarCompact(dataGridTopbarWidth.value, dataGridViewportWidth.value, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH));
|
||||
const splitDataGridToolbar = computed(() => settingsStore.editorSettings.dataGridToolbarLayout === "split");
|
||||
const responsiveDataGridToolbarActionCount = computed(() => dataGridToolbarActionCollapseCount(dataGridTopbarWidth.value, dataGridViewportWidth.value, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH));
|
||||
const compactDataGridToolbar = computed(() => dataGridTopbarOverflowCompact.value || (!splitDataGridToolbar.value && isDataGridToolbarCompact(dataGridTopbarWidth.value, dataGridViewportWidth.value, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH)));
|
||||
const responsiveDataGridToolbarActionCount = computed(() => (splitDataGridToolbar.value ? 0 : dataGridToolbarActionCollapseCount(dataGridTopbarWidth.value, dataGridViewportWidth.value, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH)));
|
||||
const compactDataGridToolbarActionCount = computed(() => Math.max(responsiveDataGridToolbarActionCount.value, dataGridTopbarOverflowActionCount.value));
|
||||
const compactDataGridToolbarActions = computed(() => (splitDataGridToolbar.value ? compactDataGridToolbarActionCount.value > 0 : compactDataGridToolbar.value));
|
||||
watch(splitDataGridToolbar, () => {
|
||||
resetDataGridTopbarOverflowCompact();
|
||||
nextTick(updateDataGridTopbarWidth);
|
||||
});
|
||||
const infiniteScrollEnabled = computed(() => props.paginationEnabled && settingsStore.editorSettings.infiniteScroll);
|
||||
const queryResultMaxRows = computed(() => effectiveQueryResultMaxRows(settingsStore.editorSettings.queryResultMaxRowsEnabled, settingsStore.editorSettings.queryResultMaxRows));
|
||||
const paginationMaxRows = computed(() => (isResultsContext.value ? queryResultMaxRows.value : undefined));
|
||||
@@ -3107,6 +3114,50 @@ function updateDataGridTopbarWidth() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (splitDataGridToolbar.value) {
|
||||
// Split rows each have the whole pane: the single-row breakpoint reserves
|
||||
// space for controls that no longer share their row. Measure them separately.
|
||||
const filters = currentTopbar.querySelector<HTMLElement>('[data-grid-topbar-row="filters"]');
|
||||
const filterWidth = filters?.clientWidth ?? 0;
|
||||
const conditionEditors = filters?.querySelectorAll<HTMLElement>("[data-grid-condition-editor]") ?? [];
|
||||
let conditionInputsNeedCompact = false;
|
||||
for (const editor of conditionEditors) {
|
||||
const input = editor.querySelector<HTMLElement>("[data-grid-condition-input-viewport]");
|
||||
if (!input) continue;
|
||||
// Input widths change when the WHERE/ORDER BY divider moves even when
|
||||
// the whole toolbar keeps the same width. Observe their own viewports.
|
||||
dataGridTopbarResizeObserver?.observe(input);
|
||||
const label = editor.querySelector<HTMLElement>(".data-grid-topbar-condition-label");
|
||||
const hiddenLabelWidth = label ? Math.max(0, label.scrollWidth - label.clientWidth) : 0;
|
||||
conditionInputsNeedCompact ||= shouldCompactDataGridConditionInput(input.clientWidth, hiddenLabelWidth, dataGridTopbarOverflowCompact.value);
|
||||
}
|
||||
const filtersOverflow = filterWidth > 0 && !!filters && filters.scrollWidth > filterWidth + 1;
|
||||
let changed = false;
|
||||
if (dataGridTopbarOverflowCompact.value && filterWidth >= dataGridFilterExpandedRequiredWidth.value && !conditionInputsNeedCompact) {
|
||||
dataGridTopbarOverflowCompact.value = false;
|
||||
dataGridFilterExpandedRequiredWidth.value = 0;
|
||||
changed = true;
|
||||
} else if (!dataGridTopbarOverflowCompact.value && filterWidth > 0 && (filtersOverflow || conditionInputsNeedCompact)) {
|
||||
dataGridFilterExpandedRequiredWidth.value = filtersOverflow ? filters!.scrollWidth + DATA_GRID_TOPBAR_EXPAND_HYSTERESIS_PX : 0;
|
||||
dataGridTopbarOverflowCompact.value = true;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
const actionRows = currentTopbar.querySelectorAll<HTMLElement>(':scope > [data-grid-topbar-row="actions"]');
|
||||
const actionsWidth = Array.from(actionRows).reduce((width, row) => width + Math.max(row.clientWidth, row.scrollWidth), 0);
|
||||
if (dataGridTopbarOverflowActionCount.value > 0 && dataGridTopbarWidth.value >= dataGridTopbarExpandedRequiredWidth.value) {
|
||||
dataGridTopbarOverflowActionCount.value = 0;
|
||||
dataGridTopbarExpandedRequiredWidth.value = 0;
|
||||
changed = true;
|
||||
} else if (dataGridTopbarWidth.value > 0 && actionsWidth > dataGridTopbarWidth.value + 1 && dataGridTopbarOverflowActionCount.value < DATA_GRID_TOOLBAR_ACTION_COLLAPSE_ORDER.length) {
|
||||
dataGridTopbarExpandedRequiredWidth.value = Math.max(dataGridTopbarExpandedRequiredWidth.value, actionsWidth + DATA_GRID_TOPBAR_EXPAND_HYSTERESIS_PX);
|
||||
dataGridTopbarOverflowActionCount.value += 1;
|
||||
changed = true;
|
||||
}
|
||||
if (changed) scheduleDataGridTopbarRecheck();
|
||||
return;
|
||||
}
|
||||
|
||||
if (dataGridTopbarOverflowCompact.value || dataGridTopbarOverflowActionCount.value > 0) {
|
||||
if (dataGridTopbarWidth.value >= dataGridTopbarExpandedRequiredWidth.value) {
|
||||
dataGridTopbarOverflowCompact.value = false;
|
||||
@@ -3139,6 +3190,7 @@ function resetDataGridTopbarOverflowCompact() {
|
||||
dataGridTopbarOverflowCompact.value = false;
|
||||
dataGridTopbarOverflowActionCount.value = 0;
|
||||
dataGridTopbarExpandedRequiredWidth.value = 0;
|
||||
dataGridFilterExpandedRequiredWidth.value = 0;
|
||||
scheduleDataGridTopbarRecheck();
|
||||
}
|
||||
|
||||
@@ -13921,7 +13973,7 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
|
||||
:class="splitDataGridToolbar ? 'grid h-16 grid-cols-[auto_minmax(0,1fr)] grid-rows-2' : 'flex h-8 items-center'"
|
||||
>
|
||||
<div v-if="hasResultToolbarLeadingSlot" data-grid-topbar-row="actions" class="flex shrink-0 items-center border-r" :class="splitDataGridToolbar ? 'col-start-1 row-start-1' : ''">
|
||||
<slot name="result-toolbar-leading" :compact="compactDataGridToolbar" />
|
||||
<slot name="result-toolbar-leading" :compact="compactDataGridToolbarActions" />
|
||||
</div>
|
||||
<!-- Clip both axes instead of creating a hidden scroll container around the toolbar controls. -->
|
||||
<div data-grid-topbar-row="filters" class="data-grid-topbar-scroll min-w-0 overflow-clip" :class="splitDataGridToolbar ? 'data-grid-topbar-scroll--row-divider col-span-2 row-start-2' : 'flex-1'">
|
||||
@@ -14035,12 +14087,12 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
|
||||
@wheel="onDataGridTopbarFixedActionWheel"
|
||||
>
|
||||
<template #leading>
|
||||
<slot v-if="hasResultToolbarActionsSlot" name="result-toolbar-actions" :compact="compactDataGridToolbar" />
|
||||
<slot v-if="hasResultToolbarActionsSlot" name="result-toolbar-actions" :compact="compactDataGridToolbarActions" />
|
||||
<DataGridVirtualRowIdentifier
|
||||
v-if="showVirtualRowIdentifierControl"
|
||||
:columns="props.tableMeta!.columns"
|
||||
:selected-columns="virtualRowIdentifierColumns"
|
||||
:compact="compactDataGridToolbar"
|
||||
:compact="compactDataGridToolbarActions"
|
||||
:disabled="virtualRowIdentifierDisabled"
|
||||
@apply="applyVirtualRowIdentifier"
|
||||
@clear="clearVirtualRowIdentifier"
|
||||
@@ -14071,7 +14123,7 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
:class="['data-grid-topbar-action-button h-5 shrink-0 text-xs px-1.5', compactDataGridToolbar ? 'data-grid-topbar-action-button--compact' : '', mongoJsonPreviewOpen ? 'text-primary bg-primary/10 hover:bg-primary/15' : '']"
|
||||
:class="['data-grid-topbar-action-button h-5 shrink-0 text-xs px-1.5', compactDataGridToolbarActions ? 'data-grid-topbar-action-button--compact' : '', mongoJsonPreviewOpen ? 'text-primary bg-primary/10 hover:bg-primary/15' : '']"
|
||||
:aria-pressed="mongoJsonPreviewOpen"
|
||||
@click="toggleMongoJsonPreview"
|
||||
>
|
||||
@@ -14079,7 +14131,7 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
|
||||
<span
|
||||
class="data-grid-topbar-action-label"
|
||||
:class="{
|
||||
'data-grid-topbar-action-label--compact': compactDataGridToolbar,
|
||||
'data-grid-topbar-action-label--compact': compactDataGridToolbarActions,
|
||||
}"
|
||||
>{{ t("grid.mongoJsonPreview") }}</span
|
||||
>
|
||||
|
||||
@@ -642,12 +642,12 @@ defineExpose({ focus, dismiss: editor.dismiss, rememberHistory: editor.rememberH
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="controlRef" class="relative flex min-w-0 flex-1 items-stretch">
|
||||
<div ref="controlRef" :data-grid-condition-editor="props.kind" class="relative flex min-w-0 flex-1 items-stretch">
|
||||
<div class="flex min-w-0 flex-1 items-center gap-1">
|
||||
<span class="data-grid-topbar-condition-label" :class="[props.kind === 'where' ? 'data-grid-topbar-condition-label--where' : 'data-grid-topbar-condition-label--order', { 'data-grid-topbar-condition-label--compact': props.compact }]">
|
||||
{{ props.kind === "where" ? "WHERE" : "ORDER BY" }}
|
||||
</span>
|
||||
<div class="relative h-6 min-w-0 flex-1 overflow-hidden">
|
||||
<div data-grid-condition-input-viewport class="relative h-6 min-w-0 flex-1 overflow-hidden">
|
||||
<div v-if="!composing" aria-hidden="true" class="data-grid-condition-highlight pointer-events-none absolute left-0 top-0" :style="collapsedHighlightStyle">
|
||||
<span v-for="(token, tokenIndex) in highlightTokens" :key="tokenIndex" :class="highlightTokenClass(token.type)">{{ token.text }}</span>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ import type { DataGridDistinctValueSource, DataGridDistinctValueSuggestionState,
|
||||
import type { DataGridLocalFilterOption } from "@/lib/dataGrid/dataGridLocalColumnFilterState";
|
||||
import type { DataGridFilterEditorView } from "@/stores/settingsStore";
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
import { clampSearchSplitWidth } from "@/lib/dataGrid/dataGridSearchSplit";
|
||||
import { clampSearchSplitWidth, MIN_SEARCH_SPLIT_PANE_WIDTH } from "@/lib/dataGrid/dataGridSearchSplit";
|
||||
|
||||
type LocalFilterSummary = {
|
||||
columnIndex: number;
|
||||
@@ -114,7 +114,15 @@ const resizing = ref(false);
|
||||
let resizeStartX = 0;
|
||||
let resizeStartWidth = 0;
|
||||
|
||||
const wherePaneStyle = computed(() => (whereWidth.value == null ? {} : { flex: `0 0 ${whereWidth.value}px` }));
|
||||
const wherePaneStyle = computed(() =>
|
||||
whereWidth.value == null
|
||||
? {}
|
||||
: {
|
||||
// Retain the dragged preference while keeping both panes visible when
|
||||
// their container later shrinks, without a reactive resize loop.
|
||||
flex: `0 0 clamp(min(${MIN_SEARCH_SPLIT_PANE_WIDTH}px, 50%), ${whereWidth.value}px, max(50%, calc(100% - ${MIN_SEARCH_SPLIT_PANE_WIDTH}px)))`,
|
||||
},
|
||||
);
|
||||
|
||||
function containerWidth(): number {
|
||||
return containerRef.value?.getBoundingClientRect().width ?? 0;
|
||||
|
||||
+150
@@ -63,6 +63,7 @@ function mountGrid(
|
||||
databaseType?: DatabaseType;
|
||||
sourceDatabase?: string;
|
||||
joinedWriteTargetCount?: number;
|
||||
filterControls?: boolean;
|
||||
} = {},
|
||||
) {
|
||||
const pinia = createPinia();
|
||||
@@ -101,6 +102,7 @@ function mountGrid(
|
||||
result,
|
||||
databaseType: options.databaseType ?? "mysql",
|
||||
context: options.context ?? "table-data",
|
||||
onExecuteSql: options.filterControls ? async () => undefined : undefined,
|
||||
queryMultiSource: options.queryMultiSource,
|
||||
joinedWriteTargets: options.joinedWriteTargetCount ? Array.from({ length: options.joinedWriteTargetCount }, (_, index) => ({ tableMeta: { tableName: `table_${index}`, columns: [], primaryKeys: [] }, sourceColumns: ["id"] })) : undefined,
|
||||
...(options.withTableMetadata
|
||||
@@ -139,6 +141,58 @@ function gridRoot(host: HTMLElement): HTMLElement {
|
||||
return root;
|
||||
}
|
||||
|
||||
function mockToolbarGeometry(geometry: { width: number; filters: number; actions: number; compactActions?: number; leading?: number; conditionInputs?: { where: number; order: number } }) {
|
||||
const observers: Array<{ callback: () => void; elements: Set<Element> }> = [];
|
||||
vi.stubGlobal(
|
||||
"ResizeObserver",
|
||||
class {
|
||||
elements = new Set<Element>();
|
||||
constructor(callback: () => void) {
|
||||
observers.push({ callback, elements: this.elements });
|
||||
}
|
||||
observe(element: Element) {
|
||||
this.elements.add(element);
|
||||
}
|
||||
unobserve(element: Element) {
|
||||
this.elements.delete(element);
|
||||
}
|
||||
disconnect() {
|
||||
this.elements.clear();
|
||||
}
|
||||
},
|
||||
);
|
||||
const isLeading = (element: HTMLElement) => !!element.querySelector("[data-test-toolbar-leading]");
|
||||
const conditionLabelWidth = (element: HTMLElement) => (element.closest("[data-grid-condition-editor]")?.getAttribute("data-grid-condition-editor") === "where" ? 42 : 67);
|
||||
|
||||
vi.spyOn(HTMLElement.prototype, "clientWidth", "get").mockImplementation(function (this: HTMLElement) {
|
||||
if (this.hasAttribute("data-grid-condition-input-viewport")) {
|
||||
const kind = this.closest("[data-grid-condition-editor]")?.getAttribute("data-grid-condition-editor") === "where" ? "where" : "order";
|
||||
const compact = !!this.closest("[data-grid-condition-editor]")?.querySelector(".data-grid-topbar-condition-label--compact");
|
||||
return (geometry.conditionInputs?.[kind] ?? 250) + (compact ? conditionLabelWidth(this) : 0);
|
||||
}
|
||||
if (this.classList.contains("data-grid-topbar-condition-label")) return this.classList.contains("data-grid-topbar-condition-label--compact") ? 0 : conditionLabelWidth(this);
|
||||
if (this.classList.contains("data-grid-topbar-shell") || this.dataset.gridTopbarRow === "filters") return geometry.width;
|
||||
if (this.dataset.gridTopbarRow === "actions") return isLeading(this) ? (geometry.leading ?? 0) : Math.max(0, geometry.width - (geometry.leading ?? 0));
|
||||
return 0;
|
||||
});
|
||||
vi.spyOn(HTMLElement.prototype, "scrollWidth", "get").mockImplementation(function (this: HTMLElement) {
|
||||
if (this.classList.contains("data-grid-topbar-condition-label")) return conditionLabelWidth(this);
|
||||
if (this.classList.contains("data-grid-topbar-shell")) return geometry.width;
|
||||
if (this.dataset.gridTopbarRow === "filters") return this.querySelector(".data-grid-topbar--compact") ? geometry.width : Math.max(geometry.width, geometry.filters);
|
||||
if (this.dataset.gridTopbarRow === "actions") {
|
||||
if (isLeading(this)) return geometry.leading ?? 0;
|
||||
const actions = this.querySelector(".data-grid-topbar-action-label--compact") ? (geometry.compactActions ?? geometry.actions) : geometry.actions;
|
||||
return Math.max(this.clientWidth, actions);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
return (selector = ".data-grid-topbar-shell") => {
|
||||
for (const observer of observers) {
|
||||
if ([...observer.elements].some((element) => element.matches(selector))) observer.callback();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function goToColumnItem(name: string, position: number): HTMLButtonElement {
|
||||
const button = [...document.querySelectorAll<HTMLButtonElement>("button")].find((candidate) => candidate.textContent?.replaceAll(/\s/g, "") === `${name}#${position}`);
|
||||
if (!button) throw new Error(`Go-to-column item ${name} not found`);
|
||||
@@ -158,6 +212,7 @@ afterEach(() => {
|
||||
app.unmount();
|
||||
host.remove();
|
||||
}
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("DataGrid go-to-column shortcut", () => {
|
||||
@@ -189,6 +244,101 @@ describe("DataGrid go-to-column shortcut", () => {
|
||||
expect(host.querySelector('[data-grid-topbar-row="filters"].data-grid-topbar-scroll--row-divider')).not.toBeNull();
|
||||
});
|
||||
|
||||
it("keeps split toolbar labels expanded below the single-row breakpoint and resets layout history", async () => {
|
||||
mockToolbarGeometry({ width: 800, filters: 600, actions: 600 });
|
||||
const { host, settingsStore } = mountGrid();
|
||||
await settle();
|
||||
const refresh = () => host.querySelector<HTMLElement>('[data-toolbar-action="refresh"] .data-grid-topbar-action-label');
|
||||
await vi.waitFor(() => expect(refresh()?.classList.contains("data-grid-topbar-action-label--compact")).toBe(true));
|
||||
|
||||
settingsStore.updateEditorSettings({ dataGridToolbarLayout: "split" });
|
||||
await vi.waitFor(() => expect(refresh()?.classList.contains("data-grid-topbar-action-label--compact")).toBe(false));
|
||||
expect(host.querySelector(".data-grid-topbar--compact")).toBeNull();
|
||||
|
||||
settingsStore.updateEditorSettings({ dataGridToolbarLayout: "single" });
|
||||
await vi.waitFor(() => expect(refresh()?.classList.contains("data-grid-topbar-action-label--compact")).toBe(true));
|
||||
settingsStore.updateEditorSettings({ dataGridToolbarLayout: "split" });
|
||||
await vi.waitFor(() => expect(refresh()?.classList.contains("data-grid-topbar-action-label--compact")).toBe(false));
|
||||
});
|
||||
|
||||
it("compacts split filter controls independently from action labels", async () => {
|
||||
const geometry = { width: 800, filters: 900, actions: 500 };
|
||||
const resize = mockToolbarGeometry(geometry);
|
||||
const { host, settingsStore } = mountGrid();
|
||||
settingsStore.updateEditorSettings({ dataGridToolbarLayout: "split" });
|
||||
await vi.waitFor(() => expect(host.querySelector(".data-grid-topbar--compact")).not.toBeNull());
|
||||
expect(host.querySelector('[data-toolbar-action="refresh"] .data-grid-topbar-action-label--compact')).toBeNull();
|
||||
|
||||
geometry.width = 940;
|
||||
resize();
|
||||
await vi.waitFor(() => expect(host.querySelector(".data-grid-topbar--compact")).toBeNull());
|
||||
expect(host.querySelector('[data-toolbar-action="refresh"] .data-grid-topbar-action-label--compact')).toBeNull();
|
||||
});
|
||||
|
||||
it("measures both split action segments and restores their labels when the pane grows", async () => {
|
||||
const geometry = { width: 700, filters: 500, actions: 650, compactActions: 450, leading: 100 };
|
||||
const resize = mockToolbarGeometry(geometry);
|
||||
const { host, settingsStore } = mountGrid({ slots: { "result-toolbar-leading": () => h("span", { "data-test-toolbar-leading": "" }, "Results") } });
|
||||
settingsStore.updateEditorSettings({ dataGridToolbarLayout: "split" });
|
||||
await vi.waitFor(() => expect(host.querySelector('[data-toolbar-action="refresh"] .data-grid-topbar-action-label--compact')).not.toBeNull());
|
||||
expect(host.querySelector(".data-grid-topbar--compact")).toBeNull();
|
||||
|
||||
geometry.width = 800;
|
||||
resize();
|
||||
await vi.waitFor(() => expect(host.querySelector('[data-toolbar-action="refresh"] .data-grid-topbar-action-label--compact')).toBeNull());
|
||||
expect(host.querySelector(".data-grid-topbar--compact")).toBeNull();
|
||||
});
|
||||
|
||||
it("restores split action labels while the filter row still needs compact controls", async () => {
|
||||
const geometry = { width: 700, filters: 900, actions: 750, compactActions: 450, leading: 100 };
|
||||
const resize = mockToolbarGeometry(geometry);
|
||||
const { host, settingsStore } = mountGrid({ slots: { "result-toolbar-leading": () => h("span", { "data-test-toolbar-leading": "" }, "Results") } });
|
||||
settingsStore.updateEditorSettings({ dataGridToolbarLayout: "split" });
|
||||
await vi.waitFor(() => expect(host.querySelector('[data-toolbar-action="refresh"] .data-grid-topbar-action-label--compact')).not.toBeNull());
|
||||
expect(host.querySelector(".data-grid-topbar--compact")).not.toBeNull();
|
||||
|
||||
geometry.width = 900;
|
||||
resize();
|
||||
await vi.waitFor(() => expect(host.querySelector('[data-toolbar-action="refresh"] .data-grid-topbar-action-label--compact')).toBeNull());
|
||||
expect(host.querySelector(".data-grid-topbar--compact")).not.toBeNull();
|
||||
|
||||
geometry.width = 940;
|
||||
resize();
|
||||
await vi.waitFor(() => expect(host.querySelector(".data-grid-topbar--compact")).toBeNull());
|
||||
});
|
||||
|
||||
it("compacts WHERE and ORDER BY when inputs become too narrow without row overflow", async () => {
|
||||
const geometry = { width: 500, filters: 500, actions: 300, conditionInputs: { where: 150, order: 120 } };
|
||||
const resize = mockToolbarGeometry(geometry);
|
||||
const { host, settingsStore } = mountGrid({ filterControls: true, withTableMetadata: true });
|
||||
settingsStore.updateEditorSettings({ dataGridToolbarLayout: "split" });
|
||||
await vi.waitFor(() => expect(host.querySelectorAll("[data-grid-condition-editor] > div > .data-grid-topbar-condition-label--compact")).toHaveLength(2));
|
||||
expect(host.querySelector('[data-toolbar-action="refresh"] .data-grid-topbar-action-label--compact')).toBeNull();
|
||||
|
||||
geometry.width = 800;
|
||||
geometry.conditionInputs = { where: 260, order: 230 };
|
||||
resize();
|
||||
await vi.waitFor(() => expect(host.querySelector(".data-grid-topbar-condition-label--compact")).toBeNull());
|
||||
});
|
||||
|
||||
it("remeasures the WHERE/ORDER BY divider without resizing the pane and avoids premature label expansion", async () => {
|
||||
const geometry = { width: 800, filters: 800, actions: 300, conditionInputs: { where: 260, order: 120 } };
|
||||
const resize = mockToolbarGeometry(geometry);
|
||||
const { host, settingsStore } = mountGrid({ filterControls: true, withTableMetadata: true });
|
||||
settingsStore.updateEditorSettings({ dataGridToolbarLayout: "split" });
|
||||
await vi.waitFor(() => expect(host.querySelectorAll("[data-grid-condition-editor] > div > .data-grid-topbar-condition-label--compact")).toHaveLength(2));
|
||||
|
||||
geometry.conditionInputs.order = 165;
|
||||
resize("[data-grid-condition-input-viewport]");
|
||||
await settle();
|
||||
expect(host.querySelectorAll("[data-grid-condition-editor] > div > .data-grid-topbar-condition-label--compact")).toHaveLength(2);
|
||||
|
||||
geometry.conditionInputs.order = 190;
|
||||
resize("[data-grid-condition-input-viewport]");
|
||||
await vi.waitFor(() => expect(host.querySelector(".data-grid-topbar-condition-label--compact")).toBeNull());
|
||||
expect(host.querySelector('[data-toolbar-action="refresh"] .data-grid-topbar-action-label--compact')).toBeNull();
|
||||
});
|
||||
|
||||
it("does not duplicate the DDL action in the table-data toolbar", async () => {
|
||||
const { host } = mountGrid({ withTableMetadata: true });
|
||||
await settle();
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
dataGridToolbarIntervalOptions,
|
||||
isDataGridToolbarActionCompact,
|
||||
isDataGridToolbarCompact,
|
||||
shouldCompactDataGridConditionInput,
|
||||
selectDataGridToolbarAddRowItem,
|
||||
selectDataGridToolbarAutoRefreshInterval,
|
||||
selectDataGridToolbarCopyItem,
|
||||
@@ -67,6 +68,19 @@ describe("data grid toolbar capabilities", () => {
|
||||
expect(isDataGridToolbarCompact(1000, 1100)).toBe(false);
|
||||
});
|
||||
|
||||
it("reserves usable condition input width even when the flex row does not overflow", () => {
|
||||
expect(shouldCompactDataGridConditionInput(159)).toBe(true);
|
||||
expect(shouldCompactDataGridConditionInput(160)).toBe(false);
|
||||
expect(shouldCompactDataGridConditionInput(250)).toBe(false);
|
||||
});
|
||||
|
||||
it("accounts for the hidden prefix before restoring condition labels", () => {
|
||||
expect(shouldCompactDataGridConditionInput(210, 60, true)).toBe(true);
|
||||
expect(shouldCompactDataGridConditionInput(235, 60, true)).toBe(true);
|
||||
expect(shouldCompactDataGridConditionInput(236, 60, true)).toBe(false);
|
||||
expect(shouldCompactDataGridConditionInput(Number.NaN)).toBe(false);
|
||||
});
|
||||
|
||||
it("progressively compacts toolbar actions as the available width shrinks", () => {
|
||||
expect(dataGridToolbarActionCollapseCount(1050, 1440, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH)).toBe(0);
|
||||
expect(dataGridToolbarActionCollapseCount(1049, 1440, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH)).toBe(1);
|
||||
|
||||
@@ -18,6 +18,16 @@ export function isDataGridToolbarCompact(toolbarWidth: number, viewportWidth: nu
|
||||
return toolbarWidth > 0 && toolbarWidth < dataGridToolbarCompactBreakpoint(viewportWidth, minimumWidth);
|
||||
}
|
||||
|
||||
// Reserve useful SQL input space before the flex row actually overflows.
|
||||
const DATA_GRID_CONDITION_INPUT_MIN_WIDTH = 160;
|
||||
const DATA_GRID_CONDITION_INPUT_EXPAND_SLACK = 16;
|
||||
|
||||
export function shouldCompactDataGridConditionInput(inputWidth: number, hiddenLabelWidth = 0, compact = false): boolean {
|
||||
if (!Number.isFinite(inputWidth)) return false;
|
||||
const expandedInputWidth = inputWidth - Math.max(0, hiddenLabelWidth);
|
||||
return expandedInputWidth < DATA_GRID_CONDITION_INPUT_MIN_WIDTH + (compact ? DATA_GRID_CONDITION_INPUT_EXPAND_SLACK : 0);
|
||||
}
|
||||
|
||||
export function dataGridToolbarActionCollapseCount(toolbarWidth: number, viewportWidth: number, minimumWidth?: number): number {
|
||||
if (!Number.isFinite(toolbarWidth) || toolbarWidth <= 0) return 0;
|
||||
const deficit = dataGridToolbarCompactBreakpoint(viewportWidth, minimumWidth) - toolbarWidth;
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 85 KiB |
Reference in New Issue
Block a user