diff --git a/README.md b/README.md
index b87cb004..e95163fe 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,9 @@ This project was bootstrapped with [Create React App](https://github.com/faceboo
In the project directory, you can run:
+### `npm install`
+Installs the dependencies required
+
### `npm start`
Runs the app in the development mode.\
diff --git a/package-lock.json b/package-lock.json
index 3ef4561e..aa257d7f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -24,8 +24,8 @@
"@types/react-router-dom": "^5.3.3",
"axios": "^1.4.0",
"bootstrap": "^5.3.3",
- "chart.js": "^3.7.0",
- "formik": "^2.2.9",
+ "chart.js": "^4.1.1",
+ "formik": "^2.4.6",
"jquery": "^3.7.1",
"jwt-decode": "^3.1.2",
"react": "^18.2.0",
@@ -38,7 +38,8 @@
"react-redux": "^8.0.5",
"react-router-dom": "^6.11.1",
"react-scripts": "^5.0.1",
- "recharts": "^2.12.3",
+ "react-select": "^5.8.3",
+ "recharts": "^2.0.0",
"redux-persist": "^6.0.0",
"sass": "^1.62.1",
"save": "^2.9.0",
@@ -2393,6 +2394,147 @@
"postcss-selector-parser": "^6.0.10"
}
},
+ "node_modules/@emotion/babel-plugin": {
+ "version": "11.13.5",
+ "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
+ "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.16.7",
+ "@babel/runtime": "^7.18.3",
+ "@emotion/hash": "^0.9.2",
+ "@emotion/memoize": "^0.9.0",
+ "@emotion/serialize": "^1.3.3",
+ "babel-plugin-macros": "^3.1.0",
+ "convert-source-map": "^1.5.0",
+ "escape-string-regexp": "^4.0.0",
+ "find-root": "^1.1.0",
+ "source-map": "^0.5.7",
+ "stylis": "4.2.0"
+ }
+ },
+ "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@emotion/babel-plugin/node_modules/source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@emotion/cache": {
+ "version": "11.13.5",
+ "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.5.tgz",
+ "integrity": "sha512-Z3xbtJ+UcK76eWkagZ1onvn/wAVb1GOMuR15s30Fm2wrMgC7jzpnO2JZXr4eujTTqoQFUrZIw/rT0c6Zzjca1g==",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/memoize": "^0.9.0",
+ "@emotion/sheet": "^1.4.0",
+ "@emotion/utils": "^1.4.2",
+ "@emotion/weak-memoize": "^0.4.0",
+ "stylis": "4.2.0"
+ }
+ },
+ "node_modules/@emotion/hash": {
+ "version": "0.9.2",
+ "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
+ "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/memoize": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
+ "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/react": {
+ "version": "11.13.5",
+ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.5.tgz",
+ "integrity": "sha512-6zeCUxUH+EPF1s+YF/2hPVODeV/7V07YU5x+2tfuRL8MdW6rv5vb2+CBEGTGwBdux0OIERcOS+RzxeK80k2DsQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "@emotion/babel-plugin": "^11.13.5",
+ "@emotion/cache": "^11.13.5",
+ "@emotion/serialize": "^1.3.3",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0",
+ "@emotion/utils": "^1.4.2",
+ "@emotion/weak-memoize": "^0.4.0",
+ "hoist-non-react-statics": "^3.3.1"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@emotion/serialize": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
+ "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/hash": "^0.9.2",
+ "@emotion/memoize": "^0.9.0",
+ "@emotion/unitless": "^0.10.0",
+ "@emotion/utils": "^1.4.2",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@emotion/sheet": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
+ "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/unitless": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
+ "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/use-insertion-effect-with-fallbacks": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz",
+ "integrity": "sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@emotion/utils": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
+ "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/weak-memoize": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
+ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
+ "license": "MIT"
+ },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@@ -2486,6 +2628,31 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@floating-ui/core": {
+ "version": "1.6.8",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz",
+ "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.8"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.6.12",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz",
+ "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.6.0",
+ "@floating-ui/utils": "^0.2.8"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.8",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz",
+ "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==",
+ "license": "MIT"
+ },
"node_modules/@fortawesome/fontawesome-common-types": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz",
@@ -3045,6 +3212,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@kurkle/color": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
+ "license": "MIT"
+ },
"node_modules/@leichtgewicht/ip-codec": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
@@ -5760,9 +5933,16 @@
}
},
"node_modules/chart.js": {
- "version": "3.7.0",
- "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.7.0.tgz",
- "integrity": "sha512-31gVuqqKp3lDIFmzpKIrBeum4OpZsQjSIAqlOpgjosHDJZlULtvwLEZKtEhIAZc7JMPaHlYMys40Qy9Mf+1AAg=="
+ "version": "4.4.6",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.6.tgz",
+ "integrity": "sha512-8Y406zevUPbbIBA/HRk33khEmQPk5+cxeflWE/2rx1NJsjVWMPw/9mSP9rxHP5eqi6LNoPBVMfZHxbwLSgldYA==",
+ "license": "MIT",
+ "dependencies": {
+ "@kurkle/color": "^0.3.0"
+ },
+ "engines": {
+ "pnpm": ">=8"
+ }
},
"node_modules/check-types": {
"version": "11.2.3",
@@ -8363,6 +8543,12 @@
"url": "https://github.com/avajs/find-cache-dir?sponsor=1"
}
},
+ "node_modules/find-root": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
+ "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==",
+ "license": "MIT"
+ },
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -8563,15 +8749,16 @@
}
},
"node_modules/formik": {
- "version": "2.4.5",
- "resolved": "https://registry.npmjs.org/formik/-/formik-2.4.5.tgz",
- "integrity": "sha512-Gxlht0TD3vVdzMDHwkiNZqJ7Mvg77xQNfmBRrNtvzcHZs72TJppSTDKHpImCMJZwcWPBJ8jSQQ95GJzXFf1nAQ==",
+ "version": "2.4.6",
+ "resolved": "https://registry.npmjs.org/formik/-/formik-2.4.6.tgz",
+ "integrity": "sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g==",
"funding": [
{
"type": "individual",
"url": "https://opencollective.com/formik"
}
],
+ "license": "Apache-2.0",
"dependencies": {
"@types/hoist-non-react-statics": "^3.3.1",
"deepmerge": "^2.1.1",
@@ -11372,6 +11559,12 @@
"node": ">= 4.0.0"
}
},
+ "node_modules/memoize-one": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
+ "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
+ "license": "MIT"
+ },
"node_modules/merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
@@ -14168,6 +14361,27 @@
}
}
},
+ "node_modules/react-select": {
+ "version": "5.8.3",
+ "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.3.tgz",
+ "integrity": "sha512-lVswnIq8/iTj1db7XCG74M/3fbGB6ZaluCzvwPGT5ZOjCdL/k0CLWhEK0vCBLuU5bHTEf6Gj8jtSvi+3v+tO1w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.0",
+ "@emotion/cache": "^11.4.0",
+ "@emotion/react": "^11.8.1",
+ "@floating-ui/dom": "^1.0.1",
+ "@types/react-transition-group": "^4.4.0",
+ "memoize-one": "^6.0.0",
+ "prop-types": "^15.6.0",
+ "react-transition-group": "^4.3.0",
+ "use-isomorphic-layout-effect": "^1.1.2"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/react-smooth": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.1.tgz",
@@ -15628,6 +15842,12 @@
"postcss": "^8.2.15"
}
},
+ "node_modules/stylis": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
+ "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
+ "license": "MIT"
+ },
"node_modules/sucrase": {
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
@@ -16529,6 +16749,20 @@
"requires-port": "^1.0.0"
}
},
+ "node_modules/use-isomorphic-layout-effect": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
+ "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
@@ -16603,6 +16837,28 @@
"node": ">= 0.8"
}
},
+ "node_modules/victory-vendor": {
+ "version": "36.9.2",
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
+ "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
+ "license": "MIT AND ISC",
+ "dependencies": {
+ "@types/d3-array": "^3.0.3",
+ "@types/d3-ease": "^3.0.0",
+ "@types/d3-interpolate": "^3.0.1",
+ "@types/d3-scale": "^4.0.2",
+ "@types/d3-shape": "^3.1.0",
+ "@types/d3-time": "^3.0.0",
+ "@types/d3-timer": "^3.0.0",
+ "d3-array": "^3.1.6",
+ "d3-ease": "^3.0.1",
+ "d3-interpolate": "^3.0.1",
+ "d3-scale": "^4.0.2",
+ "d3-shape": "^3.1.0",
+ "d3-time": "^3.0.0",
+ "d3-timer": "^3.0.1"
+ }
+ },
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
diff --git a/package.json b/package.json
index a1ed9d64..668f6cb0 100644
--- a/package.json
+++ b/package.json
@@ -20,8 +20,7 @@
"axios": "^1.4.0",
"bootstrap": "^5.3.3",
"chart.js": "^4.1.1",
- "recharts": "^2.0.0",
- "formik": "^2.2.9",
+ "formik": "^2.4.6",
"jquery": "^3.7.1",
"jwt-decode": "^3.1.2",
"react": "^18.2.0",
@@ -34,6 +33,8 @@
"react-redux": "^8.0.5",
"react-router-dom": "^6.11.1",
"react-scripts": "^5.0.1",
+ "react-select": "^5.8.3",
+ "recharts": "^2.0.0",
"redux-persist": "^6.0.0",
"sass": "^1.62.1",
"save": "^2.9.0",
diff --git a/src/components/ColumnButton.test.tsx b/src/components/ColumnButton.test.tsx
new file mode 100644
index 00000000..20e22b42
--- /dev/null
+++ b/src/components/ColumnButton.test.tsx
@@ -0,0 +1,57 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import ColumnButton from "./ColumnButton";
+
+// Mock React-Bootstrap components
+jest.mock("react-bootstrap", () => ({
+ Button: ({ children, ...props }: any) => ,
+ Tooltip: ({ id, children }: any) =>
{children}
,
+ OverlayTrigger: ({ children, overlay }: any) => (
+
+ {overlay}
+ {children}
+
+ ),
+}));
+
+describe("ColumnButton", () => {
+ const mockOnClick = jest.fn();
+
+ const baseProps = {
+ id: "test-button",
+ label: "Test Button",
+ tooltip: "This is a test tooltip",
+ variant: "primary",
+ size: "sm" as const,
+ className: "custom-class",
+ onClick: mockOnClick,
+ icon: Icon,
+ };
+
+ test("should call onClick when the button is clicked", async () => {
+ render();
+ const button = screen.getByRole("button", { name: "Test Button" });
+
+ userEvent.click(button);
+ expect(mockOnClick).toHaveBeenCalledTimes(1);
+ });
+
+ test("should render a tooltip when tooltip is provided", () => {
+ render();
+ const tooltip = screen.getByText("This is a test tooltip");
+
+ // Vanilla assertion to check if tooltip is rendered
+ expect(tooltip).not.toBeNull();
+ const button = screen.getByRole("button", { name: "Test Button" });
+ expect(button).not.toBeNull(); // Ensure the button is still present
+ });
+
+ test("should not render a tooltip when tooltip is not provided", () => {
+ render();
+ const tooltip = screen.queryByText("This is a test tooltip");
+
+ // Vanilla assertion to check if tooltip is not rendered
+ expect(tooltip).toBeNull();
+ });
+});
diff --git a/src/components/ColumnButton.tsx b/src/components/ColumnButton.tsx
new file mode 100644
index 00000000..25eea86f
--- /dev/null
+++ b/src/components/ColumnButton.tsx
@@ -0,0 +1,57 @@
+import React from "react";
+import { Button, OverlayTrigger, Tooltip } from "react-bootstrap";
+
+/**
+ * @author Rutvik Kulkarni on Nov, 2024
+ */
+
+interface ColumnButtonProps {
+ id: string;
+ label?: string;
+ tooltip?: string;
+ variant: string;
+ size?: "sm" | "lg"; // Matches React-Bootstrap Button prop
+ className?: string;
+ onClick: () => void;
+ icon: React.ReactNode;
+ }
+
+ const ColumnButton: React.FC = (props) => {
+ const {
+ id,
+ label,
+ tooltip,
+ variant,
+ size,
+ className,
+ onClick,
+ icon,
+ } = props;
+
+ const displayButton = (
+
+ );
+
+ if (tooltip) {
+ return (
+ {tooltip}}
+ >
+ {displayButton}
+
+ );
+ }
+
+ return displayButton;
+ };
+
+ export default ColumnButton;
\ No newline at end of file
diff --git a/src/pages/Courses/Course.tsx b/src/pages/Courses/Course.tsx
index d1e4db04..53ef2ccd 100644
--- a/src/pages/Courses/Course.tsx
+++ b/src/pages/Courses/Course.tsx
@@ -27,6 +27,7 @@ const Courses = () => {
(state: RootState) => state.authentication,
(prev, next) => prev.isAuthenticated === next.isAuthenticated
);
+ const currUserRole = auth.user.role.valueOf();
const navigate = useNavigate();
const location = useLocation();
const dispatch = useDispatch();
@@ -96,7 +97,7 @@ const Courses = () => {
);
const tableColumns = useMemo(
- () => COURSE_COLUMNS(onEditHandle, onDeleteHandle, onTAHandle, onCopyHandle),
+ () => COURSE_COLUMNS(onEditHandle, onDeleteHandle, onTAHandle, onCopyHandle, currUserRole),
[onDeleteHandle, onEditHandle, onTAHandle, onCopyHandle]
);
diff --git a/src/pages/Courses/CourseColumns.test.tsx b/src/pages/Courses/CourseColumns.test.tsx
new file mode 100644
index 00000000..fbe52543
--- /dev/null
+++ b/src/pages/Courses/CourseColumns.test.tsx
@@ -0,0 +1,104 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { Row } from "@tanstack/react-table";
+import { courseColumns } from "./CourseColumns";
+
+// Mock the ColumnButton component
+jest.mock("../../components/ColumnButton", () => ({ id, ...props }: any) => (
+
+));
+
+describe("courseColumns", () => {
+ const mockHandleEdit = jest.fn();
+ const mockHandleDelete = jest.fn();
+ const mockHandleTA = jest.fn();
+ const mockHandleCopy = jest.fn();
+ const mockRow: Partial> = {
+ original: { id: "123", name: "Test Course", institution: { name: "Test Institution" } },
+ };
+
+ test("should define all required columns", () => {
+ const columns = courseColumns(mockHandleEdit, mockHandleDelete, mockHandleTA, mockHandleCopy, "Super Administrator");
+ expect(columns).toHaveLength(5);
+
+ // Check each column's header
+ expect(columns[0].header).toBe("Name");
+ expect(columns[1].header).toBe("Institution");
+ expect(columns[2].header).toBe("Creation Date");
+ expect(columns[3].header).toBe("Updated Date");
+ expect(columns[4].header).toBe("Actions");
+ });
+
+ test("should call handleEdit when edit button is clicked", async () => {
+ const actionsColumn = courseColumns(
+ mockHandleEdit,
+ mockHandleDelete,
+ mockHandleTA,
+ mockHandleCopy,
+ "Super Administrator"
+ ).find((col) => col.id === "actions");
+ const CellComponent = actionsColumn?.cell as React.FC<{ row: Row }>;
+
+ render(} />);
+ const editButton = screen.getByTestId("edit");
+
+ userEvent.click(editButton);
+ expect(mockHandleEdit).toHaveBeenCalledTimes(1);
+ expect(mockHandleEdit).toHaveBeenCalledWith(mockRow);
+ });
+
+ test("should call handleDelete when delete button is clicked", async () => {
+ const actionsColumn = courseColumns(
+ mockHandleEdit,
+ mockHandleDelete,
+ mockHandleTA,
+ mockHandleCopy,
+ "Super Administrator"
+ ).find((col) => col.id === "actions");
+ const CellComponent = actionsColumn?.cell as React.FC<{ row: Row }>;
+
+ render(} />);
+ const deleteButton = screen.getByTestId("delete");
+
+ userEvent.click(deleteButton);
+ expect(mockHandleDelete).toHaveBeenCalledTimes(1);
+ expect(mockHandleDelete).toHaveBeenCalledWith(mockRow);
+ });
+
+ test("should call handleTA when assign TA button is clicked", async () => {
+ const actionsColumn = courseColumns(
+ mockHandleEdit,
+ mockHandleDelete,
+ mockHandleTA,
+ mockHandleCopy,
+ "Super Administrator"
+ ).find((col) => col.id === "actions");
+ const CellComponent = actionsColumn?.cell as React.FC<{ row: Row }>;
+
+ render(} />);
+ const assignTAButton = screen.getByTestId("assign-ta");
+
+ userEvent.click(assignTAButton);
+ expect(mockHandleTA).toHaveBeenCalledTimes(1);
+ expect(mockHandleTA).toHaveBeenCalledWith(mockRow);
+ });
+
+ test("should call handleCopy when copy button is clicked", async () => {
+ const actionsColumn = courseColumns(
+ mockHandleEdit,
+ mockHandleDelete,
+ mockHandleTA,
+ mockHandleCopy,
+ "Super Administrator"
+ ).find((col) => col.id === "actions");
+ const CellComponent = actionsColumn?.cell as React.FC<{ row: Row }>;
+
+ render(} />);
+ const copyButton = screen.getByTestId("copy");
+
+ userEvent.click(copyButton);
+ expect(mockHandleCopy).toHaveBeenCalledTimes(1);
+ expect(mockHandleCopy).toHaveBeenCalledWith(mockRow);
+ });
+});
diff --git a/src/pages/Courses/CourseColumns.tsx b/src/pages/Courses/CourseColumns.tsx
index 25002d6f..87ff85b9 100644
--- a/src/pages/Courses/CourseColumns.tsx
+++ b/src/pages/Courses/CourseColumns.tsx
@@ -2,80 +2,118 @@ import { createColumnHelper, Row } from "@tanstack/react-table";
import { Button } from "react-bootstrap";
import { BsPencilFill, BsPersonXFill } from "react-icons/bs";
import { MdContentCopy, MdDelete } from "react-icons/md";
-import { ICourseResponse as ICourse } from "../../utils/interfaces";
+import { ICourseResponse as ICourse, ROLE } from "../../utils/interfaces";
+import ColumnButton from "../../components/ColumnButton";
/**
- * @author Atharva Thorve, on December, 2023
- * @author Mrityunjay Joshi on December, 2023
+ * @author Anurag Gorkar, on December, 2024
+ * @author Makarand Pundalik, on December, 2024
+ * @author Rutvik Kulkarni, on December, 2024
*/
+/**
+ * Determines if the current user has the authority to add a TA to a course.
+ * Only users with roles SUPER_ADMIN, ADMIN, or INSTRUCTOR can assign TAs.
+ * @param {String} currUserRole - The role of the current user
+ * @returns {boolean} - True if the user has TA assignment authority, otherwise false
+ */
+const hasAddTAAuthority = (currUserRole: String): boolean => {
+ return (
+ currUserRole === ROLE.SUPER_ADMIN.valueOf() ||
+ currUserRole === ROLE.ADMIN.valueOf() ||
+ currUserRole === ROLE.INSTRUCTOR.valueOf()
+ );
+};
+
// Course Columns Configuration: Defines the columns for the courses table
type Fn = (row: Row) => void;
const columnHelper = createColumnHelper();
-export const courseColumns = (handleEdit: Fn, handleDelete: Fn, handleTA: Fn, handleCopy: Fn) => [
- // Column for the course name
- columnHelper.accessor("name", {
- id: "name",
- header: "Name",
- enableSorting: true,
- enableColumnFilter: true,
- enableGlobalFilter: false,
- }),
- // Column for the institution name
- columnHelper.accessor("institution.name", {
- id: "institution",
- header: "Institution",
- enableSorting: true,
- enableMultiSort: true,
- enableGlobalFilter: false,
- }),
+export const courseColumns = (
+ handleEdit: Fn,
+ handleDelete: Fn,
+ handleTA: Fn,
+ handleCopy: Fn,
+ currUserRole: String
+) => [
+ // Column for the course name
+ columnHelper.accessor("name", {
+ id: "name",
+ header: "Name",
+ enableSorting: true,
+ enableColumnFilter: true,
+ enableGlobalFilter: false,
+ }),
+
+ // Column for the institution name
+ columnHelper.accessor("institution.name", {
+ id: "institution",
+ header: "Institution",
+ enableSorting: true,
+ enableMultiSort: true,
+ enableGlobalFilter: false,
+ }),
- // Column for the creation date
- columnHelper.accessor("created_at", {
- header: "Creation Date",
- enableSorting: true,
- enableColumnFilter: false,
- enableGlobalFilter: false,
- }),
+ // Column for the creation date
+ columnHelper.accessor("created_at", {
+ header: "Creation Date",
+ enableSorting: true,
+ enableColumnFilter: false,
+ enableGlobalFilter: false,
+ }),
- // Column for the last updated date
- columnHelper.accessor("updated_at", {
- header: "Updated Date",
- enableSorting: true,
- enableColumnFilter: false,
- enableGlobalFilter: false,
- }),
+ // Column for the last updated date
+ columnHelper.accessor("updated_at", {
+ header: "Updated Date",
+ enableSorting: true,
+ enableColumnFilter: false,
+ enableGlobalFilter: false,
+ }),
- // Actions column with edit, delete, TA, and copy buttons
- columnHelper.display({
- id: "actions",
- header: "Actions",
- cell: ({ row }) => (
- <>
-
-
-
-
- >
- ),
- }),
-];
+ // Actions column with edit, delete, TA, and copy buttons
+ columnHelper.display({
+ id: "actions",
+ header: "Actions",
+ cell: ({ row }) => (
+ <>
+ handleEdit(row)}
+ tooltip="Edit this course"
+ icon={}
+ />
+ handleDelete(row)}
+ tooltip="Delete this course"
+ icon={}
+ />
+ {hasAddTAAuthority(currUserRole) && ( // Use the helper function to determine TA authority
+ handleTA(row)}
+ tooltip="Assign a TA to this course"
+ icon={}
+ />
+ )}
+ handleCopy(row)}
+ tooltip="Copy course details"
+ icon={}
+ />
+ >
+ ),
+ }),
+ ];
\ No newline at end of file
diff --git a/src/pages/TA/TA.tsx b/src/pages/TA/TA.tsx
index 87a1b334..f15ffcb5 100644
--- a/src/pages/TA/TA.tsx
+++ b/src/pages/TA/TA.tsx
@@ -12,6 +12,7 @@ import { alertActions } from "store/slices/alertSlice";
import { RootState } from "../../store/store";
import { ITAResponse, ROLE } from "../../utils/interfaces";
import { TAColumns as TA_COLUMNS } from "./TAColumns";
+import ColumnButton from "../../components/ColumnButton";
import DeleteTA from "./TADelete";
/**
@@ -85,26 +86,37 @@ const TAs = () => {
-
-
+
+ navigate("new")}
+ tooltip="Add TA to this course"
+ icon={}
+ />
- {showDeleteConfirmation.visible && (
-
- )}
-
-
-
+ {tableData.length === 0 ? (
+
+
+ No TAs are assigned for this course.
+
+
+ ) : (
+
+
+
+ )}
diff --git a/src/pages/TA/TAColumns.test.tsx b/src/pages/TA/TAColumns.test.tsx
new file mode 100644
index 00000000..d1e136f2
--- /dev/null
+++ b/src/pages/TA/TAColumns.test.tsx
@@ -0,0 +1,49 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { Row } from "@tanstack/react-table";
+import { TAColumns } from "./TAColumns";
+
+// Mock the ColumnButton component
+jest.mock("../../components/ColumnButton", () => ({ id, ...props }: any) => (
+
+));
+
+describe("TAColumns", () => {
+ const mockHandleDelete = jest.fn();
+ const mockRow: Partial> = { original: { id: "123", name: "Test TA" } };
+
+ test("should define all required columns", () => {
+ const columns = TAColumns(mockHandleDelete);
+ expect(columns).toHaveLength(5);
+
+ // Check each column's header
+ expect(columns[0].header).toBe("Id");
+ expect(columns[1].header).toBe("TA Name");
+ expect(columns[2].header).toBe("Full Name");
+ expect(columns[3].header).toBe("Email");
+ expect(columns[4].header).toBe("Actions");
+ });
+
+ test("should correctly render the actions column", () => {
+ const actionsColumn = TAColumns(mockHandleDelete).find((col) => col.id === "actions");
+ expect(actionsColumn).toBeDefined();
+ const CellComponent = actionsColumn?.cell as React.FC<{ row: Row }>;
+
+ render(} />);
+ const deleteButton = screen.getByTestId("delete-ta");
+ expect(deleteButton).toBeTruthy();
+ });
+
+ test("should call handleDelete when delete button is clicked", async () => {
+ const actionsColumn = TAColumns(mockHandleDelete).find((col) => col.id === "actions");
+ const CellComponent = actionsColumn?.cell as React.FC<{ row: Row }>;
+
+ render(} />);
+ const deleteButton = screen.getByTestId("delete-ta");
+
+ await userEvent.click(deleteButton);
+ expect(mockHandleDelete).toHaveBeenCalledTimes(1);
+ expect(mockHandleDelete).toHaveBeenCalledWith(mockRow);
+ });
+});
diff --git a/src/pages/TA/TAColumns.tsx b/src/pages/TA/TAColumns.tsx
index c4545b5f..03b7b88f 100644
--- a/src/pages/TA/TAColumns.tsx
+++ b/src/pages/TA/TAColumns.tsx
@@ -3,6 +3,7 @@ import { createColumnHelper, Row } from "@tanstack/react-table";
import { Button } from "react-bootstrap";
import { BsPersonXFill } from "react-icons/bs";
import { ITAResponse as ITA } from "../../utils/interfaces";
+import ColumnButton from "../../components/ColumnButton";
/**
* @author Atharva Thorve, on December, 2023
@@ -38,14 +39,15 @@ export const TAColumns = (handleDelete: Fn) => [
header: "Actions",
cell: ({ row }) => (
<>
-
+ tooltip="Delete TA"
+ icon={}
+ />
>
),
}),
diff --git a/src/pages/TA/TAEditor.tsx b/src/pages/TA/TAEditor.tsx
index 6d742c05..20ff8752 100644
--- a/src/pages/TA/TAEditor.tsx
+++ b/src/pages/TA/TAEditor.tsx
@@ -1,8 +1,7 @@
-// Importing necessary interfaces and modules
-import FormSelect from "components/Form/FormSelect";
+import React, { useEffect, useState } from "react";
+import Select from 'react-select';
import { Form, Formik, FormikHelpers } from "formik";
import useAPI from "hooks/useAPI";
-import React, { useEffect } from "react";
import { Button, InputGroup, Modal } from "react-bootstrap";
import { useDispatch } from "react-redux";
import { useLoaderData, useLocation, useNavigate, useParams } from "react-router-dom";
@@ -13,10 +12,19 @@ import { IEditor } from "../../utils/interfaces";
import { ITAFormValues, transformTARequest } from "./TAUtil";
/**
- * @author Atharva Thorve, on December, 2023
- * @author Divit Kalathil, on December, 2023
+ * @author Anurag Gorkar, on December, 2024
+ * @author Makarand Pundalik, on December, 2024
+ * @author Rutvik Kulkarni, on December, 2024
*/
+
+// Type definition for user options
+type UserOption = {
+ label: string;
+ value: string | number;
+ role?: string;
+};
+
const initialValues: ITAFormValues = {
name: "",
};
@@ -29,7 +37,6 @@ const TAEditor: React.FC = ({ mode }) => {
const { data: TAResponse, error: TAError, sendRequest } = useAPI();
const TAData = { ...initialValues };
- // Load data from the server
const { taUsers }: any = useLoaderData();
const dispatch = useDispatch();
const navigate = useNavigate();
@@ -37,9 +44,9 @@ const TAEditor: React.FC = ({ mode }) => {
const params = useParams();
const { courseId } = params;
- // logged-in TA is the parent of the TA being created and the institution is the same as the parent's
+ const [showConfirmModal, setShowConfirmModal] = useState(false);
+ const [selectedUser, setSelectedUser] = useState({ label: "", value: "" });
- // Close the modal if the TA is updated successfully and navigate to the TAs page
useEffect(() => {
if (TAResponse && TAResponse.status >= 200 && TAResponse.status < 300) {
dispatch(
@@ -48,80 +55,130 @@ const TAEditor: React.FC = ({ mode }) => {
message: `TA ${TAData.name} ${mode}d successfully!`,
})
);
- navigate(location.state?.from ? location.state.from : "/TAs");
+ navigate(location.state?.from ? location.state.from : `/courses/${courseId}/tas`);
}
- }, [dispatch, mode, navigate, TAData.name, TAResponse, location.state?.from]);
+ }, [dispatch, mode, navigate, TAData.name, TAResponse, location.state?.from, showConfirmModal]);
- // Show the error message if the TA is not updated successfully
useEffect(() => {
TAError && dispatch(alertActions.showAlert({ variant: "danger", message: TAError }));
}, [TAError, dispatch]);
const onSubmit = (values: ITAFormValues, submitProps: FormikHelpers) => {
+ const selectedUserData = taUsers.find((user: UserOption) =>
+ parseInt(String(user.value)) === parseInt(String(values.name))
+ );
+
+ if (selectedUserData?.role === 'student') {
+ // If selected user is a student, show confirmation modal
+ console.log("Student role detected...", selectedUserData);
+ setSelectedUser(selectedUserData);
+ setShowConfirmModal(true);
+ } else {
+ // If TA or other role, directly submit
+ submitTA(values);
+ }
+ submitProps.setSubmitting(false);
+ };
+
+ const submitTA = (values: ITAFormValues) => {
let method: HttpMethod = HttpMethod.GET;
- // ToDo: Need to create API in the backend for this call.
- // Note: The current API needs the TA id to create a new TA which is incorrect and needs to be fixed.
- // Currently we send the username of the user we want to add as the TA for the course.
let url: string = `/courses/${courseId}/add_ta/${values.name}`;
- // to be used to display message when TA is created
sendRequest({
url: url,
method: method,
data: {},
transformRequest: transformTARequest,
});
- submitProps.setSubmitting(false);
+ };
+
+ const handleConfirmAddStudent = () => {
+ // Submit TA addition if confirmed
+ submitTA({ name: String(selectedUser.value) });
+ setShowConfirmModal(false);
};
const handleClose = () => navigate(location.state?.from ? location.state.from : `/courses/${courseId}/tas`);
- //Validation of TA Entry
+
return (
-
-
- Add TA
-
-
- {TAError && {TAError}
}
-
- {(formik) => {
- return (
-
- );
- }}
-
-
-
+ <>
+
+
+ Add TA
+
+
+ {TAError && {TAError}
}
+
+ {(formik) => {
+ return (
+
+ );
+ }}
+
+
+
+
+ {/* Confirmation Modal for Student */}
+ setShowConfirmModal(false)} centered>
+
+ Confirm Adding Student as TA
+
+
+ Are you sure you want to add {selectedUser.label} (a student) as a Teaching Assistant for this course?
+ This action will convert {selectedUser.label} to a TA.
+
+
+
+
+
+
+ >
);
};
-export default TAEditor;
+export default TAEditor;
\ No newline at end of file
diff --git a/src/pages/TA/TAUtil.ts b/src/pages/TA/TAUtil.ts
index d49d99fa..15c13a95 100644
--- a/src/pages/TA/TAUtil.ts
+++ b/src/pages/TA/TAUtil.ts
@@ -4,8 +4,9 @@ import axiosClient from "utils/axios_client";
import { ITA, ITARequest } from "../../utils/interfaces";
/**
- * @author Atharva Thorve, on December, 2023
- * @author Divit Kalathil, on December, 2023
+ * @author Anurag Gorkar, on December, 2024
+ * @author Makarand Pundalik, on December, 2024
+ * @author Rutvik Kulkarni, on December, 2024
*/
/**
@@ -17,7 +18,7 @@ export interface ITAFormValues {
}
export const transformTAResponse = (taList: string) => {
- let taData: IFormOption[] = [{ label: "Select a TA", value: "" }];
+ let taData: IFormOption[] = [];
let tas: ITA[] = JSON.parse(taList);
tas.forEach((ta) => taData.push({ label: ta.name, value: ta.id! }));
return taData;
@@ -37,7 +38,16 @@ export async function loadTAs({ params }: any) {
const taRoleUsersResponse = await axiosClient.get(`/users/role/Teaching Assistant`, {
transformResponse: transformTAResponse
});
- const taUsers = taRoleUsersResponse.data;
+ let taUsers = taRoleUsersResponse.data;
+
+ // Making a GET request to fetch users with the "Student" role
+ const studentRoleUsersResponse = await axiosClient.get(`/users/role/Student`, {
+ transformResponse: transformTAResponse
+ });
+ let studentUsers = studentRoleUsersResponse.data;
+ for(let i=0; i