diff --git a/components/frontend/package-lock.json b/components/frontend/package-lock.json index 97ae2bac8b..e9004eb36d 100644 --- a/components/frontend/package-lock.json +++ b/components/frontend/package-lock.json @@ -1,25 +1,27 @@ { "name": "quality-time-app", - "version": "5.16.1", + "version": "5.16.2-rc.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "quality-time-app", - "version": "5.16.1", + "version": "5.16.2-rc.0", "dependencies": { "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@mui/icons-material": "^6.1.2", + "@mui/lab": "^6.0.0-beta.10", "@mui/material": "^6.1.2", + "@mui/x-date-pickers": "^7.18.0", "crypto-js": "^4.2.0", + "dayjs": "^1.11.13", "fomantic-ui-css": "^2.9.3", "history": "^5.3.0", "prop-types": "^15.8.1", "react": "^18.3.1", "react-datepicker": "^7.4.0", "react-dom": "^18.3.1", - "react-focus-lock": "^2.13.0", "react-grid-layout": "^1.4.4", "react-hash-link": "1.0.2", "react-is": "^18.3.1", @@ -4821,6 +4823,68 @@ "dev": true, "license": "MIT" }, + "node_modules/@mui/base": { + "version": "5.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.58.tgz", + "integrity": "sha512-P0E7ZrxOuyYqBvVv9w8k7wm+Xzx/KRu+BGgFcR2htTsGCpJNQJCSUXNUZ50MUmSU9hzqhwbQWNXhV1MBTl6F7A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "@floating-ui/react-dom": "^2.1.1", + "@mui/types": "^7.2.15", + "@mui/utils": "6.0.0-rc.0", + "@popperjs/core": "^2.11.8", + "clsx": "^2.1.1", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/base/node_modules/@mui/utils": { + "version": "6.0.0-rc.0", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.0.0-rc.0.tgz", + "integrity": "sha512-tBp0ILEXDL0bbDDT8PnZOjCqSm5Dfk2N0Z45uzRw+wVl6fVvloC9zw8avl+OdX1Bg3ubs/ttKn8nRNv17bpM5A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "@mui/types": "^7.2.15", + "@types/prop-types": "^15.7.12", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^18.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@mui/core-downloads-tracker": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.2.tgz", @@ -4857,6 +4921,51 @@ } } }, + "node_modules/@mui/lab": { + "version": "6.0.0-beta.10", + "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.10.tgz", + "integrity": "sha512-eqCBz5SZS8Un9To3UcjH01AxkOOgvme/g0ZstFC8Nz1Kg5/EJMA0ByhKS5AvUMzUKrv0FXMdbuPqbBvF3bVrXg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.6", + "@mui/base": "5.0.0-beta.58", + "@mui/system": "^6.1.1", + "@mui/types": "^7.2.17", + "@mui/utils": "^6.1.1", + "clsx": "^2.1.1", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material": "^6.1.1", + "@mui/material-pigment-css": "^6.1.1", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, "node_modules/@mui/material": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.2.tgz", @@ -5050,6 +5159,152 @@ } } }, + "node_modules/@mui/x-date-pickers": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.18.0.tgz", + "integrity": "sha512-12tXIoMj9vpS8fS/bS3kWPCoVrH38vNGCxgplI0vOnUrN9rJuYJz3agLPJe1S0xciTw+9W8ZSe3soaW+owoz1Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.6", + "@mui/utils": "^5.16.6", + "@mui/x-internals": "7.18.0", + "@types/react-transition-group": "^4.4.11", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0", + "@mui/system": "^5.15.14 || ^6.0.0", + "date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0", + "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0", + "dayjs": "^1.10.7", + "luxon": "^3.0.2", + "moment": "^2.29.4", + "moment-hijri": "^2.1.2", + "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "date-fns-jalali": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-hijri": { + "optional": true + }, + "moment-jalaali": { + "optional": true + } + } + }, + "node_modules/@mui/x-date-pickers/node_modules/@mui/utils": { + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.6.tgz", + "integrity": "sha512-tWiQqlhxAt3KENNiSRL+DIn9H5xNVK6Jjf70x3PnfQPz1MPBdh7yyIcAyVBT9xiw7hP3SomRhPR7hzBMBCjqEA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/types": "^7.2.15", + "@types/prop-types": "^15.7.12", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^18.3.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-internals": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.18.0.tgz", + "integrity": "sha512-lzCHOWIR0cAIY1bGrWSprYerahbnH5C31ql/2OWCEjcngL2NAV1M6oKI2Vp4HheqzJ822c60UyWyapvyjSzY/A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.6", + "@mui/utils": "^5.16.6" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0" + } + }, + "node_modules/@mui/x-internals/node_modules/@mui/utils": { + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.6.tgz", + "integrity": "sha512-tWiQqlhxAt3KENNiSRL+DIn9H5xNVK6Jjf70x3PnfQPz1MPBdh7yyIcAyVBT9xiw7hP3SomRhPR7hzBMBCjqEA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/types": "^7.2.15", + "@types/prop-types": "^15.7.12", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^18.3.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -8773,6 +9028,12 @@ "url": "https://github.com/sponsors/kossnocorp" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.3.4", "dev": true, @@ -8938,12 +9199,6 @@ "dev": true, "license": "MIT" }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", - "license": "MIT" - }, "node_modules/detect-port-alt": { "version": "1.1.6", "dev": true, @@ -10705,18 +10960,6 @@ "dev": true, "license": "ISC" }, - "node_modules/focus-lock": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-1.3.5.tgz", - "integrity": "sha512-QFaHbhv9WPUeLYBDe/PAuLKJ4Dd9OPvKs9xZBr3yLXnUrDNaVXKu2baDBXe3naPY30hgHYSsf2JW4jzas2mDEQ==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.3" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/follow-redirects": { "version": "1.15.6", "dev": true, @@ -19534,18 +19777,6 @@ "dev": true, "license": "MIT" }, - "node_modules/react-clientside-effect": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz", - "integrity": "sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.13" - }, - "peerDependencies": { - "react": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-datepicker": { "version": "7.4.0", "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-7.4.0.tgz", @@ -19718,29 +19949,6 @@ "version": "3.2.2", "license": "MIT" }, - "node_modules/react-focus-lock": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.13.2.tgz", - "integrity": "sha512-T/7bsofxYqnod2xadvuwjGKHOoL5GH7/EIPI5UyEvaU/c2CcphvGI371opFtuY/SYdbMsNiuF4HsHQ50nA/TKQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.0.0", - "focus-lock": "^1.3.5", - "prop-types": "^15.6.2", - "react-clientside-effect": "^1.2.6", - "use-callback-ref": "^1.3.2", - "use-sidecar": "^1.1.2" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/react-grid-layout": { "version": "1.4.4", "license": "MIT", @@ -23619,6 +23827,7 @@ }, "node_modules/tslib": { "version": "2.6.2", + "dev": true, "license": "0BSD" }, "node_modules/tsutils": { @@ -23925,49 +24134,6 @@ "requires-port": "^1.0.0" } }, - "node_modules/use-callback-ref": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz", - "integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", - "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "dev": true, diff --git a/components/frontend/package.json b/components/frontend/package.json index 295f738601..8163083557 100644 --- a/components/frontend/package.json +++ b/components/frontend/package.json @@ -7,15 +7,17 @@ "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@mui/icons-material": "^6.1.2", + "@mui/lab": "^6.0.0-beta.10", "@mui/material": "^6.1.2", + "@mui/x-date-pickers": "^7.18.0", "crypto-js": "^4.2.0", + "dayjs": "^1.11.13", "fomantic-ui-css": "^2.9.3", "history": "^5.3.0", "prop-types": "^15.8.1", "react": "^18.3.1", "react-datepicker": "^7.4.0", "react-dom": "^18.3.1", - "react-focus-lock": "^2.13.0", "react-grid-layout": "^1.4.4", "react-hash-link": "1.0.2", "react-is": "^18.3.1", diff --git a/components/frontend/src/App.test.js b/components/frontend/src/App.test.js index 592a87d34c..2db484e12f 100644 --- a/components/frontend/src/App.test.js +++ b/components/frontend/src/App.test.js @@ -1,5 +1,4 @@ import { act, fireEvent, render, screen } from "@testing-library/react" -import userEvent from "@testing-library/user-event" import history from "history/browser" import * as auth from "./api/auth" @@ -69,32 +68,47 @@ it("resets the user when the user clicks logout", async () => { expect(screen.queryAllByText(/admin/).length).toBe(0) }) +async function selectDate() { + await act(async () => { + fireEvent.click(screen.getByLabelText("Report date")) + }) + await act(async () => { + fireEvent.click(screen.getByRole("button", { name: "Previous month" })) + }) + await act(async () => { + fireEvent.click(screen.getAllByRole("gridcell", { name: "15" })[0]) + }) +} + it("handles a date change", async () => { render() - await userEvent.type(screen.getByPlaceholderText("YYYY-MM-DD"), "2020-03-13") - expect(screen.getAllByDisplayValue("2020-03-13").length).toBe(1) + await selectDate() + expect(screen.getByLabelText("Report date").textContent).toMatch(/15/) }) it("handles a date change between two dates in the past", async () => { history.push("/?report_date=2022-03-13") render() - await userEvent.type(screen.getByPlaceholderText("YYYY-MM-DD"), "{Backspace}4") - expect(screen.getAllByDisplayValue("2022-03-14").length).toBe(1) + await selectDate() + expect(screen.getByLabelText("Report date").textContent).toMatch(/15/) }) it("reads the report date query parameter", () => { history.push("/?report_date=2020-03-13") render() - expect(screen.getAllByDisplayValue("2020-03-13").length).toBe(1) + expect(screen.getByLabelText("Report date").textContent).toMatch(/2020/) }) it("handles a date reset", async () => { history.push("/?report_date=2020-03-13") render() await act(async () => { - fireEvent.click(screen.getByRole("button", { name: "Close" })) + fireEvent.click(screen.getByLabelText("Report date")) + }) + await act(async () => { + fireEvent.click(screen.getByRole("button", { name: "Today" })) }) - expect(screen.queryAllByDisplayValue("2020-03-13").length).toBe(0) + expect(screen.getByLabelText("Report date").textContent).toMatch(/today/) }) it("handles the nr of measurements event source", async () => { diff --git a/components/frontend/src/AppUI.js b/components/frontend/src/AppUI.js index 93ec29bead..4b35645ba8 100644 --- a/components/frontend/src/AppUI.js +++ b/components/frontend/src/AppUI.js @@ -2,6 +2,9 @@ import "react-toastify/dist/ReactToastify.css" import "./App.css" import { useColorScheme } from "@mui/material/styles" +import { LocalizationProvider } from "@mui/x-date-pickers" +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs" +import { locale_en_gb } from "dayjs/locale/en-gb" import { bool, func, number, object, string } from "prop-types" import HashLinkObserver from "react-hash-link" import { ToastContainer } from "react-toastify" @@ -73,52 +76,54 @@ export function AppUI({ backgroundColor: backgroundColor, }} > - - - - } - settings={settings} - setUIMode={setMode} - uiMode={mode} - /> - - - - - - - - + + + + + } + settings={settings} + setUIMode={setMode} + uiMode={mode} + /> + + + + + + + + + ) } diff --git a/components/frontend/src/AppUI.test.js b/components/frontend/src/AppUI.test.js index 2084e29dd4..861dd0dba8 100644 --- a/components/frontend/src/AppUI.test.js +++ b/components/frontend/src/AppUI.test.js @@ -59,6 +59,6 @@ async function renderAppUI() { it("resets all settings", async () => { history.push("?date_interval=2") await act(async () => await renderAppUI()) - fireEvent.click(screen.getByLabelText("Reset reports overview settings")) + fireEvent.click(screen.getByText("Reset settings")) expect(history.location.search).toBe("") }) diff --git a/components/frontend/src/header_footer/CollapseButton.js b/components/frontend/src/header_footer/CollapseButton.js deleted file mode 100644 index d4b6c48cfe..0000000000 --- a/components/frontend/src/header_footer/CollapseButton.js +++ /dev/null @@ -1,32 +0,0 @@ -import { Button, Icon } from "semantic-ui-react" - -import { Popup } from "../semantic_ui_react_wrappers" -import { stringsURLSearchQueryPropType } from "../sharedPropTypes" - -export function CollapseButton({ expandedItems }) { - const label = "Collapse all headers and metrics" - return ( - - expandedItems.reset()} - inverted - > - Collapse all - - - } - content={label} - /> - ) -} -CollapseButton.propTypes = { - expandedItems: stringsURLSearchQueryPropType, -} diff --git a/components/frontend/src/header_footer/DownloadAsPDFButton.test.js b/components/frontend/src/header_footer/DownloadAsPDFButton.test.js deleted file mode 100644 index d8b8abb8e0..0000000000 --- a/components/frontend/src/header_footer/DownloadAsPDFButton.test.js +++ /dev/null @@ -1,82 +0,0 @@ -import { act, fireEvent, render, screen } from "@testing-library/react" -import history from "history/browser" - -import * as fetch_server_api from "../api/fetch_server_api" -import { DownloadAsPDFButton } from "./DownloadAsPDFButton" - -beforeEach(() => { - fetch_server_api.fetch_server_api = jest.fn().mockReturnValue({ - then: jest.fn().mockReturnValue({ catch: jest.fn().mockReturnValue({ finally: jest.fn() }) }), - }) -}) - -test("DownloadAsPDFButton has the correct label for reports overview", () => { - render() - expect(screen.getAllByLabelText(/reports overview as PDF/).length).toBe(1) -}) - -test("DownloadAsPDFButton has the correct label for a report", () => { - render() - expect(screen.getAllByLabelText(/report as PDF/).length).toBe(1) -}) - -const test_report = { report_uuid: "report_uuid" } - -test("DownloadAsPDFButton indicates loading on click", async () => { - render() - await act(async () => { - fireEvent.click(screen.getByLabelText(/Download/)) - }) - expect(screen.getByLabelText(/Download/).className).toContain("loading") - expect(fetch_server_api.fetch_server_api).toHaveBeenCalledWith( - "get", - "report/report_uuid/pdf?report_url=http%3A%2F%2Flocalhost%2F", - {}, - "application/pdf", - ) -}) - -test("DownloadAsPDFButton ignores unregistered query parameters", async () => { - history.push("?unregister_key=value&nr_dates=4") - render() - await act(async () => { - fireEvent.click(screen.getByLabelText(/Download/)) - }) - expect(fetch_server_api.fetch_server_api).toHaveBeenCalledWith( - "get", - "report/report_uuid/pdf?nr_dates=4&report_url=http%3A%2F%2Flocalhost%2F%3Fnr_dates%3D4", - {}, - "application/pdf", - ) -}) - -test("DownloadAsPDFButton ignores a second click", async () => { - render() - await act(async () => { - fireEvent.click(screen.getByLabelText(/Download/)) - }) - await act(async () => { - fireEvent.click(screen.getByLabelText(/Download/)) - }) - expect(screen.getByLabelText(/Download/).className).toContain("loading") -}) - -test("DownloadAsPDFButton stops loading after returning pdf", async () => { - fetch_server_api.fetch_server_api = jest.fn().mockResolvedValue("pdf") - HTMLAnchorElement.prototype.click = jest.fn() // Prevent "Not implemented: navigation (except hash changes)" - window.URL.createObjectURL = jest.fn() - render() - await act(async () => { - fireEvent.click(screen.getByLabelText(/Download/)) - }) - expect(screen.getByLabelText(/Download/).className).not.toContain("loading") -}) - -test("DownloadAsPDFButton stops loading after receiving error", async () => { - fetch_server_api.fetch_server_api = jest.fn().mockResolvedValue({ ok: false }) - render() - await act(async () => { - fireEvent.click(screen.getByLabelText(/Download/)) - }) - expect(screen.getByLabelText(/Download/).className).not.toContain("loading") -}) diff --git a/components/frontend/src/header_footer/Menubar.css b/components/frontend/src/header_footer/Menubar.css index 18f8aec0ff..51967bdde6 100644 --- a/components/frontend/src/header_footer/Menubar.css +++ b/components/frontend/src/header_footer/Menubar.css @@ -1,26 +1,5 @@ @media print { - .menubar { + .MuiAppBar-root { display: none !important; } } - -.menubar { - opacity: 0.98; -} - -.menu .center { - display: grid; - place-content: center; -} - -.panel { - background-color: black; - border: 0px; - left: 0px; - margin: 0px; - opacity: 0.98; - position: fixed; - top: 64px; - width: 100%; - z-index: 4; -} diff --git a/components/frontend/src/header_footer/Menubar.js b/components/frontend/src/header_footer/Menubar.js index 854a6eb4f8..b4340a8a0e 100644 --- a/components/frontend/src/header_footer/Menubar.js +++ b/components/frontend/src/header_footer/Menubar.js @@ -1,18 +1,20 @@ import "./Menubar.css" +import { AppBar, Button, Drawer, Stack, Toolbar } from "@mui/material" import { element, func, string } from "prop-types" import { useEffect, useState } from "react" -import FocusLock from "react-focus-lock" -import { Button, Dropdown, Icon, Image, Menu, Portal } from "semantic-ui-react" +import { Dropdown, Icon } from "semantic-ui-react" import { login, logout } from "../api/auth" -import { Form, Message, Modal, Popup } from "../semantic_ui_react_wrappers" +import { Form, Message, Modal } from "../semantic_ui_react_wrappers" import { optionalDatePropType, settingsPropType, uiModePropType } from "../sharedPropTypes" import { Avatar } from "../widgets/Avatar" -import { DatePicker } from "../widgets/DatePicker" -import { CollapseButton } from "./CollapseButton" -import { DownloadAsPDFButton } from "./DownloadAsPDFButton" -import { ResetSettingsButton } from "./ResetSettingsButton" +import { CollapseButton } from "./buttons/CollapseButton" +import { DatePickerButton } from "./buttons/DatePickerButton" +import { DownloadAsPDFButton } from "./buttons/DownloadAsPDFButton" +import { HomeButton } from "./buttons/HomeButton" +import { ResetSettingsButton } from "./buttons/ResetSettingsButton" +import { SettingsButton } from "./buttons/SettingsButton" import { UIModeMenu } from "./UIModeMenu" function Login({ set_user }) { @@ -49,7 +51,7 @@ function Login({ set_user }) { return ( + Login @@ -144,113 +146,47 @@ export function Menubar({ const atReportsOverview = report_uuid === "" return ( <> - - - { - event.preventDefault() - setSettingsPanelVisible(false) - openReportsOverview() - }} - tabIndex={atReportsOverview ? -1 : 0} - > - { - setSettingsPanelVisible(false) - openReportsOverview() - } - } - > - - Quality-time - - - } - /> - - { - event.preventDefault() - setSettingsPanelVisible(!settingsPanelVisible) - }} - > - { - event.stopPropagation() - setSettingsPanelVisible(!settingsPanelVisible) - }} - tabIndex={0} - > - - Settings - - - - + theme.zIndex.drawer + 1, + }} // Make sure the app bar is above the drawer for the settings + > + + + + - - - - - - - - - - - - - } - /> - + + + onDate(date.$d)} reportDate={report_date} /> - - {user !== null ? ( ) : ( )} - - - - { - event.stopPropagation() - setSettingsPanelVisible(false) - }} - unmountOnHide - > - - {panel} - - + + + + setSettingsPanelVisible(false)}> + + {panel} + > ) } diff --git a/components/frontend/src/header_footer/Menubar.test.js b/components/frontend/src/header_footer/Menubar.test.js index afdd1328c5..7a0090744b 100644 --- a/components/frontend/src/header_footer/Menubar.test.js +++ b/components/frontend/src/header_footer/Menubar.test.js @@ -1,4 +1,4 @@ -import { act, fireEvent, render, screen } from "@testing-library/react" +import { act, fireEvent, render, screen, waitFor } from "@testing-library/react" import userEvent from "@testing-library/user-event" import history from "history/browser" @@ -99,7 +99,7 @@ it("does not go to home page if on reports overview", async () => { const openReportsOverview = jest.fn() renderMenubar({ report_uuid: "", openReportsOverview: openReportsOverview }) act(() => { - fireEvent.click(screen.getByAltText(/Go home/)) + fireEvent.click(screen.getByAltText(/Go to reports overview/)) }) expect(openReportsOverview).not.toHaveBeenCalled() }) @@ -108,7 +108,7 @@ it("goes to home page if on report", async () => { const openReportsOverview = jest.fn() renderMenubar({ openReportsOverview: openReportsOverview }) await act(async () => { - fireEvent.click(screen.getByAltText(/Go home/)) + fireEvent.click(screen.getByAltText(/Go to reports overview/)) }) expect(openReportsOverview).toHaveBeenCalled() }) @@ -116,7 +116,7 @@ it("goes to home page if on report", async () => { it("goes to home page on keypress", async () => { const openReportsOverview = jest.fn() renderMenubar({ openReportsOverview: openReportsOverview }) - await userEvent.type(screen.getByAltText(/Go home/), "{Enter}") + await userEvent.type(screen.getByAltText(/Go to reports overview/), "{Enter}") expect(openReportsOverview).toHaveBeenCalled() }) @@ -132,12 +132,12 @@ it("shows the view panel on space", async () => { expect(screen.getAllByText(/Hello/).length).toBe(1) }) -it("hides the view panel on click", () => { +it("hides the view panel on click", async () => { renderMenubar({ panel: Hello }) fireEvent.click(screen.getByText(/Settings/)) expect(screen.getAllByText(/Hello/).length).toBe(1) fireEvent.click(screen.getByText(/Settings/)) - expect(screen.queryAllByText(/Hello/).length).toBe(0) + await waitFor(() => expect(screen.queryAllByText(/Hello/).length).toBe(0)) }) it("hides the view panel on escape", async () => { @@ -145,5 +145,5 @@ it("hides the view panel on escape", async () => { fireEvent.click(screen.getByText(/Settings/)) expect(screen.getAllByText(/Hello/).length).toBe(1) await userEvent.keyboard("{Escape}") - expect(screen.queryAllByText(/Hello/).length).toBe(0) + await waitFor(() => expect(screen.queryAllByText(/Hello/).length).toBe(0)) }) diff --git a/components/frontend/src/header_footer/ResetSettingsButton.js b/components/frontend/src/header_footer/ResetSettingsButton.js deleted file mode 100644 index e4982f9f6e..0000000000 --- a/components/frontend/src/header_footer/ResetSettingsButton.js +++ /dev/null @@ -1,43 +0,0 @@ -import { bool, func } from "prop-types" -import { Button, Icon } from "semantic-ui-react" - -import { Popup } from "../semantic_ui_react_wrappers" -import { optionalDatePropType, settingsPropType } from "../sharedPropTypes" - -export function ResetSettingsButton({ atReportsOverview, handleDateChange, reportDate, settings }) { - const label = `Reset ${atReportsOverview ? "reports overview" : "this report's"} settings` - return ( - - { - handleDateChange(null) - settings.reset() - }} - inverted - > - - - - - Reset settings - - - } - content={label} - /> - ) -} -ResetSettingsButton.propTypes = { - atReportsOverview: bool, - handleDateChange: func, - reportDate: optionalDatePropType, - settings: settingsPropType, -} diff --git a/components/frontend/src/header_footer/SettingsPanel.test.js b/components/frontend/src/header_footer/SettingsPanel.test.js index 2a02fe6ba0..2cb0b4e16c 100644 --- a/components/frontend/src/header_footer/SettingsPanel.test.js +++ b/components/frontend/src/header_footer/SettingsPanel.test.js @@ -127,14 +127,6 @@ it("sorts a column by keypress", async () => { expect(handleSort).toHaveBeenCalledWith("comment") }) -it("ignores a keypress if the menu item is disabled", async () => { - history.push("?hidden_columns=comment") - const handleSort = jest.fn() - renderSettingsPanel({ handleSort: handleSort }) - await userEvent.type(screen.getAllByText(/Comment/)[1], " ") - expect(handleSort).not.toHaveBeenCalledWith("comment") -}) - it("sets the number of dates", async () => { history.push("?nr_dates=2") renderSettingsPanel() diff --git a/components/frontend/src/header_footer/UIModeMenu.js b/components/frontend/src/header_footer/UIModeMenu.js index 960889f80b..432db51a4b 100644 --- a/components/frontend/src/header_footer/UIModeMenu.js +++ b/components/frontend/src/header_footer/UIModeMenu.js @@ -1,25 +1,49 @@ +import Brightness4Icon from "@mui/icons-material/Brightness4" +import { IconButton, Menu, MenuItem, Tooltip } from "@mui/material" import { func } from "prop-types" -import { Dropdown } from "semantic-ui-react" +import { useState } from "react" import { uiModePropType } from "../sharedPropTypes" -import { IconCombi } from "../widgets/IconCombi" export function UIModeMenu({ setUIMode, uiMode }) { + const [anchorEl, setAnchorEl] = useState() + const handleMenu = (event) => setAnchorEl(event.currentTarget) + const onClick = (mode) => { + setAnchorEl(null) + setUIMode(mode) + } return ( - }> - - Dark/light mode - setUIMode("system")}> - Follow OS setting - - setUIMode("dark")}> - Dark mode - - setUIMode("light")}> - Light mode - - - + + + + + + setAnchorEl(null)} + open={Boolean(anchorEl)} + > + onClick("system")} selected={uiMode === "system"}> + Follow OS setting + + onClick("dark")} selected={uiMode === "dark"}> + Dark mode + + onClick("light")} selected={uiMode === "light"}> + Light mode + + + + ) } UIModeMenu.propTypes = { diff --git a/components/frontend/src/header_footer/UIModeMenu.test.js b/components/frontend/src/header_footer/UIModeMenu.test.js index b53829ae82..74c069e004 100644 --- a/components/frontend/src/header_footer/UIModeMenu.test.js +++ b/components/frontend/src/header_footer/UIModeMenu.test.js @@ -3,30 +3,45 @@ import userEvent from "@testing-library/user-event" import { UIModeMenu } from "./UIModeMenu" +const openUIModeMenu = () => fireEvent.click(screen.getByLabelText(/Dark\/light mode/)) +const click = (mode) => fireEvent.click(screen.getByText(mode)) + it("sets dark mode", () => { const setUIMode = jest.fn() render() - fireEvent.click(screen.getByText(/Dark mode/)) + openUIModeMenu() + click("Dark mode") expect(setUIMode).toHaveBeenCalledWith("dark") }) it("sets light mode", () => { const setUIMode = jest.fn() render() - fireEvent.click(screen.getByText(/Light mode/)) + openUIModeMenu() + click("Light mode") expect(setUIMode).toHaveBeenCalledWith("light") }) it("sets follows os mode", () => { const setUIMode = jest.fn() render() - fireEvent.click(screen.getByText(/Follow OS/)) + openUIModeMenu() + click("Follow OS setting") expect(setUIMode).toHaveBeenCalledWith("system") }) it("sets dark mode on keypress", async () => { const setUIMode = jest.fn() - render() + render() + openUIModeMenu() await userEvent.type(screen.getByText(/Dark mode/), " ") expect(setUIMode).toHaveBeenCalledWith("dark") }) + +it("closes the menu on escape", async () => { + const setUIMode = jest.fn() + render() + openUIModeMenu() + await userEvent.keyboard("{Escape}") + expect(setUIMode).not.toHaveBeenCalled() +}) diff --git a/components/frontend/src/header_footer/buttons/CollapseButton.js b/components/frontend/src/header_footer/buttons/CollapseButton.js new file mode 100644 index 0000000000..e3b21eedbf --- /dev/null +++ b/components/frontend/src/header_footer/buttons/CollapseButton.js @@ -0,0 +1,25 @@ +import UnfoldLessIcon from "@mui/icons-material/UnfoldLess" +import { Button, Tooltip } from "@mui/material" + +import { stringsURLSearchQueryPropType } from "../../sharedPropTypes" + +export function CollapseButton({ expandedItems }) { + return ( + + + expandedItems.reset()} + startIcon={} + sx={{ height: "100%" }} + > + Collapse all + + + + ) +} +CollapseButton.propTypes = { + expandedItems: stringsURLSearchQueryPropType, +} diff --git a/components/frontend/src/header_footer/CollapseButton.test.js b/components/frontend/src/header_footer/buttons/CollapseButton.test.js similarity index 87% rename from components/frontend/src/header_footer/CollapseButton.test.js rename to components/frontend/src/header_footer/buttons/CollapseButton.test.js index 0d9ee2bc54..2bd044e4b2 100644 --- a/components/frontend/src/header_footer/CollapseButton.test.js +++ b/components/frontend/src/header_footer/buttons/CollapseButton.test.js @@ -1,8 +1,8 @@ import { fireEvent, render, renderHook, screen } from "@testing-library/react" import history from "history/browser" -import { createTestableSettings } from "../__fixtures__/fixtures" -import { useExpandedItemsSearchQuery } from "../app_ui_settings" +import { createTestableSettings } from "../../__fixtures__/fixtures" +import { useExpandedItemsSearchQuery } from "../../app_ui_settings" import { CollapseButton } from "./CollapseButton" beforeEach(() => { @@ -19,7 +19,7 @@ it("resets the expanded items", () => { const expandedItems = renderHook(() => useExpandedItemsSearchQuery()) expect(expandedItems.result.current.value).toStrictEqual(["tab"]) renderCollapseButton({ expandedItems: expandedItems.result.current }) - fireEvent.click(screen.getByRole("button", { name: "Collapse all headers and metrics" })) + fireEvent.click(screen.getByRole("button", { name: "Collapse all" })) expandedItems.rerender() expect(expandedItems.result.current.value).toStrictEqual([]) }) @@ -28,7 +28,7 @@ it("doesn't change the expanded items if there are none", () => { const expandedItems = renderHook(() => useExpandedItemsSearchQuery()) expect(expandedItems.result.current.value).toStrictEqual([]) renderCollapseButton({ expandedItems: expandedItems.result.current }) - fireEvent.click(screen.getByRole("button", { name: "Collapse all headers and metrics" })) + fireEvent.click(screen.getByRole("button", { name: "Collapse all" })) expandedItems.rerender() expect(expandedItems.result.current.value).toStrictEqual([]) }) diff --git a/components/frontend/src/header_footer/buttons/DatePickerButton.js b/components/frontend/src/header_footer/buttons/DatePickerButton.js new file mode 100644 index 0000000000..0089be1149 --- /dev/null +++ b/components/frontend/src/header_footer/buttons/DatePickerButton.js @@ -0,0 +1,48 @@ +import EventIcon from "@mui/icons-material/Event" +import { Button, Menu, Tooltip } from "@mui/material" +import { StaticDatePicker } from "@mui/x-date-pickers/StaticDatePicker" +import { func } from "prop-types" +import { useState } from "react" + +import { datePropType } from "../../sharedPropTypes" + +export function DatePickerButton({ onChange, reportDate }) { + const [anchorEl, setAnchorEl] = useState() + const handleMenu = (event) => setAnchorEl(event.currentTarget) + const handleClose = () => setAnchorEl(null) + return ( + + + } + sx={{ height: "100%" }} + > + {reportDate ? reportDate.toDateString() : "today"} + + + { + handleClose() + onChange(value) + }} + slots={{ toolbar: null }} + slotProps={{ + actionBar: { + actions: ["today"], + }, + }} + /> + + + + ) +} +DatePickerButton.propTypes = { + onChange: func, + reportDate: datePropType, +} diff --git a/components/frontend/src/header_footer/DownloadAsPDFButton.js b/components/frontend/src/header_footer/buttons/DownloadAsPDFButton.js similarity index 61% rename from components/frontend/src/header_footer/DownloadAsPDFButton.js rename to components/frontend/src/header_footer/buttons/DownloadAsPDFButton.js index 565e9f7181..1ee66cc0fd 100644 --- a/components/frontend/src/header_footer/DownloadAsPDFButton.js +++ b/components/frontend/src/header_footer/buttons/DownloadAsPDFButton.js @@ -1,11 +1,12 @@ +import PictureAsPdf from "@mui/icons-material/PictureAsPdf" +import { LoadingButton } from "@mui/lab" +import { Tooltip } from "@mui/material" import { string } from "prop-types" import { useState } from "react" -import { Icon } from "semantic-ui-react" -import { get_report_pdf } from "../api/report" -import { registeredURLSearchParams } from "../hooks/url_search_query" -import { Button, Popup } from "../semantic_ui_react_wrappers" -import { showMessage } from "../widgets/toast" +import { get_report_pdf } from "../../api/report" +import { registeredURLSearchParams } from "../../hooks/url_search_query" +import { showMessage } from "../../widgets/toast" function download_pdf(report_uuid, query_string, callback) { const reportId = report_uuid ? `report-${report_uuid}` : "reports-overview" @@ -41,29 +42,22 @@ export function DownloadAsPDFButton({ report_uuid }) { const itemType = report_uuid ? "report" : "reports overview" const label = `Download ${itemType} as PDF` return ( - { - if (!loading) { - setLoading(true) - download_pdf(report_uuid, `?${query.toString()}`, () => { - setLoading(false) - }) - } - }} - inverted - > - Download as PDF - - } - content={`Generate a PDF version of the ${itemType} as currently displayed. This may take some time.`} - /> + + { + setLoading(true) + download_pdf(report_uuid, `?${query.toString()}`, () => { + setLoading(false) + }) + }} + startIcon={} + > + Download as PDF + + ) } DownloadAsPDFButton.propTypes = { diff --git a/components/frontend/src/header_footer/buttons/DownloadAsPDFButton.test.js b/components/frontend/src/header_footer/buttons/DownloadAsPDFButton.test.js new file mode 100644 index 0000000000..fe1d7465ef --- /dev/null +++ b/components/frontend/src/header_footer/buttons/DownloadAsPDFButton.test.js @@ -0,0 +1,100 @@ +import { act, fireEvent, render, screen, waitFor } from "@testing-library/react" +import history from "history/browser" + +import * as get_report_pdf from "../../api/report" +import * as toast from "../../widgets/toast" +import { DownloadAsPDFButton } from "./DownloadAsPDFButton" + +jest.mock("../../api/report") +jest.mock("../../widgets/toast.js") + +beforeEach(() => { + jest.resetAllMocks() + get_report_pdf.get_report_pdf.mockImplementation(() => Promise.resolve({ ok: true })) +}) + +test("DownloadAsPDFButton has the correct label for reports overview", () => { + render() + expect(screen.getAllByLabelText(/reports overview as PDF/).length).toBe(1) +}) + +test("DownloadAsPDFButton has the correct label for a report", () => { + render() + expect(screen.getAllByLabelText(/report as PDF/).length).toBe(1) +}) + +const test_report = { report_uuid: "report_uuid" } + +async function clickDownload(nrClicks = 1) { + for (let step = 0; step < nrClicks; step++) { + await act(async () => { + fireEvent.click(screen.getByText(/Download as PDF/)) + }) + } +} + +test("DownloadAsPDFButton indicates loading on click", async () => { + get_report_pdf.get_report_pdf.mockImplementation(() => { + // See https://github.com/eslint-community/eslint-plugin-promise/issues/111#issuecomment-663824152 + // eslint-disable-line promise/avoid-new + return new Promise((resolve) => setTimeout(resolve, 100)) + }) + render() + await clickDownload() + expect(screen.getAllByLabelText(/Download/)[0].className).toContain("MuiCircularProgress-indeterminate") + expect(get_report_pdf.get_report_pdf).toHaveBeenCalledWith("report_uuid", "?report_url=http%3A%2F%2Flocalhost%2F") + await waitFor(() => { + expect(toast.showMessage).toHaveBeenCalledTimes(1) + }) +}) + +test("DownloadAsPDFButton ignores unregistered query parameters", async () => { + history.push("?unregister_key=value&nr_dates=4") + get_report_pdf.get_report_pdf.mockImplementation(() => Promise.resolve({ ok: true })) + render() + await clickDownload() + expect(get_report_pdf.get_report_pdf).toHaveBeenCalledWith( + "report_uuid", + "?nr_dates=4&report_url=http%3A%2F%2Flocalhost%2F%3Fnr_dates%3D4", + ) +}) + +test("DownloadAsPDFButton ignores a second click", async () => { + get_report_pdf.get_report_pdf.mockImplementation(() => Promise.resolve({ ok: true })) + HTMLAnchorElement.prototype.click = jest.fn() // Prevent "Not implemented: navigation (except hash changes)" + window.URL.createObjectURL = jest.fn() + render() + await clickDownload(2) + expect(screen.getByLabelText(/Download/).className).not.toContain("MuiCircularProgress-indeterminate") + expect(toast.showMessage).toHaveBeenCalledTimes(0) +}) + +test("DownloadAsPDFButton stops loading after returning pdf", async () => { + get_report_pdf.get_report_pdf.mockImplementation(() => Promise.resolve({ ok: true })) + HTMLAnchorElement.prototype.click = jest.fn() // Prevent "Not implemented: navigation (except hash changes)" + window.URL.createObjectURL = jest.fn() + render() + await clickDownload() + expect(screen.getByLabelText(/Download/).className).not.toContain("MuiCircularProgress-indeterminate") + expect(toast.showMessage).toHaveBeenCalledTimes(0) +}) + +test("DownloadAsPDFButton stops loading after receiving error", async () => { + get_report_pdf.get_report_pdf.mockImplementation(() => + Promise.resolve({ ok: false, status: "403", statusText: "access denied" }), + ) + render() + await clickDownload() + expect(screen.getByLabelText(/Download/).className).not.toContain("MuiCircularProgress-indeterminate") + expect(toast.showMessage).toHaveBeenCalledTimes(1) + expect(toast.showMessage).toHaveBeenCalledWith("error", "PDF rendering failed", "HTTP code 403: access denied") +}) + +test("DownloadAsPDFButton catches errors", async () => { + get_report_pdf.get_report_pdf.mockImplementation(() => Promise.reject(new Error("Oops"))) + render() + await clickDownload() + expect(screen.getByLabelText(/Download/).className).not.toContain("MuiCircularProgress-indeterminate") + expect(toast.showMessage).toHaveBeenCalledTimes(1) + expect(toast.showMessage).toHaveBeenCalledWith("error", "Could not fetch PDF report", "Error: Oops") +}) diff --git a/components/frontend/src/header_footer/buttons/HomeButton.js b/components/frontend/src/header_footer/buttons/HomeButton.js new file mode 100644 index 0000000000..d7de1fa8a3 --- /dev/null +++ b/components/frontend/src/header_footer/buttons/HomeButton.js @@ -0,0 +1,29 @@ +import { Button, Tooltip, Typography } from "@mui/material" +import { bool, func } from "prop-types" + +export function HomeButton({ atReportsOverview, openReportsOverview, setSettingsPanelVisible }) { + const label = "Go to reports overview" + return ( + + + { + setSettingsPanelVisible(false) + openReportsOverview() + }} + startIcon={} + sx={{ textTransform: "none" }} + > + Quality-time + + + + ) +} +HomeButton.propTypes = { + atReportsOverview: bool, + openReportsOverview: func, + setSettingsPanelVisible: func, +} diff --git a/components/frontend/src/header_footer/buttons/ResetSettingsButton.js b/components/frontend/src/header_footer/buttons/ResetSettingsButton.js new file mode 100644 index 0000000000..fad6c8fea9 --- /dev/null +++ b/components/frontend/src/header_footer/buttons/ResetSettingsButton.js @@ -0,0 +1,33 @@ +import SettingsBackupRestoreIcon from "@mui/icons-material/SettingsBackupRestore" +import { Button, Tooltip } from "@mui/material" +import { bool, func } from "prop-types" + +import { optionalDatePropType, settingsPropType } from "../../sharedPropTypes" + +export function ResetSettingsButton({ atReportsOverview, handleDateChange, reportDate, settings }) { + const label = `Reset ${atReportsOverview ? "reports overview" : "this report's"} settings` + return ( + + + } + onClick={() => { + handleDateChange(null) + settings.reset() + }} + sx={{ height: "100%" }} + > + Reset settings + + + + ) +} +ResetSettingsButton.propTypes = { + atReportsOverview: bool, + handleDateChange: func, + reportDate: optionalDatePropType, + settings: settingsPropType, +} diff --git a/components/frontend/src/header_footer/ResetSettingsButton.test.js b/components/frontend/src/header_footer/buttons/ResetSettingsButton.test.js similarity index 88% rename from components/frontend/src/header_footer/ResetSettingsButton.test.js rename to components/frontend/src/header_footer/buttons/ResetSettingsButton.test.js index 25630f0bd9..f1aa984f18 100644 --- a/components/frontend/src/header_footer/ResetSettingsButton.test.js +++ b/components/frontend/src/header_footer/buttons/ResetSettingsButton.test.js @@ -1,7 +1,7 @@ import { fireEvent, render, screen } from "@testing-library/react" import history from "history/browser" -import { createTestableSettings } from "../__fixtures__/fixtures" +import { createTestableSettings } from "../../__fixtures__/fixtures" import { ResetSettingsButton } from "./ResetSettingsButton" beforeEach(() => { @@ -38,7 +38,7 @@ it("resets the settings", async () => { reportDate: new Date("2023-01-01"), settings: settings, }) - fireEvent.click(screen.getAllByLabelText(/Reset reports overview settings/)[0]) + fireEvent.click(screen.getAllByText(/Reset settings/)[0]) expect(history.location.search).toEqual("") expect(handleDateChange).toHaveBeenCalledWith(null) }) @@ -51,6 +51,6 @@ it("does not reset the settings when all have the default value", async () => { handleDateChange: handleDateChange, settings: settings, }) - fireEvent.click(screen.getAllByLabelText(/Reset this report's settings/)[0]) + fireEvent.click(screen.getAllByText(/Reset settings/)[0]) expect(handleDateChange).not.toHaveBeenCalled() }) diff --git a/components/frontend/src/header_footer/buttons/SettingsButton.js b/components/frontend/src/header_footer/buttons/SettingsButton.js new file mode 100644 index 0000000000..0ad445148a --- /dev/null +++ b/components/frontend/src/header_footer/buttons/SettingsButton.js @@ -0,0 +1,20 @@ +import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown" +import ArrowRightIcon from "@mui/icons-material/ArrowRight" +import { Button } from "@mui/material" +import { bool, func } from "prop-types" + +export function SettingsButton({ settingsPanelVisible, setSettingsPanelVisible }) { + return ( + : } + onClick={() => setSettingsPanelVisible(!settingsPanelVisible)} + > + Settings + + ) +} +SettingsButton.propTypes = { + settingsPanelVisible: bool, + setSettingsPanelVisible: func, +} diff --git a/components/frontend/src/header_footer/settings_menu/SettingsMenu.css b/components/frontend/src/header_footer/settings_menu/SettingsMenu.css deleted file mode 100644 index 5a1ef65c1b..0000000000 --- a/components/frontend/src/header_footer/settings_menu/SettingsMenu.css +++ /dev/null @@ -1,4 +0,0 @@ -.ui.horizontal.segments.equal.width > .ui.segment { - flex-grow: 1; - width: 0; -} diff --git a/components/frontend/src/header_footer/settings_menu/SettingsMenu.js b/components/frontend/src/header_footer/settings_menu/SettingsMenu.js index 87e6fa4d0c..b87b069ca3 100644 --- a/components/frontend/src/header_footer/settings_menu/SettingsMenu.js +++ b/components/frontend/src/header_footer/settings_menu/SettingsMenu.js @@ -1,18 +1,13 @@ -import "./SettingsMenu.css" - +import { MenuItem, MenuList, Stack, Tooltip, Typography } from "@mui/material" import { bool, func, number, oneOfType, string } from "prop-types" -import { Header, Menu, Segment } from "semantic-ui-react" -import { Popup } from "../../semantic_ui_react_wrappers" import { childrenPropType, popupContentPropType } from "../../sharedPropTypes" -const activeColor = "grey" - export function SettingsMenuGroup({ children }) { return ( - + {children} - + ) } SettingsMenuGroup.propTypes = { @@ -20,12 +15,11 @@ SettingsMenuGroup.propTypes = { } export function SettingsMenu({ children, title }) { - const menuProps = { compact: true, vertical: true, inverted: true, secondary: true } return ( - - {title} - {children} - + + {title} + {children} + ) } SettingsMenu.propTypes = { @@ -36,8 +30,6 @@ SettingsMenu.propTypes = { export function SettingsMenuItem({ active, children, disabled, disabledHelp, help, onClick, onClickData }) { // A menu item that can can show help when disabled so users can see why the menu item is disabled const props = { - active: active, - color: activeColor, disabled: disabled, onBeforeInput: (event) => { event.preventDefault() @@ -49,25 +41,19 @@ export function SettingsMenuItem({ active, children, disabled, disabledHelp, hel event.preventDefault() onClick(onClickData) }, + selected: active, tabIndex: 0, } if (help || (disabledHelp && disabled)) { - props["style"] = { marginLeft: 0, marginRight: 0, marginBottom: 5 } // Compensate for the span return ( - - {children} - - } - /> + + + {children} + + ) } - return {children} + return {children} } SettingsMenuItem.propTypes = { active: bool, diff --git a/components/frontend/src/header_footer/settings_menu/SortColumnMenu.js b/components/frontend/src/header_footer/settings_menu/SortColumnMenu.js index 4d8077d84f..df378d6cc9 100644 --- a/components/frontend/src/header_footer/settings_menu/SortColumnMenu.js +++ b/components/frontend/src/header_footer/settings_menu/SortColumnMenu.js @@ -1,6 +1,7 @@ +import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown" +import ArrowDropUpIcon from "@mui/icons-material/ArrowDropUp" import { bool, func, string } from "prop-types" -import { Icon } from "../../semantic_ui_react_wrappers" import { popupContentPropType, settingsPropType, @@ -85,11 +86,7 @@ SortColumnMenu.propTypes = { function SortColumnMenuItem({ column, disabled, sortColumn, sortDirection, handleSort, help }) { let sortIndicator = null if (sortColumn.equals(column) && sortDirection.value) { - // We use a triangle because the sort down and up icons are not at the same height - const iconDirection = sortDirection.equals("ascending") ? "up" : "down" - sortIndicator = ( - - ) + sortIndicator = sortDirection.equals("ascending") ? : } return ( - {capitalize(column === "name" ? "metric" : column).replaceAll("_", " ")} {sortIndicator} + {capitalize(column === "name" ? "metric" : column).replaceAll("_", " ")} {sortIndicator} ) } diff --git a/components/frontend/src/widgets/IconCombi.js b/components/frontend/src/widgets/IconCombi.js deleted file mode 100644 index 3ac1972780..0000000000 --- a/components/frontend/src/widgets/IconCombi.js +++ /dev/null @@ -1,17 +0,0 @@ -import { string } from "prop-types" -import { Icon } from "semantic-ui-react" - -export function IconCombi({ iconBottomRight, iconTopLeft, label }) { - const style = { textShadow: "0px 0px" } - return ( - - - - - ) -} -IconCombi.propTypes = { - iconBottomRight: string, - iconTopLeft: string, - label: string, -}