mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
fix(grid): show full filter labels for long cell values
This commit is contained in:
@@ -1388,7 +1388,7 @@ const localColumnFilterRuntime = useDataGridColumnFilters({
|
||||
getGlobalQueryTimeoutSecs: () => settingsStore.editorSettings.globalQueryTimeoutSecs,
|
||||
getNewRows: () => getGridNewRows(),
|
||||
getRowData: (row, sourceIndex) => getGridRowData(row, sourceIndex),
|
||||
formatValue: formatCellCached,
|
||||
formatValue: (value, columnIndex) => formatCell(value, columnIndex, undefined, false),
|
||||
waitForTableMeta,
|
||||
applyWhereFilter,
|
||||
resetGridVerticalScroll: () => resetLocalFilterGridScroll(),
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ArrowDown, ArrowUp, ArrowUpDown, Check, Database, Filter, Loader2, Search } from "@lucide/vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import LightTooltip from "@/components/ui/LightTooltip.vue";
|
||||
import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import type { DataGridLocalFilterOption, DataGridLocalFilterSort } from "@/lib/dataGrid/dataGridLocalColumnFilterState";
|
||||
|
||||
@@ -114,15 +115,20 @@ const { t } = useI18n();
|
||||
</span>
|
||||
</div>
|
||||
<div class="max-h-72 overflow-auto py-0.5">
|
||||
<button v-for="option in props.options" :key="option.key" type="button" class="grid w-full grid-cols-[1.75rem_minmax(0,1fr)_3.5rem] items-center px-2 py-1 text-left text-xs hover:bg-accent" @click="emit('toggleValue', option.key)">
|
||||
<span class="flex h-4 w-4 items-center justify-center rounded border" :class="props.draftValues?.has(option.key) ? 'border-blue-600 bg-blue-600 text-white' : 'border-border bg-background text-foreground/70'">
|
||||
<Check v-if="props.draftValues?.has(option.key)" class="h-3 w-3 stroke-[3]" />
|
||||
</span>
|
||||
<span class="truncate font-mono" :class="{ 'italic text-muted-foreground': option.value === null }">
|
||||
{{ option.label }}
|
||||
</span>
|
||||
<span class="text-right tabular-nums text-muted-foreground text-xs">{{ option.count ?? "" }}</span>
|
||||
</button>
|
||||
<LightTooltip v-for="option in props.options" :key="option.key" :text="option.label" side="left" content-class="w-max max-w-[min(32rem,calc(100vw-2rem))]">
|
||||
<button type="button" class="grid w-full grid-cols-[1.75rem_minmax(0,1fr)_3.5rem] items-center px-2 py-1 text-left text-xs hover:bg-accent" @click="emit('toggleValue', option.key)">
|
||||
<span class="flex h-4 w-4 items-center justify-center rounded border" :class="props.draftValues?.has(option.key) ? 'border-blue-600 bg-blue-600 text-white' : 'border-border bg-background text-foreground/70'">
|
||||
<Check v-if="props.draftValues?.has(option.key)" class="h-3 w-3 stroke-[3]" />
|
||||
</span>
|
||||
<span class="truncate font-mono" :class="{ 'italic text-muted-foreground': option.value === null }">
|
||||
{{ option.label }}
|
||||
</span>
|
||||
<span class="text-right tabular-nums text-muted-foreground text-xs">{{ option.count ?? "" }}</span>
|
||||
</button>
|
||||
<template #content>
|
||||
<div class="max-h-72 overflow-auto whitespace-pre-wrap break-all px-3 py-2 font-mono">{{ option.label }}</div>
|
||||
</template>
|
||||
</LightTooltip>
|
||||
<div v-if="props.draftMode === 'local' && props.allOptionsCount > props.options.length" class="px-2 py-0.5 text-center text-[10px] text-muted-foreground">
|
||||
{{ t("grid.moreValues", { count: props.allOptionsCount - props.options.length }) }}
|
||||
</div>
|
||||
|
||||
+1
@@ -11,6 +11,7 @@ vi.mock("@lucide/vue", async () => {
|
||||
return { ArrowDown: icon, ArrowUp: icon, ArrowUpDown: icon, Check: icon, Database: icon, Filter: icon, Loader2: icon, Search: icon };
|
||||
});
|
||||
vi.mock("@/components/ui/button", async () => ({ Button: (await import("./vueHostHarness")).createPassthroughStub("Button", "button") }));
|
||||
vi.mock("@/components/ui/LightTooltip.vue", async () => ({ default: (await import("./vueHostHarness")).createPassthroughStub("LightTooltip") }));
|
||||
vi.mock("@/components/ui/popover", async () => {
|
||||
const { createPassthroughStub } = await import("./vueHostHarness");
|
||||
return { Popover: createPassthroughStub("Popover"), PopoverAnchor: createPassthroughStub("PopoverAnchor"), PopoverContent: createPassthroughStub("PopoverContent"), PopoverTrigger: createPassthroughStub("PopoverTrigger") };
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { createApp, defineComponent, h, markRaw, nextTick, type App } from "vue";
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import i18n from "@/i18n";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import type { QueryResult } from "@/types/database";
|
||||
|
||||
vi.mock("@/composables/useDataGridColumnResize", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@/composables/useDataGridColumnResize")>();
|
||||
const { ref } = await import("vue");
|
||||
return {
|
||||
...actual,
|
||||
useDataGridColumnResize: () => ({
|
||||
initColumnWidths: vi.fn(),
|
||||
onResizeStart: vi.fn(),
|
||||
autoFitColumn: vi.fn(),
|
||||
renderedColumnWidths: ref([120]),
|
||||
totalWidth: ref(120),
|
||||
columnVars: ref({ "--total-w": "120px" }),
|
||||
getIsResizing: () => false,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
import DataGrid from "../DataGrid.vue";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
|
||||
const prefix = "long-value-prefix-".repeat(30);
|
||||
const alpha = `${prefix}ALPHA_END`;
|
||||
const beta = `${prefix}BETA_END`;
|
||||
const mounted: Array<{ app: App; host: HTMLElement }> = [];
|
||||
|
||||
async function settle() {
|
||||
for (let i = 0; i < 6; i++) {
|
||||
await nextTick();
|
||||
await Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
async function openFilter() {
|
||||
const pinia = createPinia();
|
||||
setActivePinia(pinia);
|
||||
useSettingsStore().updateEditorSettings({ dataGridRenderMode: "canvas", compactColumnHeaderActions: false });
|
||||
const result = markRaw<QueryResult>({ columns: ["label"], column_types: ["varchar"], rows: [[beta], [alpha]], affected_rows: 2, execution_time_ms: 1 });
|
||||
const onLocalColumnFiltersChange = vi.fn();
|
||||
const Root = defineComponent({
|
||||
setup: () => () => h(TooltipProvider, { delayDuration: 0 }, { default: () => h(DataGrid, { result, databaseType: "mysql", context: "table-data", onLocalColumnFiltersChange }) }),
|
||||
});
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
const app = createApp(Root);
|
||||
app.use(pinia);
|
||||
app.use(i18n);
|
||||
app.mount(host);
|
||||
mounted.push({ app, host });
|
||||
await settle();
|
||||
const trigger = host.querySelector<HTMLButtonElement>(`button[title="${i18n.global.t("grid.localFilter")}"]`)!;
|
||||
expect(trigger).toBeTruthy();
|
||||
trigger.click();
|
||||
await settle();
|
||||
return { onLocalColumnFiltersChange };
|
||||
}
|
||||
|
||||
function valueButtons() {
|
||||
return Array.from(document.querySelectorAll<HTMLButtonElement>("button")).filter((button) => button.textContent?.includes("long-value-prefix-"));
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const { app, host } of mounted.splice(0)) {
|
||||
app.unmount();
|
||||
host.remove();
|
||||
}
|
||||
vi.restoreAllMocks();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe("DataGrid local filter full values", () => {
|
||||
it("keeps distinct suffixes in sorted option labels beyond the grid display limit", async () => {
|
||||
await openFilter();
|
||||
expect(valueButtons().map((button) => button.textContent)).toEqual([`${alpha}1`, `${beta}1`]);
|
||||
});
|
||||
|
||||
it("finds an actual candidate when searching for its suffix beyond the display limit", async () => {
|
||||
await openFilter();
|
||||
const search = document.querySelector<HTMLInputElement>(`input[placeholder="${i18n.global.t("grid.searchValues")}"]`)!;
|
||||
search.value = "BETA_END";
|
||||
search.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
await settle();
|
||||
expect(valueButtons()).toHaveLength(1);
|
||||
expect(valueButtons()[0].textContent).toBe(`${beta}1`);
|
||||
});
|
||||
|
||||
it("filters with the original full value after changing the selected candidate", async () => {
|
||||
const { onLocalColumnFiltersChange } = await openFilter();
|
||||
const alphaButton = valueButtons().find((button) => button.textContent?.includes("ALPHA_END")) ?? valueButtons()[1];
|
||||
alphaButton.click();
|
||||
await settle();
|
||||
const apply = Array.from(document.querySelectorAll<HTMLButtonElement>("button")).find((button) => button.textContent?.trim() === i18n.global.t("grid.applyFilter"))!;
|
||||
apply.click();
|
||||
await settle();
|
||||
expect(onLocalColumnFiltersChange).toHaveBeenLastCalledWith({ "0": [`str:${beta}`] });
|
||||
});
|
||||
|
||||
it.each(["hover", "focus"] as const)("exposes the full formatted candidate on %s", async (interaction) => {
|
||||
await openFilter();
|
||||
const button = valueButtons()[0];
|
||||
vi.useFakeTimers();
|
||||
vi.spyOn(button, "matches").mockImplementation((selector) => selector === (interaction === "hover" ? ":hover" : ":focus-visible"));
|
||||
if (interaction === "hover") button.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
else button.focus();
|
||||
await vi.advanceTimersByTimeAsync(350);
|
||||
await settle();
|
||||
expect(document.querySelector('[role="tooltip"]')?.textContent).toBe(alpha);
|
||||
});
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
Reference in New Issue
Block a user