metis-desk/packages/rdx-ui/src/components/entity-table-columns-dialog.tsx
David ba034794c6 feat: add column browser fixture and entity column configuration
- Introduced `ColumnBrowserFixture` component for testing column visibility and interactions.
- Created `entity-column-configuration.ts` to define types and utility functions for managing entity columns.
- Implemented Playwright tests in `columns-ui.test.py` to validate column behavior and preferences.
- Added `visual-audit-volume.py` script to enhance the visual audit setup with synthetic data.
2026-10-22 10:10:34 +02:00

174 lines
6.0 KiB
TypeScript

import { Button } from "@repo/shadcn-ui/components/button";
import { Checkbox } from "@repo/shadcn-ui/components/checkbox";
import { Field, FieldLabel, FieldLegend, FieldSet } from "@repo/shadcn-ui/components/field";
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@repo/shadcn-ui/components/input-group";
import type { Table as TableModel } from "@tanstack/react-table";
import { useId, useState } from "react";
import type {
EntityColumnGroup,
EntityColumnMetadata,
EntityColumnOption,
} from "../lib/entity-column-configuration";
import { groupColumnOptions } from "../lib/entity-column-configuration";
import { entityListLabels } from "../lib/entity-list";
import { SearchIcon, XIcon } from "../lib/icons";
import { FormBody, FormDialog } from "./form-dialog";
export function EntityTableColumnsDialog<T>({
table,
locale,
open,
onOpenChange,
columnGroups = [],
columnMetadata = [],
onResetColumns,
}: {
table: TableModel<T>;
locale: string;
open: boolean;
onOpenChange: (open: boolean) => void;
columnGroups?: readonly EntityColumnGroup[] | undefined;
columnMetadata?: readonly (EntityColumnMetadata & { id: string })[] | undefined;
onResetColumns?: (() => void) | undefined;
}) {
const id = useId();
const labels = entityListLabels(locale);
const [search, setSearch] = useState("");
const metadata = new Map([...columnMetadata].reverse().map((column) => [column.id, column]));
// La selección es un control interno del picker, no una columna de datos configurable.
const options: EntityColumnOption[] = table
.getAllLeafColumns()
.filter((column) => column.id !== "selection")
.map((column) => ({
...metadata.get(column.id),
id: column.id,
label: String(column.columnDef.header),
visible: column.getIsVisible(),
hideable: column.getCanHide(),
}));
const groups = groupColumnOptions(options, columnGroups, search);
const visible = options.filter((column) => column.visible).length;
const matches = groups.reduce((total, group) => total + group.options.length, 0);
function changeOpen(nextOpen: boolean) {
setSearch("");
onOpenChange(nextOpen);
}
return (
<FormDialog
cancelLabel={labels.close}
layout="picker"
onOpenChange={changeOpen}
open={open}
title={labels.columns}
>
<div className="flex shrink-0 flex-col gap-3 px-1 pb-3">
<Field>
<FieldLabel className="sr-only" htmlFor={`${id}-search`}>
{labels.searchColumns}
</FieldLabel>
<InputGroup>
<InputGroupAddon>
<SearchIcon aria-hidden="true" />
</InputGroupAddon>
<InputGroupInput
data-quick-search=""
id={`${id}-search`}
onChange={(event) => setSearch(event.target.value)}
placeholder={labels.searchColumns}
value={search}
/>
<InputGroupAddon align="inline-end">
<InputGroupButton
aria-label={labels.clearColumnSearch}
disabled={!search}
onClick={() => {
setSearch("");
document.getElementById(`${id}-search`)?.focus();
}}
size="icon-sm"
>
<XIcon aria-hidden="true" />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</Field>
<div className="flex flex-wrap items-center justify-between gap-2">
<p aria-live="polite" className="text-sm text-muted-foreground">
{labels.visibleColumns(visible, options.length)}
</p>
<Button
onClick={() => (onResetColumns ? onResetColumns() : table.resetColumnVisibility())}
size="sm"
type="button"
variant="outline"
>
{labels.restoreColumns}
</Button>
</div>
</div>
<FormBody>
<span className="sr-only" id={`${id}-required`}>
{labels.requiredColumnHelp}
</span>
{search.trim() && (
<p aria-live="polite" className="text-sm text-muted-foreground">
{labels.matchingColumns(matches)}
</p>
)}
{matches === 0 && <p role="status">{labels.noMatchingColumns}</p>}
{groups.map(({ group, options: groupOptions }) => (
<FieldSet
aria-describedby={group?.description ? `${id}-description-${group.id}` : undefined}
className="shrink-0 gap-1 pb-3"
key={group ? `group-${group.id}` : "ungrouped"}
>
{(group || columnGroups.length > 0) && (
<FieldLegend variant="label">{group?.label ?? labels.otherColumns}</FieldLegend>
)}
{group?.description && (
<p
className="mb-2 text-sm text-muted-foreground"
id={`${id}-description-${group.id}`}
>
{group.description}
</p>
)}
{groupOptions.map((column) => (
<label
className="flex min-h-9 items-center gap-3 rounded px-1 py-1"
htmlFor={`${id}-column-${column.id}`}
key={column.id}
>
<Checkbox
aria-describedby={column.hideable ? undefined : `${id}-required`}
checked={column.visible}
disabled={!column.hideable}
id={`${id}-column-${column.id}`}
onCheckedChange={(checked) =>
table.getColumn(column.id)?.toggleVisibility(checked === true)
}
/>
<span className="min-w-0 flex-1 break-words">{column.label}</span>
{!column.hideable && (
<span aria-hidden="true" className="text-xs text-muted-foreground">
{labels.requiredColumn}
</span>
)}
</label>
))}
</FieldSet>
))}
</FormBody>
</FormDialog>
);
}