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();
|
||||
});
|
||||
});
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { createApp, defineComponent, h, ref, computed } from "vue";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { useDataGridColumnFilters, type DataGridColumnFilterState, type DataGridLocalFilterDraft } from "@/composables/useDataGridColumnFilters";
|
||||
import type { DataGridLocalFilterOption } from "@/lib/dataGrid/dataGridLocalColumnFilterState";
|
||||
import type { CellValue } from "@/lib/dataGrid/cellValue";
|
||||
import type { QueryResult } from "@/types/database";
|
||||
|
||||
vi.mock("@/lib/backend/api", () => ({ executeQuery: vi.fn() }));
|
||||
|
||||
const disposers: Array<() => void> = [];
|
||||
|
||||
function mountColumnFilters(rows: CellValue[][]) {
|
||||
const state: DataGridColumnFilterState = {
|
||||
localColumnFilters: ref({}),
|
||||
localFilterOpenColumn: ref<number | null>(null),
|
||||
localFilterSearch: ref(""),
|
||||
localFilterDraft: ref<DataGridLocalFilterDraft | null>(null),
|
||||
serverFilterLoading: ref(false),
|
||||
serverFilterError: ref(""),
|
||||
serverFilterOptions: ref<DataGridLocalFilterOption[]>([]),
|
||||
serverFilterLimited: ref(false),
|
||||
serverFilterValueByKey: ref(new Map()),
|
||||
serverColumnFilters: ref({}),
|
||||
};
|
||||
const result: QueryResult = { columns: ["status"], rows };
|
||||
let filters: ReturnType<typeof useDataGridColumnFilters> | undefined;
|
||||
const app = createApp(
|
||||
defineComponent({
|
||||
setup() {
|
||||
filters = useDataGridColumnFilters({
|
||||
state,
|
||||
getResult: () => result,
|
||||
getTableMeta: () => undefined,
|
||||
getConnectionId: () => undefined,
|
||||
getSchema: () => undefined,
|
||||
getExecutionDatabase: () => "",
|
||||
resolvedDatabaseType: computed(() => undefined),
|
||||
canUseWhereSearch: computed(() => false),
|
||||
canUseServerColumnFilter: computed(() => false),
|
||||
structuredFilterCount: computed(() => 0),
|
||||
hasStructuredFilters: computed(() => false),
|
||||
whereFilterInput: ref(""),
|
||||
getConnectionConfig: () => undefined,
|
||||
getIdentifierQuote: () => undefined,
|
||||
getNewRows: () => [],
|
||||
getRowData: (row) => row,
|
||||
formatValue: (value) => String(value),
|
||||
waitForTableMeta: () => Promise.resolve(null),
|
||||
applyWhereFilter: () => Promise.resolve(),
|
||||
resetGridVerticalScroll: () => {},
|
||||
emitLocalFiltersChange: () => {},
|
||||
});
|
||||
return () => h("div");
|
||||
},
|
||||
}),
|
||||
);
|
||||
app.mount(document.createElement("div"));
|
||||
disposers.push(() => app.unmount());
|
||||
if (!filters) throw new Error("useDataGridColumnFilters was not mounted");
|
||||
return filters;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const dispose of disposers.splice(0)) dispose();
|
||||
});
|
||||
|
||||
describe("useDataGridColumnFilters local filter sort", () => {
|
||||
it("sorts values by count descending on the first count click and back to value ascending on the first value click", () => {
|
||||
const filters = mountColumnFilters([["beta"], ["beta"], ["beta"], ["alpha"], ["alpha"], ["zulu"]]);
|
||||
|
||||
filters.openLocalFilter(0);
|
||||
expect(filters.localFilterSort.value).toEqual({ field: "value", direction: "asc" });
|
||||
expect(filters.localFilterOptions.value.map((option) => option.label)).toEqual(["alpha", "beta", "zulu"]);
|
||||
|
||||
filters.toggleLocalFilterSort("count");
|
||||
expect(filters.localFilterSort.value).toEqual({ field: "count", direction: "desc" });
|
||||
expect(filters.localFilterOptions.value.map((option) => [option.label, option.count])).toEqual([
|
||||
["beta", 3],
|
||||
["alpha", 2],
|
||||
["zulu", 1],
|
||||
]);
|
||||
|
||||
filters.toggleLocalFilterSort("value");
|
||||
expect(filters.localFilterSort.value).toEqual({ field: "value", direction: "asc" });
|
||||
expect(filters.localFilterOptions.value.map((option) => option.label)).toEqual(["alpha", "beta", "zulu"]);
|
||||
});
|
||||
|
||||
it("flips the direction when the same sort header is clicked again", () => {
|
||||
const filters = mountColumnFilters([["beta"], ["alpha"], ["alpha"]]);
|
||||
|
||||
filters.openLocalFilter(0);
|
||||
filters.toggleLocalFilterSort("count");
|
||||
expect(filters.localFilterSort.value).toEqual({ field: "count", direction: "desc" });
|
||||
expect(filters.localFilterOptions.value.map((option) => [option.label, option.count])).toEqual([
|
||||
["alpha", 2],
|
||||
["beta", 1],
|
||||
]);
|
||||
|
||||
filters.toggleLocalFilterSort("count");
|
||||
expect(filters.localFilterSort.value).toEqual({ field: "count", direction: "asc" });
|
||||
expect(filters.localFilterOptions.value.map((option) => [option.label, option.count])).toEqual([
|
||||
["beta", 1],
|
||||
["alpha", 2],
|
||||
]);
|
||||
});
|
||||
|
||||
it("ignores sort toggles while no local filter draft is open", () => {
|
||||
const filters = mountColumnFilters([["beta"], ["alpha"]]);
|
||||
|
||||
filters.toggleLocalFilterSort("count");
|
||||
|
||||
expect(filters.localFilterSort.value).toEqual({ field: "value", direction: "asc" });
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
import { computed, onUnmounted, watch, type ComputedRef, type Ref } from "vue";
|
||||
import { computed, onUnmounted, ref, watch, type ComputedRef, type Ref } from "vue";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import { buildColumnValueFilterCondition, buildColumnValuesFilterCondition, parseFilterValue, removeColumnValueFilterCondition, replaceColumnValueFilterCondition, appendColumnValueFilterCondition } from "@/lib/dataGrid/dataGridColumnFilter";
|
||||
import { buildDataGridColumnDistinctValuesSql } from "@/lib/dataGrid/dataGridSql";
|
||||
import { buildDataGridLocalFilterOptions, dataGridLocalFilterKey, dataGridLocalFilterLabel, rowMatchesDataGridLocalColumnFilters, type DataGridLocalFilterOption } from "@/lib/dataGrid/dataGridLocalColumnFilterState";
|
||||
import { buildDataGridLocalFilterOptions, sortDataGridLocalFilterOptions, type DataGridLocalFilterSort, dataGridLocalFilterKey, dataGridLocalFilterLabel, rowMatchesDataGridLocalColumnFilters, type DataGridLocalFilterOption } from "@/lib/dataGrid/dataGridLocalColumnFilterState";
|
||||
import type { DataGridCachedServerColumnFilter } from "@/lib/dataGrid/dataGridFilterBuilderPersistence";
|
||||
import type { CellValue } from "@/lib/dataGrid/cellValue";
|
||||
import type { ColumnInfo, DatabaseType, QueryResult } from "@/types/database";
|
||||
@@ -72,6 +72,17 @@ const SERVER_COLUMN_FILTER_DEBOUNCE_MS = 300;
|
||||
|
||||
export function useDataGridColumnFilters(options: UseDataGridColumnFiltersOptions) {
|
||||
const { state } = options;
|
||||
const localFilterSort = ref<DataGridLocalFilterSort>({ field: "value", direction: "asc" });
|
||||
|
||||
function toggleLocalFilterSort(field: DataGridLocalFilterSort["field"]) {
|
||||
if (state.localFilterDraft.value?.mode !== "local") return;
|
||||
const current = localFilterSort.value;
|
||||
localFilterSort.value = {
|
||||
field,
|
||||
direction: current.field === field ? (current.direction === "asc" ? "desc" : "asc") : field === "count" ? "desc" : "asc",
|
||||
};
|
||||
}
|
||||
|
||||
let serverFilterRequestId = 0;
|
||||
let serverFilterSearchTimer: ReturnType<typeof window.setTimeout> | undefined;
|
||||
|
||||
@@ -138,7 +149,13 @@ export function useDataGridColumnFilters(options: UseDataGridColumnFiltersOption
|
||||
const localFilterOptions = computed(() => {
|
||||
if (state.localFilterDraft.value?.mode === "server") return state.serverFilterOptions.value;
|
||||
const query = state.localFilterSearch.value.trim().toLowerCase();
|
||||
return localFilterAllOptions.value.filter((option) => !query || option.label.toLowerCase().includes(query)).slice(0, 500);
|
||||
const matchingOptions = localFilterAllOptions.value.filter((option) => !query || option.label.toLowerCase().includes(query));
|
||||
// Sort before limiting so frequent values outside the default first 500 remain discoverable.
|
||||
// buildDataGridLocalFilterOptions already returns value-ascending options, so the default
|
||||
// sort state skips the re-sort instead of re-running it on every search keystroke.
|
||||
const sort = localFilterSort.value;
|
||||
const sorted = sort.field === "value" && sort.direction === "asc" ? matchingOptions : sortDataGridLocalFilterOptions(matchingOptions, sort);
|
||||
return sorted.slice(0, 500);
|
||||
});
|
||||
const localFilterTypedValue = computed(() => state.localFilterSearch.value.trim());
|
||||
const localFilterDraftIsAllSelected = computed(() => {
|
||||
@@ -172,6 +189,7 @@ export function useDataGridColumnFilters(options: UseDataGridColumnFiltersOption
|
||||
|
||||
function openLocalFilter(columnIndex: number, requestedMode: DataGridLocalFilterMode = "local") {
|
||||
options.onOpen?.();
|
||||
localFilterSort.value = { field: "value", direction: "asc" };
|
||||
state.localFilterSearch.value = "";
|
||||
const mode = requestedMode === "server" && options.canUseServerColumnFilter.value ? "server" : "local";
|
||||
const allKeys = mode === "server" ? [] : buildLocalFilterOptions(columnIndex).map((option) => option.key);
|
||||
@@ -426,6 +444,8 @@ export function useDataGridColumnFilters(options: UseDataGridColumnFiltersOption
|
||||
rowMatchesLocalColumnFilters,
|
||||
localFilterAllOptions,
|
||||
localFilterOptions,
|
||||
localFilterSort,
|
||||
toggleLocalFilterSort,
|
||||
localFilterTypedValue,
|
||||
canApplyTypedLocalFilterValue,
|
||||
openLocalFilter,
|
||||
|
||||
+37
-1
@@ -1,5 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildDataGridLocalFilterOptions, dataGridLocalFilterKey, restoreDataGridLocalColumnFilters, rowMatchesDataGridLocalColumnFilters, serializeDataGridLocalColumnFilters } from "@/lib/dataGrid/dataGridLocalColumnFilterState";
|
||||
import { buildDataGridLocalFilterOptions, dataGridLocalFilterKey, restoreDataGridLocalColumnFilters, rowMatchesDataGridLocalColumnFilters, serializeDataGridLocalColumnFilters, sortDataGridLocalFilterOptions, type DataGridLocalFilterOption } from "@/lib/dataGrid/dataGridLocalColumnFilterState";
|
||||
|
||||
function filterOption(label: string, count: number | null): DataGridLocalFilterOption {
|
||||
return { key: `str:${label}`, label, count, value: label };
|
||||
}
|
||||
|
||||
describe("data grid local column filter state", () => {
|
||||
it("round-trips selected values so a remounted grid restores its filters", () => {
|
||||
@@ -67,4 +71,36 @@ describe("data grid local column filter state", () => {
|
||||
{ key: "str:beta", label: "beta", count: 2, value: "beta" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("sorts filter options by count in both directions and treats a missing count as zero", () => {
|
||||
const options = [filterOption("beta", 2), filterOption("alpha", 5), filterOption("delta", null), filterOption("gamma", 1)];
|
||||
|
||||
expect(sortDataGridLocalFilterOptions(options, { field: "count", direction: "asc" }).map((option) => option.label)).toEqual(["delta", "gamma", "beta", "alpha"]);
|
||||
expect(sortDataGridLocalFilterOptions(options, { field: "count", direction: "desc" }).map((option) => option.label)).toEqual(["alpha", "beta", "gamma", "delta"]);
|
||||
});
|
||||
|
||||
it("breaks equal-count ties by value ascending regardless of the count direction", () => {
|
||||
const options = [filterOption("delta", 3), filterOption("beta", 3), filterOption("alpha", 3)];
|
||||
|
||||
expect(sortDataGridLocalFilterOptions(options, { field: "count", direction: "asc" }).map((option) => option.label)).toEqual(["alpha", "beta", "delta"]);
|
||||
expect(sortDataGridLocalFilterOptions(options, { field: "count", direction: "desc" }).map((option) => option.label)).toEqual(["alpha", "beta", "delta"]);
|
||||
});
|
||||
|
||||
it("keeps null first for value ascending and last for value descending", () => {
|
||||
const options: DataGridLocalFilterOption[] = [filterOption("alpha", 1), { key: "__dbx_null__", label: "NULL", count: null, value: null }, filterOption("beta", 1)];
|
||||
|
||||
expect(sortDataGridLocalFilterOptions(options, { field: "value", direction: "asc" }).map((option) => option.value)).toEqual([null, "alpha", "beta"]);
|
||||
expect(sortDataGridLocalFilterOptions(options, { field: "value", direction: "desc" }).map((option) => option.value)).toEqual(["beta", "alpha", null]);
|
||||
});
|
||||
|
||||
it("returns a new sorted array without mutating the input options", () => {
|
||||
const options = [filterOption("beta", 2), filterOption("alpha", 5)];
|
||||
const snapshot = options.map((option) => ({ ...option }));
|
||||
|
||||
const sorted = sortDataGridLocalFilterOptions(options, { field: "count", direction: "desc" });
|
||||
|
||||
expect(sorted).not.toBe(options);
|
||||
expect(sorted.map((option) => option.label)).toEqual(["alpha", "beta"]);
|
||||
expect(options).toEqual(snapshot);
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,27 @@ export type DataGridLocalFilterOption = {
|
||||
value: CellValue;
|
||||
};
|
||||
|
||||
export type DataGridLocalFilterSort = {
|
||||
field: "value" | "count";
|
||||
direction: "asc" | "desc";
|
||||
};
|
||||
|
||||
function compareLocalFilterValues(a: DataGridLocalFilterOption, b: DataGridLocalFilterOption): number {
|
||||
if (a.value === null && b.value !== null) return -1;
|
||||
if (a.value !== null && b.value === null) return 1;
|
||||
return a.label.localeCompare(b.label, undefined, { numeric: true, sensitivity: "base" });
|
||||
}
|
||||
|
||||
export function sortDataGridLocalFilterOptions(options: readonly DataGridLocalFilterOption[], sort: DataGridLocalFilterSort): DataGridLocalFilterOption[] {
|
||||
const direction = sort.direction === "asc" ? 1 : -1;
|
||||
return [...options].sort((a, b) => {
|
||||
if (sort.field === "count") {
|
||||
return ((a.count ?? 0) - (b.count ?? 0)) * direction || compareLocalFilterValues(a, b);
|
||||
}
|
||||
return compareLocalFilterValues(a, b) * direction;
|
||||
});
|
||||
}
|
||||
|
||||
export function dataGridLocalFilterKey(value: CellValue): string {
|
||||
if (value === null) return "__dbx_null__";
|
||||
if (typeof value === "boolean") return `bool:${value}`;
|
||||
@@ -62,14 +83,7 @@ export function buildDataGridLocalFilterOptions<TRow>({
|
||||
addValue(row[columnIndex] ?? null);
|
||||
}
|
||||
|
||||
return [...byKey.values()].sort((a, b) => {
|
||||
if (a.value === null && b.value !== null) return -1;
|
||||
if (a.value !== null && b.value === null) return 1;
|
||||
return a.label.localeCompare(b.label, undefined, {
|
||||
numeric: true,
|
||||
sensitivity: "base",
|
||||
});
|
||||
});
|
||||
return sortDataGridLocalFilterOptions([...byKey.values()], { field: "value", direction: "asc" });
|
||||
}
|
||||
|
||||
export function restoreDataGridLocalColumnFilters(serialized: SerializedDataGridLocalColumnFilters | undefined, columnCount: number, currentColumns?: readonly string[], serializedColumns?: readonly string[]): Record<number, Set<string>> {
|
||||
|
||||
Reference in new issue
Block a user