mirror of
				https://gitlab.nic.cz/turris/reforis/foris-js.git
				synced 2025-11-03 23:00:31 +01:00 
			
		
		
		
	Merge branch 'dev' into 'master'
Dev See merge request turris/reforis/foris-js!276
This commit is contained in:
		
							
								
								
									
										17
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -8,6 +8,20 @@ and this project adheres to
 | 
			
		||||
 | 
			
		||||
## [Unreleased]
 | 
			
		||||
 | 
			
		||||
## [6.7.0] - 2025-03-11
 | 
			
		||||
 | 
			
		||||
### Added
 | 
			
		||||
 | 
			
		||||
- Added encryption property to guest WiFi settings in tests
 | 
			
		||||
- Added global fuzzy search and columns visibility to RichTable
 | 
			
		||||
 | 
			
		||||
### Changed
 | 
			
		||||
 | 
			
		||||
- Made thead of RichTable lighter
 | 
			
		||||
- Updated dependencies in package.json to latest versions
 | 
			
		||||
- Enhanced ActionButtonWithModal to support dynamic methods
 | 
			
		||||
- NPM audit fix
 | 
			
		||||
 | 
			
		||||
## [6.6.2] - 2025-02-20
 | 
			
		||||
 | 
			
		||||
### Changed
 | 
			
		||||
@@ -448,7 +462,8 @@ and this project adheres to
 | 
			
		||||
## [0.0.7] - 2019-09-02
 | 
			
		||||
 | 
			
		||||
[unreleased]:
 | 
			
		||||
    https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v6.6.2...dev
 | 
			
		||||
    https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v6.7.0...dev
 | 
			
		||||
