mirror of
				https://gitlab.nic.cz/turris/reforis/foris-js.git
				synced 2025-11-03 23:00:31 +01:00 
			
		
		
		
	Add RichTable component with header, body, and pagination
This commit is contained in:
		
							
								
								
									
										70
									
								
								src/common/RichTable/RichTable.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/common/RichTable/RichTable.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,70 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
 | 
			
		||||
 *
 | 
			
		||||
 * This is free software, licensed under the GNU General Public License v3.
 | 
			
		||||
 * See /LICENSE for more information.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import React, { useMemo, useState } from "react";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    flexRender,
 | 
			
		||||
    getCoreRowModel,
 | 
			
		||||
    getSortedRowModel,
 | 
			
		||||
    getPaginationRowModel,
 | 
			
		||||
    useReactTable,
 | 
			
		||||
} from "@tanstack/react-table";
 | 
			
		||||
 | 
			
		||||
import RichTableBody from "./RichTableBody";
 | 
			
		||||
import RichTableHeader from "./RichTableHeader";
 | 
			
		||||
import RichTablePagination from "./RichTablePagination";
 | 
			
		||||
 | 
			
		||||
const fallbackData = [];
 | 
			
		||||
 | 
			
		||||
const RichTable = ({
 | 
			
		||||
    columns,
 | 
			
		||||
    data,
 | 
			
		||||
    withPagination,
 | 
			
		||||
    pageSize = 5,
 | 
			
		||||
    pageIndex = 0,
 | 
			
		||||
}) {
 | 
			
		||||
    const tableColumns = useMemo(() => columns, [columns]);
 | 
			
		||||
    const [tableData] = useState(data ?? fallbackData);
 | 
			
		||||
    const [sorting, setSorting] = useState([]);
 | 
			
		||||
    const [pagination, setPagination] = useState({
 | 
			
		||||
        pageIndex,
 | 
			
		||||
        pageSize,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const table = useReactTable({
 | 
			
		||||
        data: tableData,
 | 
			
		||||
        columns: tableColumns,
 | 
			
		||||
        getCoreRowModel: getCoreRowModel(),
 | 
			
		||||
        getSortedRowModel: getSortedRowModel(),
 | 
			
		||||
        getPaginationRowModel: getPaginationRowModel(),
 | 
			
		||||
        onPaginationChange: setPagination,
 | 
			
		||||
        onSortingChange: setSorting,
 | 
			
		||||
        state: {
 | 
			
		||||
            sorting,
 | 
			
		||||
            pagination,
 | 
			
		||||
        },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div className="table-responsive">
 | 
			
		||||
            <table className="table table-hover text-nowrap">
 | 
			
		||||
                <RichTableHeader table={table} flexRender={flexRender} />
 | 
			
		||||
                <RichTableBody table={table} flexRender={flexRender} />
 | 
			
		||||
            </table>
 | 
			
		||||
            {withPagination && (
 | 
			
		||||
                <RichTablePagination
 | 
			
		||||
                    table={table}
 | 
			
		||||
                    tablePageSize={pageSize}
 | 
			
		||||
                    allRows={tableData.length}
 | 
			
		||||
                />
 | 
			
		||||
            )}
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default RichTable;
 | 
			
		||||
							
								
								
									
										48
									
								
								src/common/RichTable/RichTableBody.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/common/RichTable/RichTableBody.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
 | 
			
		||||
 *
 | 
			
		||||
 * This is free software, licensed under the GNU General Public License v3.
 | 
			
		||||
 * See /LICENSE for more information.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
import propTypes from "prop-types";
 | 
			
		||||
 | 
			
		||||
RichTableBody.propTypes = {
 | 
			
		||||
    table: propTypes.shape({
 | 
			
		||||
        getRowModel: propTypes.func.isRequired,
 | 
			
		||||
    }).isRequired,
 | 
			
		||||
    flexRender: propTypes.func.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function RichTableBody({ table, flexRender }) {
 | 
			
		||||
    return (
 | 
			
		||||
        <tbody>
 | 
			
		||||
            {table.getRowModel().rows.map((row) => {
 | 
			
		||||
                return (
 | 
			
		||||
                    <tr key={row.id} className="align-middle">
 | 
			
		||||
                        {row.getVisibleCells().map((cell) => {
 | 
			
		||||
                            return (
 | 
			
		||||
                                <td
 | 
			
		||||
                                    key={cell.id}
 | 
			
		||||
                                    {...(cell.column.columnDef.className && {
 | 
			
		||||
                                        className:
 | 
			
		||||
                                            cell.column.columnDef.className,
 | 
			
		||||
                                    })}
 | 
			
		||||
                                >
 | 
			
		||||
                                    {flexRender(
 | 
			
		||||
                                        cell.column.columnDef.cell,
 | 
			
		||||
                                        cell.getContext()
 | 
			
		||||
                                    )}
 | 
			
		||||
                                </td>
 | 
			
		||||
                            );
 | 
			
		||||
                        })}
 | 
			
		||||
                    </tr>
 | 
			
		||||
                );
 | 
			
		||||
            })}
 | 
			
		||||
        </tbody>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default RichTableBody;
 | 
			
		||||
							
								
								
									
										96
									
								
								src/common/RichTable/RichTableHeader.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								src/common/RichTable/RichTableHeader.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,96 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
 | 
			
		||||
 *
 | 
			
		||||
 * This is free software, licensed under the GNU General Public License v3.
 | 
			
		||||
 * See /LICENSE for more information.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    faSquareCaretUp,
 | 
			
		||||
    faSquareCaretDown,
 | 
			
		||||
} from "@fortawesome/free-solid-svg-icons";
 | 
			
		||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 | 
			
		||||
import propTypes from "prop-types";
 | 
			
		||||
 | 
			
		||||
RichTableHeader.propTypes = {
 | 
			
		||||
    table: propTypes.shape({
 | 
			
		||||
        getHeaderGroups: propTypes.func.isRequired,
 | 
			
		||||
    }).isRequired,
 | 
			
		||||
    flexRender: propTypes.func.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function RichTableHeader({ table, flexRender }) {
 | 
			
		||||
    const getThTitle = (header) => {
 | 
			
		||||
        if (!header.column.getCanSort()) return undefined;
 | 
			
		||||
 | 
			
		||||
        const nextSortingOrder = header.column.getNextSortingOrder();
 | 
			
		||||
        if (nextSortingOrder === "asc") return _("Sort ascending");
 | 
			
		||||
        if (nextSortingOrder === "desc") return _("Sort descending");
 | 
			
		||||
        return _("Clear sort");
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <thead className="thead-light">
 | 
			
		||||
            {table.getHeaderGroups().map((headerGroup) => (
 | 
			
		||||
                <tr key={headerGroup.id} role="row">
 | 
			
		||||
                    {headerGroup.headers.map((header) => (
 | 
			
		||||
                        <th
 | 
			
		||||
                            key={header.id}
 | 
			
		||||
                            colSpan={header.colSpan}
 | 
			
		||||
                            {...(header.column.columnDef.headerClassName && {
 | 
			
		||||
                                className:
 | 
			
		||||
                                    header.column.columnDef.headerClassName,
 | 
			
		||||
                            })}
 | 
			
		||||
                        >
 | 
			
		||||
                            {header.isPlaceholder ||
 | 
			
		||||
                            header.column.columnDef.headerIsHidden ? (
 | 
			
		||||
                                <div className="d-none" aria-hidden="true">
 | 
			
		||||
                                    {flexRender(
 | 
			
		||||
                                        header.column.columnDef.header,
 | 
			
		||||
                                        header.getContext()
 | 
			
		||||
                                    )}
 | 
			
		||||
                                </div>
 | 
			
		||||
                            ) : (
 | 
			
		||||
                                <button
 | 
			
		||||
                                    type="button"
 | 
			
		||||
                                    className={`btn btn-link text-decoration-none text-reset fw-bold p-0 d-flex align-items-center
 | 
			
		||||
                                                    ${
 | 
			
		||||
                                                        header.column.getCanSort()
 | 
			
		||||
                                                            ? "d-flex align-items-center"
 | 
			
		||||
                                                            : ""
 | 
			
		||||
                                                    }
 | 
			
		||||
                                                `}
 | 
			
		||||
                                    onClick={header.column.getToggleSortingHandler()}
 | 
			
		||||
                                    title={getThTitle(header)}
 | 
			
		||||
                                >
 | 
			
		||||
                                    {flexRender(
 | 
			
		||||
                                        header.column.columnDef.header,
 | 
			
		||||
                                        header.getContext()
 | 
			
		||||
                                    )}
 | 
			
		||||
                                    {{
 | 
			
		||||
                                        asc: (
 | 
			
		||||
                                            <FontAwesomeIcon
 | 
			
		||||
                                                icon={faSquareCaretUp}
 | 
			
		||||
                                                className="ms-1 text-primary"
 | 
			
		||||
                                            />
 | 
			
		||||
                                        ),
 | 
			
		||||
                                        desc: (
 | 
			
		||||
                                            <FontAwesomeIcon
 | 
			
		||||
                                                icon={faSquareCaretDown}
 | 
			
		||||
                                                className="ms-1 text-primary"
 | 
			
		||||
                                            />
 | 
			
		||||
                                        ),
 | 
			
		||||
                                    }[header.column.getIsSorted()] ?? null}
 | 
			
		||||
                                </button>
 | 
			
		||||
                            )}
 | 
			
		||||
                        </th>
 | 
			
		||||
                    ))}
 | 
			
		||||
                </tr>
 | 
			
		||||
            ))}
 | 
			
		||||
        </thead>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default RichTableHeader;
 | 
			
		||||
							
								
								
									
										128
									
								
								src/common/RichTable/RichTablePagination.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								src/common/RichTable/RichTablePagination.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,128 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
 | 
			
		||||
 *
 | 
			
		||||
 * This is free software, licensed under the GNU General Public License v3.
 | 
			
		||||
 * See /LICENSE for more information.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import React, { useMemo } from "react";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    faAngleLeft,
 | 
			
		||||
    faAnglesLeft,
 | 
			
		||||
    faAngleRight,
 | 
			
		||||
    faAnglesRight,
 | 
			
		||||
} from "@fortawesome/free-solid-svg-icons";
 | 
			
		||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 | 
			
		||||
import propTypes from "prop-types";
 | 
			
		||||
 | 
			
		||||
RichTablePagination.propTypes = {
 | 
			
		||||
    table: propTypes.shape({
 | 
			
		||||
        getState: propTypes.func.isRequired,
 | 
			
		||||
        getCanPreviousPage: propTypes.func.isRequired,
 | 
			
		||||
        getCanNextPage: propTypes.func.isRequired,
 | 
			
		||||
        firstPage: propTypes.func.isRequired,
 | 
			
		||||
        previousPage: propTypes.func.isRequired,
 | 
			
		||||
        nextPage: propTypes.func.isRequired,
 | 
			
		||||
        lastPage: propTypes.func.isRequired,
 | 
			
		||||
        setPageSize: propTypes.func.isRequired,
 | 
			
		||||
        getPageCount: propTypes.func.isRequired,
 | 
			
		||||
    }).isRequired,
 | 
			
		||||
    tablePageSize: propTypes.number,
 | 
			
		||||
    allRows: propTypes.number,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function RichTablePagination({ table, tablePageSize, allRows }) {
 | 
			
		||||
    const { pagination } = table.getState();
 | 
			
		||||
    const prevPagBtnDisabled = !table.getCanPreviousPage();
 | 
			
		||||
    const nextPagBtnDisabled = !table.getCanNextPage();
 | 
			
		||||
 | 
			
		||||
    const pageSizes = useMemo(() => {
 | 
			
		||||
        return [tablePageSize ?? 5, 10, 25].filter(
 | 
			
		||||
            (value, index, self) => self.indexOf(value) === index
 | 
			
		||||
        );
 | 
			
		||||
    }, [tablePageSize]);
 | 
			
		||||
 | 
			
		||||
    const renderPaginationButton = (icon, ariaLabel, onClick, disabled) => (
 | 
			
		||||
        <li
 | 
			
		||||
            className={`page-item ${disabled ? "disabled" : ""}`}
 | 
			
		||||
            style={{ cursor: disabled ? "not-allowed" : "pointer" }}
 | 
			
		||||
        >
 | 
			
		||||
            <button
 | 
			
		||||
                type="button"
 | 
			
		||||
                className="page-link"
 | 
			
		||||
                aria-label={ariaLabel}
 | 
			
		||||
                onClick={onClick}
 | 
			
		||||
                disabled={disabled}
 | 
			
		||||
            >
 | 
			
		||||
                <FontAwesomeIcon icon={icon} />
 | 
			
		||||
            </button>
 | 
			
		||||
        </li>
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <nav
 | 
			
		||||
            aria-label={_("Pagination navigation bar")}
 | 
			
		||||
            className="d-flex gap-2 justify-content-start align-items-center mx-2 mb-1 text-nowrap"
 | 
			
		||||
        >
 | 
			
		||||
            <ul className="pagination pagination-sm mb-0">
 | 
			
		||||
                {renderPaginationButton(
 | 
			
		||||
                    faAnglesLeft,
 | 
			
		||||
                    _("First page"),
 | 
			
		||||
                    () => table.firstPage(),
 | 
			
		||||
                    prevPagBtnDisabled
 | 
			
		||||
                )}
 | 
			
		||||
                {renderPaginationButton(
 | 
			
		||||
                    faAngleLeft,
 | 
			
		||||
                    _("Previous page"),
 | 
			
		||||
                    () => table.previousPage(),
 | 
			
		||||
                    prevPagBtnDisabled
 | 
			
		||||
                )}
 | 
			
		||||
                {renderPaginationButton(
 | 
			
		||||
                    faAngleRight,
 | 
			
		||||
                    _("Next page"),
 | 
			
		||||
                    () => table.nextPage(),
 | 
			
		||||
                    nextPagBtnDisabled
 | 
			
		||||
                )}
 | 
			
		||||
                {renderPaginationButton(
 | 
			
		||||
                    faAnglesRight,
 | 
			
		||||
                    _("Last page"),
 | 
			
		||||
                    () => table.lastPage(),
 | 
			
		||||
                    nextPagBtnDisabled
 | 
			
		||||
                )}
 | 
			
		||||
            </ul>
 | 
			
		||||
            <span>
 | 
			
		||||
                {_("Page")} 
 | 
			
		||||
                <span className="fw-bold">
 | 
			
		||||
                    {pagination.pageIndex + 1}
 | 
			
		||||
                     {_("of")} 
 | 
			
		||||
                    {table.getPageCount().toLocaleString()}
 | 
			
		||||
                </span>
 | 
			
		||||
            </span>
 | 
			
		||||
            <div
 | 
			
		||||
                className="vr mx-1 align-self-center"
 | 
			
		||||
                style={{ height: "1.5rem" }}
 | 
			
		||||
            />
 | 
			
		||||
            <span>{_("Rows per page:")}</span>
 | 
			
		||||
            <select
 | 
			
		||||
                className="form-select form-select-sm w-auto"
 | 
			
		||||
                aria-label={_("Select rows per page")}
 | 
			
		||||
                value={pagination.pageSize}
 | 
			
		||||
                onChange={(e) => {
 | 
			
		||||
                    table.setPageSize(Number(e.target.value));
 | 
			
		||||
                }}
 | 
			
		||||
            >
 | 
			
		||||
                {pageSizes.map((pageSize) => (
 | 
			
		||||
                    <option key={pageSize} value={pageSize}>
 | 
			
		||||
                        {pageSize}
 | 
			
		||||
                    </option>
 | 
			
		||||
                ))}
 | 
			
		||||
                <option key={allRows} value={allRows}>
 | 
			
		||||
                    {_("All")}
 | 
			
		||||
                </option>
 | 
			
		||||
            </select>
 | 
			
		||||
        </nav>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default RichTablePagination;
 | 
			
		||||
		Reference in New Issue
	
	Block a user