import { Button } from "@repo/shadcn-ui/components/button"; import { Input } from "@repo/shadcn-ui/components/input"; import type { ReactNode } from "react"; import { useId, useRef } from "react"; import { useEntityTable } from "../hooks/use-entity-table"; import type { EntityColumnGroup, EntityColumnMetadata } from "../lib/entity-column-configuration"; import { entityListLabels } from "../lib/entity-list"; import type { EntityListState } from "../lib/entity-list-state"; import { entityTableAvailability } from "../lib/entity-list-state"; import type { EntityTableNavigation } from "../lib/entity-table-navigation"; import { EntityListPagination } from "./entity-list-controls"; import { EntityListStatus } from "./entity-list-status"; import { EntityTableConfiguration } from "./entity-table-configuration"; import { EntityTableContent } from "./entity-table-content"; export { EntityAction, EntityRowActions, EntityStatus } from "./entity-list-controls"; export function EntityListHeader({ title, description, icon, actions, locale = "es", }: { title: string; description?: string; icon?: ReactNode; actions?: ReactNode; locale?: string; }) { return ( {icon && ( {icon} )} {title} {description ?? `${entityListLabels(locale).description} ${title.toLocaleLowerCase(locale)}.`} {actions && {actions}} ); } export type EntityColumn = EntityColumnMetadata & { id: string; label: string; render: (item: T) => ReactNode; value?: (item: T) => string | number | boolean | null | undefined; defaultSort?: "asc" | "desc"; hideable?: boolean; defaultVisible?: boolean; }; export function EntityTable({ items, columns, title, entityLabel = title, locale = "es", search, onSearchChange, searchPlaceholder, filters, filtersApplied = false, onClearFilters, filterKey = "", columnVisibilityStorageKey, columnGroups, state, navigation, onRetry, columnsDisabled, loading = false, unavailable = false, emptyMessage, rowClassName, onRowClick, layout = "content", }: { items: readonly T[]; columns: readonly EntityColumn[]; title: string; entityLabel?: string | ((count: number) => string); locale?: string; search?: string; onSearchChange?: (value: string) => void; searchPlaceholder?: string; filters?: ReactNode; filtersApplied?: boolean; onClearFilters?: () => void; filterKey?: string; columnVisibilityStorageKey?: string | undefined; columnGroups?: readonly EntityColumnGroup[]; state?: EntityListState | undefined; navigation?: EntityTableNavigation | undefined; onRetry?: (() => Promise) | undefined; columnsDisabled?: boolean | undefined; loading?: boolean; unavailable?: boolean; emptyMessage?: string; rowClassName?: (item: T) => string; onRowClick?: ((item: T) => void) | undefined; layout?: "content" | "fill"; }) { const id = useId(); const searchRef = useRef(null); const labels = entityListLabels(locale); const availability = entityTableAvailability(state, loading, unavailable); const pending = availability.loading; const missing = availability.unavailable; const { table, total, range, pagination, setPagination, currentSearch, setLocalSearch } = useEntityTable({ items, columns, locale, search, onSearchChange, filterKey, columnVisibilityStorageKey, navigation, }); const info = entityTableInfo( pending, missing, labels.loading, labels.range( range.from, range.to, total, entityTableLabel(entityLabel, total, locale), layout === "fill" ? "compact" : "full" ) ); const message = entityTableMessage( pending, missing, Boolean(currentSearch || filtersApplied), labels, emptyMessage ); const clear = () => { (onSearchChange ?? setLocalSearch)(""); onClearFilters?.(); setPagination((previous) => ({ ...previous, pageIndex: 0 })); }; function resetSortPage() { if (!navigation) { setPagination((previous) => ({ ...previous, pageIndex: 0 })); } } const configuration = ( table.setColumnVisibility({})} onSort={resetSortPage} presentation={layout === "fill" ? "picker" : "full"} table={table} /> ); return ( [data-slot=table-container]]:min-h-0 [&>[data-slot=table-container]]:flex-1 [&>[data-slot=table-container]]:overflow-auto [&>[data-slot=table-container]]:overscroll-contain [&>[data-slot=table-container]]:[scrollbar-gutter:stable] [&_[data-slot=table-header]]:sticky [&_[data-slot=table-header]]:top-0 [&_[data-slot=table-header]]:z-20 [&_[data-slot=table-header]]:bg-background" : ""}`} data-query-state={state?.status} data-slot="entity-table" > {searchPlaceholder ?? labels.search} (onSearchChange ?? setLocalSearch)(event.target.value)} placeholder={searchPlaceholder ?? labels.placeholder} ref={searchRef} value={currentSearch} /> {layout === "fill" && configuration} {filters} {(currentSearch || filtersApplied) && ( {labels.clear} )} {layout !== "fill" && ( searchRef.current} retryLabel={labels.retry} state={availability.summaryState} > {info} {configuration} )} ); } function entityTableInfo( loading: boolean, unavailable: boolean, loadingLabel: string, info: string ) { if (loading) { return loadingLabel; } return unavailable ? "—" : info; } function entityTableLabel( label: string | ((count: number) => string), total: number, locale: string ) { return (typeof label === "function" ? label(total) : label).toLocaleLowerCase(locale); } function entityTableMessage( loading: boolean, unavailable: boolean, filtered: boolean, labels: ReturnType, emptyMessage: string | undefined ) { if (loading) { return labels.loading; } if (unavailable) { return labels.unavailable; } return filtered ? labels.noResults : (emptyMessage ?? labels.empty); }
{description ?? `${entityListLabels(locale).description} ${title.toLocaleLowerCase(locale)}.`}