first commit
This commit is contained in:
143
ui/components/table/hooks/useColumnConfig.ts
Normal file
143
ui/components/table/hooks/useColumnConfig.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { ColumnPinningState, VisibilityState } from "@tanstack/react-table";
|
||||
import { parseAsString, useQueryState } from "nuqs";
|
||||
import { useCallback, useMemo } from "react";
|
||||
|
||||
export interface ColumnConfigEntry {
|
||||
id: string;
|
||||
visible: boolean;
|
||||
pinned: "left" | "right" | false;
|
||||
}
|
||||
|
||||
interface UseColumnConfigOptions {
|
||||
/** All available column IDs in their default order */
|
||||
columnIds: string[];
|
||||
/** URL query param name for persistence */
|
||||
paramName?: string;
|
||||
/** Columns excluded from configuration (always visible, always in position) */
|
||||
fixedColumns?: { left?: string[]; right?: string[] };
|
||||
}
|
||||
|
||||
// URL format: col1,col2:h,col3:l,col4:r
|
||||
// no suffix = visible & unpinned, :h = hidden, :l = pinned left, :r = pinned right
|
||||
function serialize(entries: ColumnConfigEntry[]): string {
|
||||
return entries
|
||||
.map((e) => {
|
||||
let flags = "";
|
||||
if (!e.visible) flags += "h";
|
||||
if (e.pinned === "left") flags += "l";
|
||||
if (e.pinned === "right") flags += "r";
|
||||
const encoded = encodeURIComponent(e.id);
|
||||
return flags ? `${encoded}:${flags}` : encoded;
|
||||
})
|
||||
.join(",");
|
||||
}
|
||||
|
||||
function deserialize(str: string): ColumnConfigEntry[] {
|
||||
if (!str) return [];
|
||||
return str.split(",").map((part) => {
|
||||
const [rawId, flags = ""] = part.split(":");
|
||||
const id = decodeURIComponent(rawId);
|
||||
return {
|
||||
id,
|
||||
visible: !flags.includes("h"),
|
||||
pinned: flags.includes("l") ? ("left" as const) : flags.includes("r") ? ("right" as const) : false,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function useColumnConfig({ columnIds, paramName = "cols", fixedColumns }: UseColumnConfigOptions) {
|
||||
const [raw, setRaw] = useQueryState(paramName, parseAsString.withDefault(""));
|
||||
|
||||
const fixedLeft = fixedColumns?.left ?? [];
|
||||
const fixedRight = fixedColumns?.right ?? [];
|
||||
const fixedSet = useMemo(() => new Set([...fixedLeft, ...fixedRight]), [fixedLeft, fixedRight]);
|
||||
|
||||
const configurableIds = useMemo(() => columnIds.filter((id) => !fixedSet.has(id)), [columnIds, fixedSet]);
|
||||
|
||||
// Merge URL config with available columns (handles added/removed columns)
|
||||
const entries = useMemo(() => {
|
||||
const parsed = deserialize(raw);
|
||||
const result: ColumnConfigEntry[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
// Columns present in URL config that still exist
|
||||
for (const entry of parsed) {
|
||||
if (configurableIds.includes(entry.id) && !seen.has(entry.id)) {
|
||||
result.push(entry);
|
||||
seen.add(entry.id);
|
||||
}
|
||||
}
|
||||
|
||||
// New columns not yet in URL config
|
||||
for (const id of configurableIds) {
|
||||
if (!seen.has(id)) {
|
||||
result.push({ id, visible: true, pinned: false });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [raw, configurableIds]);
|
||||
|
||||
// TanStack table state
|
||||
const columnOrder = useMemo(() => [...fixedLeft, ...entries.map((e) => e.id), ...fixedRight], [entries, fixedLeft, fixedRight]);
|
||||
|
||||
const columnVisibility = useMemo(() => {
|
||||
const vis: VisibilityState = {};
|
||||
for (const entry of entries) {
|
||||
if (!entry.visible) vis[entry.id] = false;
|
||||
}
|
||||
return vis;
|
||||
}, [entries]);
|
||||
|
||||
const columnPinning = useMemo(
|
||||
(): ColumnPinningState => ({
|
||||
left: [...fixedLeft, ...entries.filter((e) => e.pinned === "left").map((e) => e.id)],
|
||||
right: [...entries.filter((e) => e.pinned === "right").map((e) => e.id), ...fixedRight],
|
||||
}),
|
||||
[entries, fixedLeft, fixedRight],
|
||||
);
|
||||
|
||||
const persist = useCallback(
|
||||
(newEntries: ColumnConfigEntry[]) => {
|
||||
const serialized = serialize(newEntries);
|
||||
setRaw(serialized || null);
|
||||
},
|
||||
[setRaw],
|
||||
);
|
||||
|
||||
const toggleVisibility = useCallback(
|
||||
(columnId: string) => {
|
||||
persist(entries.map((e) => (e.id === columnId ? { ...e, visible: !e.visible } : e)));
|
||||
},
|
||||
[entries, persist],
|
||||
);
|
||||
|
||||
const togglePin = useCallback(
|
||||
(columnId: string, position: "left" | "right") => {
|
||||
persist(entries.map((e) => (e.id === columnId ? { ...e, pinned: e.pinned === position ? false : position } : e)));
|
||||
},
|
||||
[entries, persist],
|
||||
);
|
||||
|
||||
const reorder = useCallback(
|
||||
(newEntries: ColumnConfigEntry[]) => {
|
||||
persist(newEntries);
|
||||
},
|
||||
[persist],
|
||||
);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setRaw(null);
|
||||
}, [setRaw]);
|
||||
|
||||
return {
|
||||
entries,
|
||||
columnOrder,
|
||||
columnVisibility,
|
||||
columnPinning,
|
||||
toggleVisibility,
|
||||
togglePin,
|
||||
reorder,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user