[6.7.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v6.6.2...v6.7.0
 | 
			
		||||
[6.6.2]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v6.6.1...v6.6.2
 | 
			
		||||
[6.6.1]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v6.6.0...v6.6.1
 | 
			
		||||
[6.6.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v6.5.0...v6.6.0
 | 
			
		||||
 
 | 
			
		||||
@@ -21,10 +21,7 @@ module.exports = {
 | 
			
		||||
    testPathIgnorePatterns: ["/node_modules/", "/__fixtures__/", "/dist/"],
 | 
			
		||||
    testEnvironment: "jsdom",
 | 
			
		||||
    verbose: false,
 | 
			
		||||
    setupFilesAfterEnv: [
 | 
			
		||||
        "@testing-library/react/cleanup-after-each",
 | 
			
		||||
        "<rootDir>/src/testUtils/setup",
 | 
			
		||||
    ],
 | 
			
		||||
    setupFilesAfterEnv: ["<rootDir>/src/testUtils/setup"],
 | 
			
		||||
    globals: {
 | 
			
		||||
        TZ: "utc",
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8473
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8473
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										47
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										47
									
								
								package.json
									
									
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
    "name": "foris",
 | 
			
		||||
    "version": "6.6.2",
 | 
			
		||||
    "version": "6.7.0",
 | 
			
		||||
    "description": "Foris JS library is a set of components and utils for reForis application and plugins.",
 | 
			
		||||
    "author": "CZ.NIC, z.s.p.o.",
 | 
			
		||||
    "repository": {
 | 
			
		||||
@@ -14,17 +14,18 @@
 | 
			
		||||
    "license": "GPL-3.0",
 | 
			
		||||
    "main": "./src/index.js",
 | 
			
		||||
    "dependencies": {
 | 
			
		||||
        "@fortawesome/fontawesome-svg-core": "^6.6.0",
 | 
			
		||||
        "@fortawesome/free-regular-svg-icons": "^6.6.0",
 | 
			
		||||
        "@fortawesome/free-solid-svg-icons": "^6.6.0",
 | 
			
		||||
        "@fortawesome/fontawesome-svg-core": "^6.7.2",
 | 
			
		||||
        "@fortawesome/free-regular-svg-icons": "^6.7.2",
 | 
			
		||||
        "@fortawesome/free-solid-svg-icons": "^6.7.2",
 | 
			
		||||
        "@fortawesome/react-fontawesome": "^0.2.2",
 | 
			
		||||
        "@tanstack/react-table": "^8.20.5",
 | 
			
		||||
        "axios": "^1.7.2",
 | 
			
		||||
        "@tanstack/match-sorter-utils": "^8.19.4",
 | 
			
		||||
        "@tanstack/react-table": "^8.21.2",
 | 
			
		||||
        "axios": "^1.7.9",
 | 
			
		||||
        "immutability-helper": "^3.1.1",
 | 
			
		||||
        "moment": "^2.30.1",
 | 
			
		||||
        "qrcode.react": "^3.1.0",
 | 
			
		||||
        "react-datetime": "^3.2.0",
 | 
			
		||||
        "react-uid": "^2.3.3"
 | 
			
		||||
        "qrcode.react": "^4.2.0",
 | 
			
		||||
        "react-datetime": "^3.3.1",
 | 
			
		||||
        "react-uid": "^2.4.0"
 | 
			
		||||
    },
 | 
			
		||||
    "peerDependencies": {
 | 
			
		||||
        "bootstrap": "^5.3.3",
 | 
			
		||||
@@ -34,32 +35,32 @@
 | 
			
		||||
        "react-router-dom": "^5.1.2"
 | 
			
		||||
    },
 | 
			
		||||
    "devDependencies": {
 | 
			
		||||
        "@babel/cli": "^7.24.7",
 | 
			
		||||
        "@babel/core": "^7.24.7",
 | 
			
		||||
        "@babel/plugin-transform-runtime": "^7.24.7",
 | 
			
		||||
        "@babel/preset-env": "^7.24.7",
 | 
			
		||||
        "@babel/preset-react": "^7.24.7",
 | 
			
		||||
        "@testing-library/react": "^8.0.9",
 | 
			
		||||
        "babel-loader": "^8.1.0",
 | 
			
		||||
        "@babel/cli": "^7.26.4",
 | 
			
		||||
        "@babel/core": "^7.26.9",
 | 
			
		||||
        "@babel/plugin-transform-runtime": "^7.26.9",
 | 
			
		||||
        "@babel/preset-env": "^7.26.9",
 | 
			
		||||
        "@babel/preset-react": "^7.26.3",
 | 
			
		||||
        "@testing-library/react": "^12.1.5",
 | 
			
		||||
        "babel-loader": "^9.2.1",
 | 
			
		||||
        "babel-polyfill": "^6.26.0",
 | 
			
		||||
        "bootstrap": "^5.3.3",
 | 
			
		||||
        "css-loader": "^5.2.4",
 | 
			
		||||
        "css-loader": "^7.1.2",
 | 
			
		||||
        "eslint": "^8.57.0",
 | 
			
		||||
        "eslint-config-reforis": "^2.1.1",
 | 
			
		||||
        "eslint-config-reforis": "^2.2.1",
 | 
			
		||||
        "file-loader": "^6.0.0",
 | 
			
		||||
        "jest": "^29.7.0",
 | 
			
		||||
        "jest-environment-jsdom": "^29.7.0",
 | 
			
		||||
        "jest-mock-axios": "^4.7.3",
 | 
			
		||||
        "moment-timezone": "^0.5.45",
 | 
			
		||||
        "prettier": "^3.3.2",
 | 
			
		||||
        "jest-mock-axios": "^4.8.0",
 | 
			
		||||
        "moment-timezone": "^0.5.47",
 | 
			
		||||
        "prettier": "^3.5.3",
 | 
			
		||||
        "prop-types": "15.8.1",
 | 
			
		||||
        "react": "16.9.0",
 | 
			
		||||
        "react-dom": "16.9.0",
 | 
			
		||||
        "react-router-dom": "^5.1.2",
 | 
			
		||||
        "react-styleguidist": "^12.0.1",
 | 
			
		||||
        "snapshot-diff": "^0.10.0",
 | 
			
		||||
        "style-loader": "^1.2.1",
 | 
			
		||||
        "webpack": "^5.92.1"
 | 
			
		||||
        "style-loader": "^4.0.0",
 | 
			
		||||
        "webpack": "^5.98.0"
 | 
			
		||||
    },
 | 
			
		||||
    "scripts": {
 | 
			
		||||
        "lint": "eslint src",
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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.
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
import { render, fireEvent, getByLabelText, wait } from "customTestRender";
 | 
			
		||||
import { render, fireEvent, getByLabelText, waitFor } from "customTestRender";
 | 
			
		||||
 | 
			
		||||
import NumberInput from "../NumberInput";
 | 
			
		||||
 | 
			
		||||
@@ -34,7 +34,7 @@ describe("<NumberInput/>", () => {
 | 
			
		||||
    it("Increase number with button", async () => {
 | 
			
		||||
        const increaseButton = getByLabelText(componentContainer, /Increase/);
 | 
			
		||||
        fireEvent.mouseDown(increaseButton);
 | 
			
		||||
        await wait(() =>
 | 
			
		||||
        await waitFor(() =>
 | 
			
		||||
            expect(onChangeMock).toHaveBeenCalledWith({ target: { value: 2 } })
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
@@ -42,7 +42,7 @@ describe("<NumberInput/>", () => {
 | 
			
		||||
    it("Decrease number with button", async () => {
 | 
			
		||||
        const decreaseButton = getByLabelText(componentContainer, /Decrease/);
 | 
			
		||||
        fireEvent.mouseDown(decreaseButton);
 | 
			
		||||
        await wait(() =>
 | 
			
		||||
        await waitFor(() =>
 | 
			
		||||
            expect(onChangeMock).toHaveBeenCalledWith({ target: { value: 0 } })
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
@@ -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.
 | 
			
		||||
@@ -9,7 +9,7 @@ import React, { useState, useEffect } from "react";
 | 
			
		||||
 | 
			
		||||
import PropTypes from "prop-types";
 | 
			
		||||
 | 
			
		||||
import { useAPIPost } from "../../api/hooks";
 | 
			
		||||
import { useAPIPost, useAPIPut } from "../../api/hooks";
 | 
			
		||||
import { API_STATE } from "../../api/utils";
 | 
			
		||||
import Button from "../../bootstrap/Button";
 | 
			
		||||
import {
 | 
			
		||||
@@ -23,6 +23,8 @@ import { useAlert } from "../../context/alertContext/AlertContext";
 | 
			
		||||
ActionButtonWithModal.propTypes = {
 | 
			
		||||
    /** Component that triggers the action. */
 | 
			
		||||
    actionTrigger: PropTypes.elementType.isRequired,
 | 
			
		||||
    /** Method to use for the action. */
 | 
			
		||||
    actionMethod: PropTypes.string,
 | 
			
		||||
    /** URL to send the action to. */
 | 
			
		||||
    actionUrl: PropTypes.string.isRequired,
 | 
			
		||||
    /** Title of the modal. */
 | 
			
		||||
@@ -41,6 +43,7 @@ ActionButtonWithModal.propTypes = {
 | 
			
		||||
 | 
			
		||||
function ActionButtonWithModal({
 | 
			
		||||
    actionTrigger: ActionTriggerComponent,
 | 
			
		||||
    actionMethod = "POST",
 | 
			
		||||
    actionUrl,
 | 
			
		||||
    modalTitle,
 | 
			
		||||
    modalMessage,
 | 
			
		||||
@@ -51,24 +54,43 @@ function ActionButtonWithModal({
 | 
			
		||||
}) {
 | 
			
		||||
    const [triggered, setTriggered] = useState(false);
 | 
			
		||||
    const [modalShown, setModalShown] = useState(false);
 | 
			
		||||
    const [triggerActionStatus, triggerAction] = useAPIPost(actionUrl);
 | 
			
		||||
    const [triggerPostActionStatus, triggerPostAction] = useAPIPost(actionUrl);
 | 
			
		||||
    const [triggerPutActionStatus, triggerPutAction] = useAPIPut(actionUrl);
 | 
			
		||||
 | 
			
		||||
    const [setAlert] = useAlert();
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (triggerActionStatus.state === API_STATE.SUCCESS) {
 | 
			
		||||
        if (
 | 
			
		||||
            triggerPostActionStatus.state === API_STATE.SUCCESS ||
 | 
			
		||||
            triggerPutActionStatus.state === API_STATE.SUCCESS
 | 
			
		||||
        ) {
 | 
			
		||||
            setAlert(
 | 
			
		||||
                successMessage || _("Action successful."),
 | 
			
		||||
                API_STATE.SUCCESS
 | 
			
		||||
            );
 | 
			
		||||
            setTriggered(false);
 | 
			
		||||
        }
 | 
			
		||||
        if (triggerActionStatus.state === API_STATE.ERROR) {
 | 
			
		||||
        if (
 | 
			
		||||
            triggerPostActionStatus.state === API_STATE.ERROR ||
 | 
			
		||||
            triggerPutActionStatus.state === API_STATE.ERROR
 | 
			
		||||
        ) {
 | 
			
		||||
            setAlert(errorMessage || _("Action failed."));
 | 
			
		||||
            setTriggered(false);
 | 
			
		||||
        }
 | 
			
		||||
    }, [triggerActionStatus, setAlert, successMessage, errorMessage]);
 | 
			
		||||
    }, [
 | 
			
		||||
        triggerPostActionStatus,
 | 
			
		||||
        triggerPutActionStatus,
 | 
			
		||||
        setAlert,
 | 
			
		||||
        successMessage,
 | 
			
		||||
        errorMessage,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    const actionHandler = () => {
 | 
			
		||||
        setTriggered(true);
 | 
			
		||||
        triggerAction();
 | 
			
		||||
        if (actionMethod === "POST") {
 | 
			
		||||
            triggerPostAction();
 | 
			
		||||
        } else {
 | 
			
		||||
            triggerPutAction();
 | 
			
		||||
        }
 | 
			
		||||
        setModalShown(false);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
@@ -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.
 | 
			
		||||
@@ -32,7 +32,7 @@ function RichTableHeader({ table, flexRender }) {
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <thead className="thead-light">
 | 
			
		||||
        <thead className="table-light">
 | 
			
		||||
            {table.getHeaderGroups().map((headerGroup) => (
 | 
			
		||||
                <tr key={headerGroup.id} role="row">
 | 
			
		||||
                    {headerGroup.headers.map((header) => (
 | 
			
		||||
@@ -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.
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2019-2021 CZ.NIC z.s.p.o. (http://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.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { render, fireEvent, wait } from "customTestRender";
 | 
			
		||||
import { render, fireEvent, waitFor } from "customTestRender";
 | 
			
		||||
 | 
			
		||||
import mockAxios from "jest-mock-axios";
 | 
			
		||||
import WebSockets from "webSockets/WebSockets";
 | 
			
		||||
@@ -35,7 +35,7 @@ describe("<ResetWiFiSettings/>", () => {
 | 
			
		||||
            expect.anything()
 | 
			
		||||
        );
 | 
			
		||||
        mockAxios.mockResponse({ data: { foo: "bar" } });
 | 
			
		||||
        await wait(() =>
 | 
			
		||||
        await waitFor(() =>
 | 
			
		||||
            expect(mockSetAlert).toBeCalledWith(
 | 
			
		||||
                "Wi-Fi settings are set to defaults.",
 | 
			
		||||
                ALERT_TYPES.SUCCESS
 | 
			
		||||
@@ -46,7 +46,7 @@ describe("<ResetWiFiSettings/>", () => {
 | 
			
		||||
    it("should display alert on open ports - failure", async () => {
 | 
			
		||||
        fireEvent.click(getAllByText("Reset Wi-Fi Settings")[1]);
 | 
			
		||||
        mockJSONError();
 | 
			
		||||
        await wait(() =>
 | 
			
		||||
        await waitFor(() =>
 | 
			
		||||
            expect(mockSetAlert).toBeCalledWith(
 | 
			
		||||
                "An error occurred during resetting Wi-Fi settings."
 | 
			
		||||
            )
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,16 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2019-2021 CZ.NIC z.s.p.o. (http://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.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
import diffSnapshot from "snapshot-diff";
 | 
			
		||||
import mockAxios from "jest-mock-axios";
 | 
			
		||||
 | 
			
		||||
import { fireEvent, render, wait } from "customTestRender";
 | 
			
		||||
import { fireEvent, render, waitFor } from "customTestRender";
 | 
			
		||||
import WebSockets from "webSockets/WebSockets";
 | 
			
		||||
import { mockJSONError } from "testUtils/network";
 | 
			
		||||
 | 
			
		||||
@@ -45,7 +46,7 @@ describe("<WiFiSettings/>", () => {
 | 
			
		||||
        getByLabelText = renderRes.getByLabelText;
 | 
			
		||||
        getByText = renderRes.getByText;
 | 
			
		||||
        mockAxios.mockResponse({ data: wifiSettingsFixture() });
 | 
			
		||||
        await wait(() => renderRes.getByText("Wi-Fi 1"));
 | 
			
		||||
        await waitFor(() => renderRes.getByText("Wi-Fi 1"));
 | 
			
		||||
        firstRender = renderRes.asFragment();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@@ -60,7 +61,7 @@ describe("<WiFiSettings/>", () => {
 | 
			
		||||
        );
 | 
			
		||||
        const errorMessage = "An API error occurred.";
 | 
			
		||||
        mockJSONError(errorMessage);
 | 
			
		||||
        await wait(() => {
 | 
			
		||||
        await waitFor(() => {
 | 
			
		||||
            expect(getByText(errorMessage)).toBeTruthy();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
@@ -181,6 +182,7 @@ describe("<WiFiSettings/>", () => {
 | 
			
		||||
                    guest_wifi: {
 | 
			
		||||
                        SSID: "TestGuestSSID",
 | 
			
		||||
                        enabled: true,
 | 
			
		||||
                        encryption: "WPA2",
 | 
			
		||||
                        password: "test_password",
 | 
			
		||||
                    },
 | 
			
		||||
                    hidden: false,
 | 
			
		||||
 
 | 
			
		||||
@@ -223,6 +223,7 @@ export function wifiSettingsFixture() {
 | 
			
		||||
                guest_wifi: {
 | 
			
		||||
                    SSID: "TestGuestSSID",
 | 
			
		||||
                    enabled: false,
 | 
			
		||||
                    encryption: "WPA2",
 | 
			
		||||
                    password: "",
 | 
			
		||||
                },
 | 
			
		||||
                hidden: false,
 | 
			
		||||
 
 | 
			
		||||
@@ -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.
 | 
			
		||||
@@ -9,13 +9,7 @@ import React from "react";
 | 
			
		||||
 | 
			
		||||
import Button from "bootstrap/Button";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    fireEvent,
 | 
			
		||||
    getByText,
 | 
			
		||||
    queryByText,
 | 
			
		||||
    render,
 | 
			
		||||
    wait,
 | 
			
		||||
} from "customTestRender";
 | 
			
		||||
import { fireEvent, getByText, render, waitFor } from "customTestRender";
 | 
			
		||||
import mockAxios from "jest-mock-axios";
 | 
			
		||||
import { mockJSONError } from "testUtils/network";
 | 
			
		||||
import { mockSetAlert } from "testUtils/alertContextMock";
 | 
			
		||||
@@ -73,7 +67,7 @@ describe("<ActionButtonWithModal/>", () => {
 | 
			
		||||
        fireEvent.click(getByText(componentContainer, "Action"));
 | 
			
		||||
        fireEvent.click(getByText(componentContainer, "Confirm action"));
 | 
			
		||||
        mockJSONError();
 | 
			
		||||
        await wait(() =>
 | 
			
		||||
        await waitFor(() =>
 | 
			
		||||
            expect(mockSetAlert).toBeCalledWith("Action request failed.")
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
@@ -82,7 +76,7 @@ describe("<ActionButtonWithModal/>", () => {
 | 
			
		||||
        fireEvent.click(getByText(componentContainer, "Action"));
 | 
			
		||||
        fireEvent.click(getByText(componentContainer, "Confirm action"));
 | 
			
		||||
        mockAxios.mockResponse({ status: 200 });
 | 
			
		||||
        await wait(() =>
 | 
			
		||||
        await waitFor(() =>
 | 
			
		||||
            expect(mockSetAlert).toBeCalledWith(
 | 
			
		||||
                "Action request succeeded.",
 | 
			
		||||
                "success"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2019-2022 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.
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
import { render, wait, getByText } from "customTestRender";
 | 
			
		||||
import { render, waitFor, getByText } from "customTestRender";
 | 
			
		||||
import mockAxios from "jest-mock-axios";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
@@ -38,7 +38,7 @@ describe("CustomizationContext", () => {
 | 
			
		||||
    it("should render component without customization", async () => {
 | 
			
		||||
        mockAxios.mockResponse({ data: {} });
 | 
			
		||||
 | 
			
		||||
        await wait(() => getByText(componentContainer, ORIGINAL));
 | 
			
		||||
        await waitFor(() => getByText(componentContainer, ORIGINAL));
 | 
			
		||||
 | 
			
		||||
        expect(componentContainer).toMatchSnapshot();
 | 
			
		||||
    });
 | 
			
		||||
@@ -46,7 +46,7 @@ describe("CustomizationContext", () => {
 | 
			
		||||
    it("should render customized component", async () => {
 | 
			
		||||
        mockAxios.mockResponse({ data: { customization: "shield" } });
 | 
			
		||||
 | 
			
		||||
        await wait(() => getByText(componentContainer, CUSTOM));
 | 
			
		||||
        await waitFor(() => getByText(componentContainer, CUSTOM));
 | 
			
		||||
 | 
			
		||||
        expect(componentContainer).toMatchSnapshot();
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2019 CZ.NIC z.s.p.o. (http://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.
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
import { act, fireEvent, render, waitForElement } from "customTestRender";
 | 
			
		||||
import { act, fireEvent, render, waitFor } from "customTestRender";
 | 
			
		||||
import mockAxios from "jest-mock-axios";
 | 
			
		||||
import WebSockets from "webSockets/WebSockets";
 | 
			
		||||
import ForisForm from "../components/ForisForm";
 | 
			
		||||
@@ -59,7 +59,7 @@ describe("useForm hook.", () => {
 | 
			
		||||
        );
 | 
			
		||||
        mockAxios.mockResponse({ field: "fetchedData" });
 | 
			
		||||
 | 
			
		||||
        input = await waitForElement(() => getByTestId("test-input"));
 | 
			
		||||
        input = await waitFor(() => getByTestId("test-input"));
 | 
			
		||||
        form = container.firstChild.firstChild;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user