feat(grid): sort local filter values by count
This commit is contained in:
7 files changed
+310
-16
No files matched your search
@@ -1329,6 +1329,8 @@ const {
|
||||
localFilteredRows,
|
||||
localFilterAllOptions,
|
||||
localFilterOptions,
|
||||
localFilterSort,
|
||||
toggleLocalFilterSort,
|
||||
localFilterTypedValue,
|
||||
canApplyTypedLocalFilterValue,
|
||||
openLocalFilter,
|
||||
@@ -13001,6 +13003,8 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
|
||||
:draft-mode="localFilterDraft?.mode"
|
||||
:draft-values="localFilterDraft?.values"
|
||||
:options="localFilterOptions"
|
||||
:sort="localFilterSort"
|
||||
@sort="toggleLocalFilterSort"
|
||||
:all-options-count="localFilterAllOptions.length"
|
||||
:can-apply-typed-value="canApplyTypedLocalFilterValue"
|
||||
:typed-value="localFilterTypedValue"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { Check, Database, Filter, Loader2, Search } from "@lucide/vue";
|
||||
import { ArrowDown, ArrowUp, ArrowUpDown, Check, Database, Filter, Loader2, Search } from "@lucide/vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import type { DataGridLocalFilterOption } from "@/lib/dataGrid/dataGridLocalColumnFilterState";
|
||||
import type { DataGridLocalFilterOption, DataGridLocalFilterSort } from "@/lib/dataGrid/dataGridLocalColumnFilterState";
|
||||
|
||||
type LocalFilterMode = "local" | "server";
|
||||
|
||||
@@ -20,6 +20,7 @@ interface DataGridColumnFilterPopoverProps {
|
||||
draftMode?: LocalFilterMode;
|
||||
draftValues?: Set<string>;
|
||||
options: DataGridLocalFilterOption[];
|
||||
sort?: DataGridLocalFilterSort;
|
||||
allOptionsCount: number;
|
||||
canApplyTypedValue: boolean;
|
||||
typedValue: string;
|
||||
@@ -35,6 +36,7 @@ const emit = defineEmits<{
|
||||
"update:open": [value: boolean];
|
||||
"update:search": [value: string];
|
||||
resizeStart: [event: MouseEvent, direction: "left" | "right"];
|
||||
sort: [field: DataGridLocalFilterSort["field"]];
|
||||
toggleAll: [];
|
||||
toggleValue: [key: string];
|
||||
applyTypedValue: [];
|
||||
@@ -84,8 +86,24 @@ const { t } = useI18n();
|
||||
>
|
||||
<Check v-if="props.options.length > 0 && props.draftValues && props.options.every((option) => props.draftValues?.has(option.key))" class="h-3 w-3 stroke-[3]" />
|
||||
</button>
|
||||
<span>{{ t("grid.value") }}</span>
|
||||
<span class="text-right">{{ t("grid.count") }}</span>
|
||||
<template v-if="props.draftMode === 'local' && props.sort">
|
||||
<button
|
||||
v-for="field in ['value', 'count'] as const"
|
||||
:key="field"
|
||||
type="button"
|
||||
class="flex min-w-0 items-center gap-0.5 rounded hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
:class="field === 'count' ? 'justify-end' : 'justify-start'"
|
||||
:aria-label="`${t(`grid.${field}`)}: ${t(props.sort.field === field ? (props.sort.direction === 'asc' ? 'grid.sortAscending' : 'grid.sortDescending') : field === 'count' ? 'grid.sortDescending' : 'grid.sortAscending')}`"
|
||||
@click.stop="emit('sort', field)"
|
||||
>
|
||||
<span>{{ t(`grid.${field}`) }}</span>
|
||||
<component :is="props.sort.field === field ? (props.sort.direction === 'asc' ? ArrowUp : ArrowDown) : ArrowUpDown" class="h-3 w-3 shrink-0" aria-hidden="true" />
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span>{{ t("grid.value") }}</span>
|
||||
<span class="text-right">{{ t("grid.count") }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="props.draftMode === 'server' && (props.serverLoading || props.serverError || props.serverLimited)" class="flex items-center gap-1.5 border-b px-2 py-1 text-[11px] text-muted-foreground">
|
||||
<Loader2 v-if="props.serverLoading" class="h-3 w-3 animate-spin" />
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { dispatch, findAll, findOne, mountComponent } from "./vueHostHarness";
|
||||
import type { DataGridLocalFilterOption, DataGridLocalFilterSort } from "@/lib/dataGrid/dataGridLocalColumnFilterState";
|
||||
|
||||
vi.mock("vue-i18n", () => ({ useI18n: () => ({ t: (key: string) => key }) }));
|
||||
vi.mock("@lucide/vue", async () => {
|
||||
const { createPassthroughStub } = await import("./vueHostHarness");
|
||||
const icon = createPassthroughStub("Icon", "i");
|
||||
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/popover", async () => {
|
||||
const { createPassthroughStub } = await import("./vueHostHarness");
|
||||
return { Popover: createPassthroughStub("Popover"), PopoverAnchor: createPassthroughStub("PopoverAnchor"), PopoverContent: createPassthroughStub("PopoverContent"), PopoverTrigger: createPassthroughStub("PopoverTrigger") };
|
||||
});
|
||||
|
||||
import DataGridColumnFilterPopover from "@/components/grid/DataGridColumnFilterPopover.vue";
|
||||
|
||||
const options: DataGridLocalFilterOption[] = [
|
||||
{ key: "str:alpha", label: "alpha", count: 1, value: "alpha" },
|
||||
{ key: "str:beta", label: "beta", count: 3, value: "beta" },
|
||||
];
|
||||
|
||||
function mountPopover(sort: DataGridLocalFilterSort, onSort: ReturnType<typeof vi.fn>) {
|
||||
return mountComponent(DataGridColumnFilterPopover, {
|
||||
open: true,
|
||||
compactHeaderActions: false,
|
||||
canUseServerFilter: false,
|
||||
active: false,
|
||||
serverModeActive: false,
|
||||
panelTitle: "status",
|
||||
search: "",
|
||||
popoverWidth: 280,
|
||||
popoverOffsetX: 0,
|
||||
draftMode: "local",
|
||||
draftValues: new Set(options.map((option) => option.key)),
|
||||
options,
|
||||
sort,
|
||||
allOptionsCount: options.length,
|
||||
canApplyTypedValue: false,
|
||||
typedValue: "",
|
||||
serverLoading: false,
|
||||
serverError: "",
|
||||
serverLimited: false,
|
||||
serverValueLimit: 1000,
|
||||
onSort,
|
||||
});
|
||||
}
|
||||
|
||||
describe("DataGridColumnFilterPopover sort toggles", () => {
|
||||
it("emits sort with the clicked field and announces count-desc / value-asc as the first-click directions", async () => {
|
||||
const onSort = vi.fn();
|
||||
const mounted = mountPopover({ field: "value", direction: "asc" }, onSort);
|
||||
|
||||
// From the default value-asc state, the count header advertises descending
|
||||
// for its first click while the active value header stays ascending.
|
||||
const countButton = findOne(mounted.root, (node) => node.props["aria-label"] === "grid.count: grid.sortDescending");
|
||||
expect(findOne(mounted.root, (node) => node.props["aria-label"] === "grid.value: grid.sortAscending")).toBeTruthy();
|
||||
|
||||
dispatch(countButton, "click");
|
||||
expect(onSort).toHaveBeenCalledExactlyOnceWith("count");
|
||||
|
||||
await mounted.setProps({ sort: { field: "count", direction: "desc" } });
|
||||
// Switching back from count-desc, the value header advertises ascending.
|
||||
const valueButton = findOne(mounted.root, (node) => node.props["aria-label"] === "grid.value: grid.sortAscending");
|
||||
dispatch(valueButton, "click");
|
||||
expect(onSort).toHaveBeenLastCalledWith("value");
|
||||
|
||||
expect(onSort).toHaveBeenCalledTimes(2);
|
||||
mounted.unmount();
|
||||
});
|
||||
|
||||
it("renders plain labels without sort controls outside the local draft mode", async () => {
|
||||
const onSort = vi.fn();
|
||||
const mounted = mountPopover({ field: "value", direction: "asc" }, onSort);
|
||||
|
||||
await mounted.setProps({ draftMode: "server" });
|
||||
|
||||
expect(findAll(mounted.root, (node) => node.type === "button" && typeof node.props["aria-label"] === "string" && node.props["aria-label"].startsWith("grid.value: "))).toHaveLength(0);
|
||||
expect(findAll(mounted.root, (node) => node.type === "button" && typeof node.props["aria-label"] === "string" && node.props["aria-label"].startsWith("grid.count: "))).toHaveLength(0);
|
||||
expect(onSort).not.toHaveBeenCalled();
|
||||
mounted.unmount();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user