- 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.
174 lines
6.0 KiB
TypeScript
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>
|
|
);
|
|
}
|