mirror of
				https://gitlab.nic.cz/turris/reforis/foris-js.git
				synced 2025-11-03 23:00:31 +01:00 
			
		
		
		
	Add global fuzzy search and columns visibility to RichTable
This commit is contained in:
		
							
								
								
									
										36
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										36
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -13,6 +13,7 @@
 | 
			
		||||
                "@fortawesome/free-regular-svg-icons": "^6.7.2",
 | 
			
		||||
                "@fortawesome/free-solid-svg-icons": "^6.7.2",
 | 
			
		||||
                "@fortawesome/react-fontawesome": "^0.2.2",
 | 
			
		||||
                "@tanstack/match-sorter-utils": "^8.19.4",
 | 
			
		||||
                "@tanstack/react-table": "^8.21.2",
 | 
			
		||||
                "axios": "^1.7.9",
 | 
			
		||||
                "immutability-helper": "^3.1.1",
 | 
			
		||||
@@ -3384,6 +3385,22 @@
 | 
			
		||||
                "@sinonjs/commons": "^3.0.0"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/@tanstack/match-sorter-utils": {
 | 
			
		||||
            "version": "8.19.4",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.19.4.tgz",
 | 
			
		||||
            "integrity": "sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==",
 | 
			
		||||
            "license": "MIT",
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
                "remove-accents": "0.5.0"
 | 
			
		||||
            },
 | 
			
		||||
            "engines": {
 | 
			
		||||
                "node": ">=12"
 | 
			
		||||
            },
 | 
			
		||||
            "funding": {
 | 
			
		||||
                "type": "github",
 | 
			
		||||
                "url": "https://github.com/sponsors/tannerlinsley"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/@tanstack/react-table": {
 | 
			
		||||
            "version": "8.21.2",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.2.tgz",
 | 
			
		||||
@@ -15865,6 +15882,12 @@
 | 
			
		||||
                "url": "https://opencollective.com/unified"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/remove-accents": {
 | 
			
		||||
            "version": "0.5.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz",
 | 
			
		||||
            "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==",
 | 
			
		||||
            "license": "MIT"
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/repeat-string": {
 | 
			
		||||
            "version": "1.6.1",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
 | 
			
		||||
@@ -20893,6 +20916,14 @@
 | 
			
		||||
                "@sinonjs/commons": "^3.0.0"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "@tanstack/match-sorter-utils": {
 | 
			
		||||
            "version": "8.19.4",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.19.4.tgz",
 | 
			
		||||
            "integrity": "sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==",
 | 
			
		||||
            "requires": {
 | 
			
		||||
                "remove-accents": "0.5.0"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "@tanstack/react-table": {
 | 
			
		||||
            "version": "8.21.2",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.2.tgz",
 | 
			
		||||
@@ -30193,6 +30224,11 @@
 | 
			
		||||
                "mdast-util-to-markdown": "^0.6.0"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "remove-accents": {
 | 
			
		||||
            "version": "0.5.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz",
 | 
			
		||||
            "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A=="
 | 
			
		||||
        },
 | 
			
		||||
        "repeat-string": {
 | 
			
		||||
            "version": "1.6.1",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@
 | 
			
		||||
        "@fortawesome/free-regular-svg-icons": "^6.7.2",
 | 
			
		||||
        "@fortawesome/free-solid-svg-icons": "^6.7.2",
 | 
			
		||||
        "@fortawesome/react-fontawesome": "^0.2.2",
 | 
			
		||||
        "@tanstack/match-sorter-utils": "^8.19.4",
 | 
			
		||||
        "@tanstack/react-table": "^8.21.2",
 | 
			
		||||
        "axios": "^1.7.9",
 | 
			
		||||
        "immutability-helper": "^3.1.1",
 | 
			
		||||
@@ -70,4 +71,4 @@
 | 
			
		||||
        "docs": "npx styleguidist build ",
 | 
			
		||||
        "docs:watch": "styleguidist server"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -34,12 +34,14 @@ const Input = forwardRef(
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <div className="mb-3">
 | 
			
		||||
                <label
 | 
			
		||||
                    className={`form-label ${labelClassName || ""}`.trim()}
 | 
			
		||||
                    htmlFor={uid}
 | 
			
		||||
                >
 | 
			
		||||
                    {label}
 | 
			
		||||
                </label>
 | 
			
		||||
                {label && (
 | 
			
		||||
                    <label
 | 
			
		||||
                        className={`form-label ${labelClassName || ""}`.trim()}
 | 
			
		||||
                        htmlFor={uid}
 | 
			
		||||
                    >
 | 
			
		||||
                        {label}
 | 
			
		||||
                    </label>
 | 
			
		||||
                )}
 | 
			
		||||
                <div className={`input-group ${groupClassName || ""}`.trim()}>
 | 
			
		||||
                    <input
 | 
			
		||||
                        className={`form-control ${inputClassName}`.trim()}
 | 
			
		||||
@@ -65,7 +67,7 @@ Input.displayName = "Input";
 | 
			
		||||
 | 
			
		||||
Input.propTypes = {
 | 
			
		||||
    type: PropTypes.string.isRequired,
 | 
			
		||||
    label: PropTypes.string.isRequired,
 | 
			
		||||
    label: PropTypes.string,
 | 
			
		||||
    helpText: PropTypes.string,
 | 
			
		||||
    error: PropTypes.string,
 | 
			
		||||
    className: PropTypes.string,
 | 
			
		||||
 
 | 
			
		||||
@@ -7,18 +7,22 @@
 | 
			
		||||
 | 
			
		||||
import React, { useMemo, useState } from "react";
 | 
			
		||||
 | 
			
		||||
import { rankItem } from "@tanstack/match-sorter-utils";
 | 
			
		||||
import {
 | 
			
		||||
    flexRender,
 | 
			
		||||
    getCoreRowModel,
 | 
			
		||||
    getSortedRowModel,
 | 
			
		||||
    getFilteredRowModel,
 | 
			
		||||
    getPaginationRowModel,
 | 
			
		||||
    useReactTable,
 | 
			
		||||
} from "@tanstack/react-table";
 | 
			
		||||
import PropTypes from "prop-types";
 | 
			
		||||
 | 
			
		||||
import RichTableBody from "./RichTableBody";
 | 
			
		||||
import RichTableColumnsDropdown from "./RichTableColumnsDropdown";
 | 
			
		||||
import RichTableHeader from "./RichTableHeader";
 | 
			
		||||
import RichTablePagination from "./RichTablePagination";
 | 
			
		||||
import Input from "../../bootstrap/Input";
 | 
			
		||||
 | 
			
		||||
RichTable.propTypes = {
 | 
			
		||||
    /** Columns to be displayed in the table */
 | 
			
		||||
@@ -46,36 +50,69 @@ export default function RichTable({
 | 
			
		||||
        pageIndex,
 | 
			
		||||
        pageSize,
 | 
			
		||||
    });
 | 
			
		||||
    const [globalFilter, setGlobalFilter] = useState("");
 | 
			
		||||
    const [columnVisibility, setColumnVisibility] = useState({});
 | 
			
		||||
 | 
			
		||||
    const table = useReactTable({
 | 
			
		||||
        data,
 | 
			
		||||
        columns: tableColumns,
 | 
			
		||||
        filterFns: {
 | 
			
		||||
            fuzzy: fuzzyFilter,
 | 
			
		||||
        },
 | 
			
		||||
        globalFilterFn: "fuzzy",
 | 
			
		||||
        getCoreRowModel: getCoreRowModel(),
 | 
			
		||||
        getSortedRowModel: getSortedRowModel(),
 | 
			
		||||
        getPaginationRowModel: getPaginationRowModel(),
 | 
			
		||||
        onPaginationChange: setPagination,
 | 
			
		||||
        getFilteredRowModel: getFilteredRowModel(),
 | 
			
		||||
        onSortingChange: setSorting,
 | 
			
		||||
        onPaginationChange: setPagination,
 | 
			
		||||
        onGlobalFilterChange: setGlobalFilter,
 | 
			
		||||
        onColumnVisibilityChange: setColumnVisibility,
 | 
			
		||||
        state: {
 | 
			
		||||
            sorting,
 | 
			
		||||
            pagination,
 | 
			
		||||
            globalFilter,
 | 
			
		||||
            columnVisibility,
 | 
			
		||||
        },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const paginationIsNeeded = data.length > pageSize && withPagination;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div className="table-responsive">
 | 
			
		||||
            <table className="table table-hover text-nowrap">
 | 
			
		||||
                <RichTableHeader table={table} flexRender={flexRender} />
 | 
			
		||||
                <RichTableBody table={table} flexRender={flexRender} />
 | 
			
		||||
            </table>
 | 
			
		||||
            {paginationIsNeeded && (
 | 
			
		||||
                <RichTablePagination
 | 
			
		||||
                    table={table}
 | 
			
		||||
                    tablePageSize={pageSize}
 | 
			
		||||
                    allRows={data.length}
 | 
			
		||||
        <div>
 | 
			
		||||
            <div className="d-flex justify-content-between align-items-center">
 | 
			
		||||
                <Input
 | 
			
		||||
                    className="me-3"
 | 
			
		||||
                    type="text"
 | 
			
		||||
                    placeholder={_("Search…")}
 | 
			
		||||
                    value={globalFilter ?? ""}
 | 
			
		||||
                    onChange={(e) => setGlobalFilter(String(e.target.value))}
 | 
			
		||||
                />
 | 
			
		||||
            )}
 | 
			
		||||
                <RichTableColumnsDropdown columns={table.getAllLeafColumns()} />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="table-responsive">
 | 
			
		||||
                <table className="table table-hover text-nowrap">
 | 
			
		||||
                    <RichTableHeader table={table} flexRender={flexRender} />
 | 
			
		||||
                    <RichTableBody
 | 
			
		||||
                        table={table}
 | 
			
		||||
                        columns={tableColumns}
 | 
			
		||||
                        flexRender={flexRender}
 | 
			
		||||
                    />
 | 
			
		||||
                </table>
 | 
			
		||||
                {paginationIsNeeded && (
 | 
			
		||||
                    <RichTablePagination
 | 
			
		||||
                        table={table}
 | 
			
		||||
                        tablePageSize={pageSize}
 | 
			
		||||
                        allRows={data.length}
 | 
			
		||||
                    />
 | 
			
		||||
                )}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function fuzzyFilter(row, columnId, value, addMeta) {
 | 
			
		||||
    const itemRank = rankItem(row.getValue(columnId), value);
 | 
			
		||||
    addMeta({ itemRank });
 | 
			
		||||
    return itemRank.passed;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,34 +13,44 @@ RichTableBody.propTypes = {
 | 
			
		||||
    table: propTypes.shape({
 | 
			
		||||
        getRowModel: propTypes.func.isRequired,
 | 
			
		||||
    }).isRequired,
 | 
			
		||||
    columns: propTypes.array.isRequired,
 | 
			
		||||
    flexRender: propTypes.func.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function RichTableBody({ table, flexRender }) {
 | 
			
		||||
function RichTableBody({ table, columns, 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>
 | 
			
		||||
                );
 | 
			
		||||
            })}
 | 
			
		||||
            {table.getRowModel().rows?.length ? (
 | 
			
		||||
                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>
 | 
			
		||||
                    );
 | 
			
		||||
                })
 | 
			
		||||
            ) : (
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td colSpan={columns.length} className="text-center py-4">
 | 
			
		||||
                        <span>{_("No results.")}</span>
 | 
			
		||||
                    </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
            )}
 | 
			
		||||
        </tbody>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										90
									
								
								src/common/RichTable/RichTableColumnsDropdown.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								src/common/RichTable/RichTableColumnsDropdown.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,90 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2019-2025 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 { faCheck, faRotateLeft } from "@fortawesome/free-solid-svg-icons";
 | 
			
		||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 | 
			
		||||
import PropTypes from "prop-types";
 | 
			
		||||
 | 
			
		||||
import Button from "../../bootstrap/Button";
 | 
			
		||||
 | 
			
		||||
RichTableColumnsDropdown.propTypes = {
 | 
			
		||||
    columns: PropTypes.array.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function RichTableColumnsDropdown({ columns }) {
 | 
			
		||||
    return (
 | 
			
		||||
        <div className="dropdown mb-3">
 | 
			
		||||
            <Button
 | 
			
		||||
                className="btn btn-outline-secondary dropdown-toggle"
 | 
			
		||||
                data-bs-toggle="dropdown"
 | 
			
		||||
            >
 | 
			
		||||
                {_("Columns")}
 | 
			
		||||
            </Button>
 | 
			
		||||
            <ul className="dropdown-menu dropdown-menu-end">
 | 
			
		||||
                {columns.map((column) => {
 | 
			
		||||
                    return (
 | 
			
		||||
                        <li key={column.id}>
 | 
			
		||||
                            <button
 | 
			
		||||
                                type="button"
 | 
			
		||||
                                className="dropdown-item d-flex align-items-center"
 | 
			
		||||
                                onClick={column.getToggleVisibilityHandler()}
 | 
			
		||||
                                style={{ paddingLeft: "2rem" }}
 | 
			
		||||
                                disabled={
 | 
			
		||||
                                    column.columnDef?.enableHiding === false
 | 
			
		||||
                                }
 | 
			
		||||
                            >
 | 
			
		||||
                                {column.getIsVisible() && (
 | 
			
		||||
                                    <FontAwesomeIcon
 | 
			
		||||
                                        icon={faCheck}
 | 
			
		||||
                                        className="position-absolute text-secondary me-2"
 | 
			
		||||
                                        style={{ left: "0.6rem" }}
 | 
			
		||||
                                        width="1rem"
 | 
			
		||||
                                    />
 | 
			
		||||
                                )}
 | 
			
		||||
                                <span>{column.columnDef.header}</span>
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </li>
 | 
			
		||||
                    );
 | 
			
		||||
                })}
 | 
			
		||||
                {columns.some((column) => !column.getIsVisible()) && (
 | 
			
		||||
                    <>
 | 
			
		||||
                        <li>
 | 
			
		||||
                            <hr className="dropdown-divider" />
 | 
			
		||||
                        </li>
 | 
			
		||||
                        <li>
 | 
			
		||||
                            <button
 | 
			
		||||
                                type="button"
 | 
			
		||||
                                className="dropdown-item d-flex align-items-center"
 | 
			
		||||
                                style={{ paddingLeft: "2rem" }}
 | 
			
		||||
                                onClick={() => {
 | 
			
		||||
                                    // toggleVisibility for columns that are hidden
 | 
			
		||||
                                    columns.forEach((column) => {
 | 
			
		||||
                                        if (!column.getIsVisible()) {
 | 
			
		||||
                                            column.toggleVisibility();
 | 
			
		||||
                                        }
 | 
			
		||||
                                    });
 | 
			
		||||
                                }}
 | 
			
		||||
                            >
 | 
			
		||||
                                <FontAwesomeIcon
 | 
			
		||||
                                    icon={faRotateLeft}
 | 
			
		||||
                                    className="position-absolute text-secondary me-2"
 | 
			
		||||
                                    width="1rem"
 | 
			
		||||
                                    style={{ left: "0.6rem" }}
 | 
			
		||||
                                />
 | 
			
		||||
                                {_("Reset")}
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </li>
 | 
			
		||||
                    </>
 | 
			
		||||
                )}
 | 
			
		||||
            </ul>
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default RichTableColumnsDropdown;
 | 
			
		||||
@@ -55,6 +55,12 @@ function RichTableHeader({ table, flexRender }) {
 | 
			
		||||
                            ) : (
 | 
			
		||||
                                <button
 | 
			
		||||
                                    type="button"
 | 
			
		||||
                                    style={
 | 
			
		||||
                                        header.column.columnDef
 | 
			
		||||
                                            .headerClassName === "text-center"
 | 
			
		||||
                                            ? { justifySelf: "center" }
 | 
			
		||||
                                            : {}
 | 
			
		||||
                                    }
 | 
			
		||||
                                    className={`btn btn-link text-decoration-none text-reset fw-bold p-0 d-flex align-items-center
 | 
			
		||||
                                                    ${
 | 
			
		||||
                                                        header.column.getCanSort()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
 | 
			
		||||
 * Copyright (C) 2019-2025 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.
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user