Files
kandev/apps/web/components/ui/data-table-pagination.tsx
T
João Salavisa f09e938673 feat(i18n): localize the last batch of live surfaces before Office (#2357)
Migrates the eleven remaining files that `i18next/no-literal-string` flagged
outside `app/office`, `app/demo` and the task-create surfaces owned by #2326:
the session prepare panel, the onboarding agents step, the host system-metrics
readout, the system-health issues dialog, the shared panel search bar, the
mermaid diagram block, the settings agent card, both release-notes surfaces,
and the shared data-table pair.

Findings drop 884 -> 826 (-58) and the guard allowlist grows 442 -> 453, file
by file rather than by directory since every one of these directories still
holds un-migrated files.

Defects repaired beyond the codemod's output:

- `data-table-pagination.tsx` rendered `{count} results` as a bare JSX
  fragment, invisible to both plural gates. It is now one interpolated key
  with `_one`/`_other`, as are the health-dialog issue count and the
  release-notes "N new releases" helper.
- Two mixed literal+expression blocks were half-converted, rendering
  identically in English while being structurally untranslatable. Both were
  rebuilt as a single message.
- The codemod re-sorted `settings.json` into `localeCompare` order and
  re-encoded every non-ASCII character to `\uXXXX` under a comment claiming it
  matched the existing files. No catalog is stored escaped and only `common`
  is in that order, so both halves were reverted; all four catalogs are
  additions-only.
- Copy in default parameters and plain helpers is invisible to the lint rule,
  so `Search…`, the six prepare-panel header labels, the setup-script step name
  and error, the metric labels, `lastUpdatedText` and `buildDescription` were
  migrated by hand.

`task:preparingEnvironment` was already taken by #2326 holding "Preparing
environment" without the ellipsis this surface renders, so the prepare panel
uses `preparingEnvironmentEllipsis` rather than silently changing its copy.

Units, wire ids, the `{workspaceId}` URL token, the `"ellipsis"` page sentinel
and every mermaid theme/CSS value stay English as protocol.
2026-08-07 11:34:35 +02:00

173 lines
5.6 KiB
TypeScript

"use client";
import { type Table } from "@tanstack/react-table";
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
} from "@kandev/ui/pagination";
import { Button } from "@kandev/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@kandev/ui/select";
import {
IconChevronLeft,
IconChevronRight,
IconChevronsLeft,
IconChevronsRight,
} from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
interface DataTablePaginationProps<TData> {
table: Table<TData>;
}
function getPageNumbers(currentPage: number, pageCount: number): (number | "ellipsis")[] {
const pages: (number | "ellipsis")[] = [];
const showPages = 5;
if (pageCount <= showPages) {
for (let i = 1; i <= pageCount; i++) pages.push(i);
return pages;
}
pages.push(1);
if (currentPage > 3) pages.push("ellipsis");
const start = Math.max(2, currentPage - 1);
const end = Math.min(pageCount - 1, currentPage + 1);
for (let i = start; i <= end; i++) pages.push(i);
if (currentPage < pageCount - 2) pages.push("ellipsis");
if (pageCount > 1) pages.push(pageCount);
return pages;
}
function PageButtons({
pages,
currentPage,
onPageChange,
}: {
pages: (number | "ellipsis")[];
currentPage: number;
onPageChange: (page: number) => void;
}) {
return (
<>
{pages.map((page, idx) =>
page === "ellipsis" ? (
<PaginationItem key={`ellipsis-${idx}`}>
<PaginationEllipsis />
</PaginationItem>
) : (
<PaginationItem key={page}>
<Button
variant={page === currentPage ? "outline" : "ghost"}
size="icon"
className="cursor-pointer"
onClick={() => onPageChange(page - 1)}
>
{page}
</Button>
</PaginationItem>
),
)}
</>
);
}
export function DataTablePagination<TData>({ table }: DataTablePaginationProps<TData>) {
const { t } = useTranslation();
const currentPage = table.getState().pagination.pageIndex + 1;
const pageCount = table.getPageCount();
const pages = getPageNumbers(currentPage, pageCount);
const pagination = table.getState().pagination;
return (
<div className="flex items-center justify-between px-2">
<div className="flex-1 text-sm text-muted-foreground">
{table.getFilteredRowModel().rows.length > 0 &&
t("common:showingRange", {
from: pagination.pageIndex * pagination.pageSize + 1,
to: Math.min((pagination.pageIndex + 1) * pagination.pageSize, table.getRowCount()),
count: table.getRowCount(),
})}
</div>
<div className="flex items-center space-x-6 lg:space-x-8">
<div className="flex items-center space-x-2">
<p className="text-sm font-medium whitespace-nowrap">{t("common:rowsPerPage")}</p>
<Select
value={`${pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value));
}}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue placeholder={pagination.pageSize} />
</SelectTrigger>
<SelectContent side="top">
{[10, 25, 50].map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Pagination>
<PaginationContent>
<PaginationItem>
<Button
variant="ghost"
size="icon"
className="hidden lg:flex cursor-pointer"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">{t("common:goToFirstPage")}</span>
<IconChevronsLeft className="h-4 w-4" />
</Button>
</PaginationItem>
<PaginationItem>
<Button
variant="ghost"
size="icon"
className="cursor-pointer"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">{t("common:goToPreviousPage")}</span>
<IconChevronLeft className="h-4 w-4" />
</Button>
</PaginationItem>
<PageButtons
pages={pages}
currentPage={currentPage}
onPageChange={(p) => table.setPageIndex(p)}
/>
<PaginationItem>
<Button
variant="ghost"
size="icon"
className="cursor-pointer"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">{t("common:goToNextPage")}</span>
<IconChevronRight className="h-4 w-4" />
</Button>
</PaginationItem>
<PaginationItem>
<Button
variant="ghost"
size="icon"
className="hidden lg:flex cursor-pointer"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">{t("common:goToLastPage")}</span>
<IconChevronsRight className="h-4 w-4" />
</Button>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
</div>
);
}