WiredPagedTable.tsx 8.2 Ko
Télécharger le fichier
import { ReactNode, useEffect, useState } from 'react';
import { localizeWithFallback } from '../../api';
import { WiredPageRequests } from '../../hooks/wired-tools/useWiredPageRequests';
import { clampInputPage, DEFAULT_PAGING_TEXT, NO_PAGE, parseInputPage, restrictPageInput, splitPagingText } from './WiredPaging.helpers';
import { BaseButton } from '../../common/BaseButton';
import { Hint } from '../../common/Tooltip';
import { NativeTextTree } from '../../common/native-text/NativeTextTree';
export interface WiredTableColumn {
id: string;
title: string;
/** Tailwind width class of the header cell, e.g. `w-[120px]`; the last column takes the rest. */
className?: string;
align?: 'left' | 'right';
}
export interface WiredTableCell {
content: ReactNode;
/** The full text shown on hover when the cell is cut short. */
title?: string;
className?: string;
}
export interface WiredPagedTableProps<T> {
columns: WiredTableColumn[];
rows: T[];
getRowId: (row: T) => string;
getCell: (row: T, columnId: string) => WiredTableCell;
/** 1-based; `NO_PAGE` while no page has arrived. */
currentPage: number;
totalEntries: number;
lastPage: number;
requests: WiredPageRequests;
emptyText: string;
/** Bumped by the window when a page the user asked for should scroll back to the top. */
scrollResetKey?: number;
/** Height class of the scrolling body. */
bodyClassName?: string;
/** The window's footer text, e.g. `wiredmenu.logs_overview.bottom_text`, with `%page%` where the page field sits. */
pagingTextKey?: string;
}
const PAGE_BUTTON_CLASS = 'min-w-[36px] rounded border border-[#b8b2a4] bg-white px-2 py-1 text-[12px] disabled:opacity-40';
/**
* A table over a pagination footer: first / previous / a page field / next / last, and the
* "N found. Showing page [ ] of M" line. The caller says which page is shown and hands in the
* rate limited requests, so the header's own refresh button and filters share the limiter.
*/
export const WiredPagedTable = <T,>(props: WiredPagedTableProps<T>) => {
const { columns, rows, getRowId, getCell, currentPage, totalEntries, lastPage, requests, emptyText, scrollResetKey = 0, bodyClassName = 'max-h-[300px]', pagingTextKey = '' } = props;
const [pageInput, setPageInput] = useState(currentPage === NO_PAGE ? '1' : String(currentPage));
const [bodyElement, setBodyElement] = useState<HTMLDivElement | null>(null);
useEffect(() => {
setPageInput(currentPage === NO_PAGE ? '1' : String(currentPage));
}, [currentPage]);
useEffect(() => {
if (bodyElement) bodyElement.scrollTop = 0;
}, [scrollResetKey, bodyElement]);
const isFirst = currentPage !== NO_PAGE && currentPage <= 1;
const isLast = currentPage !== NO_PAGE && lastPage !== NO_PAGE && currentPage >= lastPage;
const pagingText = currentPage === NO_PAGE ? null : splitPagingText(localizeWithFallback(pagingTextKey, DEFAULT_PAGING_TEXT), totalEntries, Math.max(1, lastPage));
const navigateToInputPage = () => {
const typed = parseInputPage(pageInput);
const clamped = clampInputPage(typed, lastPage);
if (clamped !== typed) setPageInput(String(clamped));
if (clamped === currentPage) return;
requests.requestPage(clamped);
};
return (
<NativeTextTree>
<div className="flex flex-col gap-2 min-h-0 grow">
<div ref={setBodyElement} className={`overflow-y-auto border border-[#d1ccbf] rounded bg-white ${bodyClassName}`} data-testid="wired-paged-table-body">
<table className="w-full text-[12px] table-fixed">
<thead className="bg-[#efede5] sticky top-0">
<tr>
{columns.map((column) => (
<th key={column.id} className={`px-2 py-1 ${column.align === 'right' ? 'text-right' : 'text-left'} ${column.className ?? ''}`}>
{column.title}
</th>
))}
</tr>
</thead>
<tbody>
{!rows.length && (
<tr>
<td className="px-2 py-3 text-center text-[#8b8678]" colSpan={columns.length}>
{emptyText}
</td>
</tr>
)}
{rows.map((row, index) => (
<tr key={getRowId(row)} className={index % 2 === 0 ? 'bg-white' : 'bg-[#f8f6f0]'}>
{columns.map((column) => {
const cell = getCell(row, column.id);
return (
<td
key={column.id}
className={`px-2 py-1 truncate ${column.align === 'right' ? 'text-right' : ''} ${cell.className ?? ''}`}
title={cell.title}
>
{cell.content}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
<div className="flex items-center justify-between gap-3">
<div className="flex gap-2">
<Hint content="First page">
<BaseButton aria-label="First page" className={PAGE_BUTTON_CLASS} disabled={isFirst} type="button" onClick={() => requests.requestFirstPage()}>
«
</BaseButton>
</Hint>
<Hint content="Previous page">
<BaseButton aria-label="Previous page" className={PAGE_BUTTON_CLASS} disabled={isFirst} type="button" onClick={() => requests.requestPreviousPage()}>
‹
</BaseButton>
</Hint>
</div>
<div className="flex items-center gap-2 text-[12px] text-[#555]">
{pagingText && <span>{pagingText.start}</span>}
<input
aria-label="Page"
className="w-[42px] rounded border border-[#b8b2a4] bg-white px-1 py-[2px] text-center text-[12px]"
type="text"
inputMode="numeric"
value={pageInput}
onChange={(event) => setPageInput(restrictPageInput(event.target.value))}
onBlur={navigateToInputPage}
onKeyDown={(event) => {
if (event.key !== 'Enter') return;
event.preventDefault();
navigateToInputPage();
}}
/>
{pagingText && <span>{pagingText.end}</span>}
</div>
<div className="flex gap-2">
<Hint content="Next page">
<BaseButton aria-label="Next page" className={PAGE_BUTTON_CLASS} disabled={isLast} type="button" onClick={() => requests.requestNextPage()}>
›
</BaseButton>
</Hint>
<Hint content="Last page">
<BaseButton aria-label="Last page" className={PAGE_BUTTON_CLASS} disabled={isLast} type="button" onClick={() => requests.requestLastPage()}>
»
</BaseButton>
</Hint>
</div>
</div>
</div>
</NativeTextTree>
);
};
