mirror of
https://gitlab.nic.cz/turris/reforis/foris-js.git
synced 2025-04-12 07:06:40 +02:00
Merge branch 'add-fuzzy-search-and-column-visibility' into 'dev'
Add global fuzzy search and columns visibility to RichTable See merge request turris/reforis/foris-js!273
This commit is contained in:
commit
0a839bf369
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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user