From d9a8b099d7070e2833e1817d4666d0a1445537ec Mon Sep 17 00:00:00 2001 From: Peter Harrison <16875803+palisadoes@users.noreply.github.com> Date: Sun, 27 Oct 2024 15:50:40 -0400 Subject: [PATCH 1/9] Update pull-request.yml --- .github/workflows/pull-request.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 75588cc4dfc..2927f97672a 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -111,6 +111,14 @@ jobs: LICENSE setup.ts .coderabbit.yaml + CODE_OF_CONDUCT.md + CODE_STYLE.md + CONTRIBUTING.md + DOCUMENTATION.md + INSTALLATION.md + ISSUE_GUIDELINES.md + PR_GUIDELINES.md + README.md - name: List all changed unauthorized files if: steps.changed-unauth-files.outputs.any_changed == 'true' || steps.changed-unauth-files.outputs.any_deleted == 'true' From 2935df17bbcc8fbf141c1c38c4762cc1e1c75b9e Mon Sep 17 00:00:00 2001 From: Peter Harrison <16875803+palisadoes@users.noreply.github.com> Date: Sun, 27 Oct 2024 21:30:27 +0100 Subject: [PATCH 2/9] Update pull-request.yml --- .github/workflows/pull-request.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 2927f97672a..633757c9f2b 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -79,9 +79,9 @@ jobs: echo "Error: Source and Target Branches are the same. Please ensure they are different." exit 1 - Check-Unauthorized-Changes: + Check-Sensitive-Files: if: ${{ github.actor != 'dependabot[bot]' }} - name: Checks if no unauthorized files are changed + name: Checks if sensitive files have been changed without authorization runs-on: ubuntu-latest steps: - name: Checkout code From 6f8408efe82f5f285d05f507dd87586988f63c5e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Oct 2024 06:31:57 +0100 Subject: [PATCH 3/9] chore(deps): bump web-vitals from 4.2.3 to 4.2.4 (#2377) Bumps [web-vitals](https://github.com/GoogleChrome/web-vitals) from 4.2.3 to 4.2.4. - [Changelog](https://github.com/GoogleChrome/web-vitals/blob/main/CHANGELOG.md) - [Commits](https://github.com/GoogleChrome/web-vitals/compare/v4.2.3...v4.2.4) --- updated-dependencies: - dependency-name: web-vitals dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8a44446d6e8..51e3477b5d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,7 +65,7 @@ "vite": "^5.4.8", "vite-plugin-environment": "^1.1.3", "vite-tsconfig-paths": "^5.0.1", - "web-vitals": "^4.2.3" + "web-vitals": "^4.2.4" }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", @@ -17609,9 +17609,9 @@ } }, "node_modules/web-vitals": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.3.tgz", - "integrity": "sha512-/CFAm1mNxSmOj6i0Co+iGFJ58OS4NRGVP+AWS/l509uIK5a1bSoIVaHz/ZumpHTfHSZBpgrJ+wjfpAOrTHok5Q==" + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==" }, "node_modules/webidl-conversions": { "version": "6.1.0", diff --git a/package.json b/package.json index 5473cf84af2..70d28f4e00c 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "vite": "^5.4.8", "vite-plugin-environment": "^1.1.3", "vite-tsconfig-paths": "^5.0.1", - "web-vitals": "^4.2.3" + "web-vitals": "^4.2.4" }, "scripts": { "serve": "cross-env ESLINT_NO_DEV_ERRORS=true vite --config config/vite.config.ts", From 499dcf78e482cc3bb7154be96f0df175f39173cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Oct 2024 06:44:00 +0100 Subject: [PATCH 4/9] chore(deps): bump @mui/x-data-grid from 7.16.0 to 7.22.0 (#2378) Bumps [@mui/x-data-grid](https://github.com/mui/mui-x/tree/HEAD/packages/x-data-grid) from 7.16.0 to 7.22.0. - [Release notes](https://github.com/mui/mui-x/releases) - [Changelog](https://github.com/mui/mui-x/blob/v7.22.0/CHANGELOG.md) - [Commits](https://github.com/mui/mui-x/commits/v7.22.0/packages/x-data-grid) --- updated-dependencies: - dependency-name: "@mui/x-data-grid" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 30 +++++++++++++++--------------- package.json | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 51e3477b5d3..44ac22e2294 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@mui/private-theming": "^5.15.12", "@mui/system": "^5.14.12", "@mui/x-charts": "^7.17.0", - "@mui/x-data-grid": "^7.16.0", + "@mui/x-data-grid": "^7.22.0", "@mui/x-date-pickers": "^7.11.1", "@pdfme/generator": "^4.5.2", "@reduxjs/toolkit": "^2.3.0", @@ -2139,9 +2139,9 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", - "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -3797,13 +3797,13 @@ } }, "node_modules/@mui/x-data-grid": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.16.0.tgz", - "integrity": "sha512-71ZyffTeF8RPa399UkMlUbQ8T70kOrUK3fBXfinnal4mwgISlKwBN8EHNZZhyxSQ4vpWs3wHrHZ6MGQeXNUhJQ==", + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.22.0.tgz", + "integrity": "sha512-gXl7+hG0YRNU3YODlPvz6Q/9+EeUsPAWn/u2YMQmYTgwAxeY5QE3lY224VRnwM5v9SfTFheo1kzAKmXPdjb9tQ==", "dependencies": { - "@babel/runtime": "^7.25.6", - "@mui/utils": "^5.16.6", - "@mui/x-internals": "7.16.0", + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0", + "@mui/x-internals": "7.21.0", "clsx": "^2.1.1", "prop-types": "^15.8.1", "reselect": "^5.1.1" @@ -3898,12 +3898,12 @@ } }, "node_modules/@mui/x-internals": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.16.0.tgz", - "integrity": "sha512-ijer5XYmWlJqWaTmF6TGH1odG7EAupv8iDWYmDm2yVR9IQ+L2nQSuhiFClI+wmGx40KS2VKOlzDMPpF0t7/HCg==", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.21.0.tgz", + "integrity": "sha512-94YNyZ0BhK5Z+Tkr90RKf47IVCW8R/1MvdUhh6MCQg6sZa74jsX+x+gEZ4kzuCqOsuyTyxikeQ8vVuCIQiP7UQ==", "dependencies": { - "@babel/runtime": "^7.25.6", - "@mui/utils": "^5.16.6" + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0" }, "engines": { "node": ">=14.0.0" diff --git a/package.json b/package.json index 70d28f4e00c..0d93e91febf 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@mui/private-theming": "^5.15.12", "@mui/system": "^5.14.12", "@mui/x-charts": "^7.17.0", - "@mui/x-data-grid": "^7.16.0", + "@mui/x-data-grid": "^7.22.0", "@mui/x-date-pickers": "^7.11.1", "@pdfme/generator": "^4.5.2", "@reduxjs/toolkit": "^2.3.0", From 52690f726be697938c1bed79f537b3556dc9d959 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Oct 2024 06:44:36 +0100 Subject: [PATCH 5/9] chore(deps): bump prettier from 3.3.2 to 3.3.3 (#2381) Bumps [prettier](https://github.com/prettier/prettier) from 3.3.2 to 3.3.3. - [Release notes](https://github.com/prettier/prettier/releases) - [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/prettier/compare/3.3.2...3.3.3) --- updated-dependencies: - dependency-name: prettier dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 44ac22e2294..4669c8cb49e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,7 +41,7 @@ "inquirer": "^8.0.0", "js-cookie": "^3.0.1", "markdown-toc": "^1.2.0", - "prettier": "^3.3.2", + "prettier": "^3.3.3", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", "react-bootstrap": "^2.10.5", @@ -14329,9 +14329,9 @@ } }, "node_modules/prettier": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", - "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "bin": { "prettier": "bin/prettier.cjs" }, diff --git a/package.json b/package.json index 0d93e91febf..b8bf704a923 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "inquirer": "^8.0.0", "js-cookie": "^3.0.1", "markdown-toc": "^1.2.0", - "prettier": "^3.3.2", + "prettier": "^3.3.3", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", "react-bootstrap": "^2.10.5", From 3a112ecbafef8ebcd63dc526fc303cef149bc114 Mon Sep 17 00:00:00 2001 From: Peter Harrison <16875803+palisadoes@users.noreply.github.com> Date: Tue, 29 Oct 2024 06:46:05 +0100 Subject: [PATCH 6/9] Update dependabot.yaml --- .github/dependabot.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 2fc49726ff4..9a3e9a54e9e 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -7,7 +7,7 @@ updates: directory: "/" # Schedule automated updates to run weekly schedule: - interval: "weekly" + interval: "monthly" # Labels to apply to Dependabot PRs labels: - "dependencies" @@ -15,4 +15,4 @@ updates: target-branch: "develop" # Customize commit message prefix commit-message: - prefix: "chore(deps):" \ No newline at end of file + prefix: "chore(deps):" From ffdaea23e0f2ff3cec6c6b06dc035cbed2f3f510 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Oct 2024 06:51:56 +0100 Subject: [PATCH 7/9] chore(deps): bump @typescript-eslint/eslint-plugin from 8.8.1 to 8.11.0 (#2379) Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 8.8.1 to 8.11.0. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.11.0/packages/eslint-plugin) --- updated-dependencies: - dependency-name: "@typescript-eslint/eslint-plugin" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 72 +++++++++++++++++++++++------------------------ package.json | 2 +- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4669c8cb49e..f358aabd626 100644 --- a/package-lock.json +++ b/package-lock.json @@ -88,7 +88,7 @@ "@types/react-google-recaptcha": "^2.1.9", "@types/react-router-dom": "^5.1.8", "@types/sanitize-html": "^2.13.0", - "@typescript-eslint/eslint-plugin": "^8.8.1", + "@typescript-eslint/eslint-plugin": "^8.11.0", "@typescript-eslint/parser": "^8.5.0", "babel-jest": "^29.7.0", "cross-env": "^7.0.3", @@ -5111,16 +5111,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.1.tgz", - "integrity": "sha512-xfvdgA8AP/vxHgtgU310+WBnLB4uJQ9XdyP17RebG26rLtDrQJV3ZYrcopX91GrHmMoH8bdSwMRh2a//TiJ1jQ==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.11.0.tgz", + "integrity": "sha512-KhGn2LjW1PJT2A/GfDpiyOfS4a8xHQv2myUagTM5+zsormOmBlYsnQ6pobJ8XxJmh6hnHwa2Mbe3fPrDJoDhbA==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.8.1", - "@typescript-eslint/type-utils": "8.8.1", - "@typescript-eslint/utils": "8.8.1", - "@typescript-eslint/visitor-keys": "8.8.1", + "@typescript-eslint/scope-manager": "8.11.0", + "@typescript-eslint/type-utils": "8.11.0", + "@typescript-eslint/utils": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -5278,13 +5278,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.1.tgz", - "integrity": "sha512-X4JdU+66Mazev/J0gfXlcC/dV6JI37h+93W9BRYXrSn0hrE64IoWgVkO9MSJgEzoWkxONgaQpICWg8vAN74wlA==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.11.0.tgz", + "integrity": "sha512-Uholz7tWhXmA4r6epo+vaeV7yjdKy5QFCERMjs1kMVsLRKIrSdM6o21W2He9ftp5PP6aWOVpD5zvrvuHZC0bMQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.8.1", - "@typescript-eslint/visitor-keys": "8.8.1" + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5295,13 +5295,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.1.tgz", - "integrity": "sha512-qSVnpcbLP8CALORf0za+vjLYj1Wp8HSoiI8zYU5tHxRVj30702Z1Yw4cLwfNKhTPWp5+P+k1pjmD5Zd1nhxiZA==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.11.0.tgz", + "integrity": "sha512-ItiMfJS6pQU0NIKAaybBKkuVzo6IdnAhPFZA/2Mba/uBjuPQPet/8+zh5GtLHwmuFRShZx+8lhIs7/QeDHflOg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "8.8.1", - "@typescript-eslint/utils": "8.8.1", + "@typescript-eslint/typescript-estree": "8.11.0", + "@typescript-eslint/utils": "8.11.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -5319,9 +5319,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.1.tgz", - "integrity": "sha512-WCcTP4SDXzMd23N27u66zTKMuEevH4uzU8C9jf0RO4E04yVHgQgW+r+TeVTNnO1KIfrL8ebgVVYYMMO3+jC55Q==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.11.0.tgz", + "integrity": "sha512-tn6sNMHf6EBAYMvmPUaKaVeYvhUsrE6x+bXQTxjQRp360h1giATU0WvgeEys1spbvb5R+VpNOZ+XJmjD8wOUHw==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5332,13 +5332,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.1.tgz", - "integrity": "sha512-A5d1R9p+X+1js4JogdNilDuuq+EHZdsH9MjTVxXOdVFfTJXunKJR/v+fNNyO4TnoOn5HqobzfRlc70NC6HTcdg==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.11.0.tgz", + "integrity": "sha512-yHC3s1z1RCHoCz5t06gf7jH24rr3vns08XXhfEqzYpd6Hll3z/3g23JRi0jM8A47UFKNc3u/y5KIMx8Ynbjohg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.8.1", - "@typescript-eslint/visitor-keys": "8.8.1", + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -5384,15 +5384,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.1.tgz", - "integrity": "sha512-/QkNJDbV0bdL7H7d0/y0qBbV2HTtf0TIyjSDTvvmQEzeVx8jEImEbLuOA4EsvE8gIgqMitns0ifb5uQhMj8d9w==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.11.0.tgz", + "integrity": "sha512-CYiX6WZcbXNJV7UNB4PLDIBtSdRmRI/nb0FMyqHPTQD1rMjA0foPLaPUV39C/MxkTd/QKSeX+Gb34PPsDVC35g==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.8.1", - "@typescript-eslint/types": "8.8.1", - "@typescript-eslint/typescript-estree": "8.8.1" + "@typescript-eslint/scope-manager": "8.11.0", + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/typescript-estree": "8.11.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5406,12 +5406,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.1.tgz", - "integrity": "sha512-0/TdC3aeRAsW7MDvYRwEc1Uwm0TIBfzjPFgg60UU2Haj5qsCs9cc3zNgY71edqE3LbWfF/WoZQd3lJoDXFQpag==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.11.0.tgz", + "integrity": "sha512-EaewX6lxSjRJnc+99+dqzTeoDZUfyrA52d2/HRrkI830kgovWsmIiTfmr0NZorzqic7ga+1bS60lRBUgR3n/Bw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/types": "8.11.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { diff --git a/package.json b/package.json index b8bf704a923..e9f3b8feb04 100644 --- a/package.json +++ b/package.json @@ -122,7 +122,7 @@ "@types/react-google-recaptcha": "^2.1.9", "@types/react-router-dom": "^5.1.8", "@types/sanitize-html": "^2.13.0", - "@typescript-eslint/eslint-plugin": "^8.8.1", + "@typescript-eslint/eslint-plugin": "^8.11.0", "@typescript-eslint/parser": "^8.5.0", "babel-jest": "^29.7.0", "cross-env": "^7.0.3", From 6fadd0cd39da59cc19cab4cd836832a52c3bf8dd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Oct 2024 06:52:17 +0100 Subject: [PATCH 8/9] chore(deps): bump typedoc from 0.26.7 to 0.26.10 (#2380) Bumps [typedoc](https://github.com/TypeStrong/TypeDoc) from 0.26.7 to 0.26.10. - [Release notes](https://github.com/TypeStrong/TypeDoc/releases) - [Changelog](https://github.com/TypeStrong/typedoc/blob/master/CHANGELOG.md) - [Commits](https://github.com/TypeStrong/TypeDoc/compare/v0.26.7...v0.26.10) --- updated-dependencies: - dependency-name: typedoc dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index f358aabd626..31efdaed899 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,7 +59,7 @@ "redux": "^5.0.1", "redux-thunk": "^3.1.0", "sanitize-html": "^2.13.0", - "typedoc": "^0.26.7", + "typedoc": "^0.26.10", "typedoc-plugin-markdown": "^4.2.1", "typescript": "^5.6.3", "vite": "^5.4.8", @@ -16725,9 +16725,9 @@ } }, "node_modules/typedoc": { - "version": "0.26.7", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.26.7.tgz", - "integrity": "sha512-gUeI/Wk99vjXXMi8kanwzyhmeFEGv1LTdTQsiyIsmSYsBebvFxhbcyAx7Zjo4cMbpLGxM4Uz3jVIjksu/I2v6Q==", + "version": "0.26.10", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.26.10.tgz", + "integrity": "sha512-xLmVKJ8S21t+JeuQLNueebEuTVphx6IrP06CdV7+0WVflUSW3SPmR+h1fnWVdAR/FQePEgsSWCUHXqKKjzuUAw==", "dependencies": { "lunr": "^2.3.9", "markdown-it": "^14.1.0", diff --git a/package.json b/package.json index e9f3b8feb04..8ef05d105f2 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "redux": "^5.0.1", "redux-thunk": "^3.1.0", "sanitize-html": "^2.13.0", - "typedoc": "^0.26.7", + "typedoc": "^0.26.10", "typedoc-plugin-markdown": "^4.2.1", "typescript": "^5.6.3", "vite": "^5.4.8", From af2f509368ad3f5054a311cb355ce81401d9d509 Mon Sep 17 00:00:00 2001 From: Meetul Rathore Date: Wed, 30 Oct 2024 14:47:41 +0530 Subject: [PATCH 9/9] feat: Add functionalities for bulk tag operations (GSoC) (#2362) * add people to tag functaionality * minor change * more translations * minor change * add a variable for page size * add tag actions * add tests * translations * add subtags infinite scroll * minor correction * exclude ManageTag from countline check * fix linting * fix linting * fix linting * make coderabbit suggested changes * more changes * more changes * minor correction * add error component for tagNode subtags query * fix translation * fix translation --- .github/workflows/pull-request.yml | 2 +- public/locales/en/translation.json | 24 +- public/locales/fr/translation.json | 24 +- public/locales/hi/translation.json | 24 +- public/locales/sp/translation.json | 24 +- public/locales/zh/translation.json | 24 +- src/GraphQl/Mutations/TagMutations.ts | 34 ++ .../AddPeopleToTag/AddPeopleToTag.tsx | 2 +- .../TagActions/TagActions.module.css | 174 ++++++ src/components/TagActions/TagActions.test.tsx | 320 +++++++++++ src/components/TagActions/TagActions.tsx | 414 ++++++++++++++ src/components/TagActions/TagActionsMocks.ts | 533 ++++++++++++++++++ src/components/TagActions/TagNode.tsx | 219 +++++++ src/screens/ManageTag/ManageTag.test.tsx | 145 ++++- src/screens/ManageTag/ManageTag.tsx | 239 +++++++- src/screens/ManageTag/ManageTagMocks.ts | 197 ++++++- src/utils/interfaces.ts | 35 +- src/utils/organizationTagsUtils.ts | 3 + 18 files changed, 2396 insertions(+), 41 deletions(-) create mode 100644 src/components/TagActions/TagActions.module.css create mode 100644 src/components/TagActions/TagActions.test.tsx create mode 100644 src/components/TagActions/TagActions.tsx create mode 100644 src/components/TagActions/TagActionsMocks.ts create mode 100644 src/components/TagActions/TagNode.tsx diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 633757c9f2b..ac079715a1c 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -38,7 +38,7 @@ jobs: - name: Count number of lines run: | chmod +x ./.github/workflows/countline.py - ./.github/workflows/countline.py --lines 600 --exclude_files src/screens/LoginPage/LoginPage.tsx src/GraphQl/Queries/Queries.ts src/screens/OrgList/OrgList.tsx src/GraphQl/Mutations/mutations.ts src/components/EventListCard/EventListCardModals.tsx + ./.github/workflows/countline.py --lines 600 --exclude_files src/screens/LoginPage/LoginPage.tsx src/GraphQl/Queries/Queries.ts src/screens/OrgList/OrgList.tsx src/GraphQl/Mutations/mutations.ts src/components/EventListCard/EventListCardModals.tsx src/screens/ManageTag/ManageTag.tsx - name: Get changed TypeScript files id: changed-files diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 83bb999138c..7d2108bc063 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -335,11 +335,31 @@ "subTags": "Sub Tags", "assignedToAll": "Tag Assigned to All", "successfullyAssignedToPeople": "Tag assigned successfully", - "assignPeople": "Assign", "errorOccurredWhileLoadingMembers": "Error occured while loading members", "userName": "User Name", "actions": "Actions", - "noOneSelected": "No One Selected" + "noOneSelected": "No One Selected", + "assignToTags": "Assign to Tags", + "removeFromTags": "Remove from Tags", + "assign": "Assign", + "remove": "Remove", + "successfullyAssignedToTags": "Successfully Assigned to Tags", + "successfullyRemovedFromTags": "Successfully Removed from Tags", + "errorOccurredWhileLoadingOrganizationUserTags": "Error occurred while loading organization tags", + "errorOccurredWhileLoadingSubTags": "Error occurred while loading subTags tags", + "removeUserTag": "Delete Tag", + "removeUserTagMessage": "Do you want to delete this tag? It delete all the sub tags and all the associations.", + "tagDetails": "Tag Details", + "tagName": "Name", + "tagUpdationSuccess": "Tag updated successfully", + "tagRemovalSuccess": "Tag deleted successfully", + "noTagSelected": "No Tag Selected", + "changeNameToEdit": "Change the name to make an update", + "selectTag": "Select Tag", + "collapse": "Collapse", + "expand": "Expand", + "tagNamePlaceholder": "Write the name of the tag", + "allTags": "All Tags" }, "userListCard": { "addAdmin": "Add Admin", diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json index cd560dcda6a..7f748262729 100644 --- a/public/locales/fr/translation.json +++ b/public/locales/fr/translation.json @@ -335,11 +335,31 @@ "subTags": "Sous-étiquettes", "assignedToAll": "Étiquette attribuée à tous", "successfullyAssignedToPeople": "Étiquette attribuée avec succès", - "assignPeople": "Attribuer", "errorOccurredWhileLoadingMembers": "Erreur survenue lors du chargement des membres", "userName": "Nom d'utilisateur", "actions": "Actions", - "noOneSelected": "Personne sélectionnée" + "noOneSelected": "Personne sélectionnée", + "assignToTags": "Attribuer aux étiquettes", + "removeFromTags": "Retirer des étiquettes", + "assign": "Attribuer", + "remove": "Retirer", + "successfullyAssignedToTags": "Attribué aux étiquettes avec succès", + "successfullyRemovedFromTags": "Retiré des étiquettes avec succès", + "errorOccurredWhileLoadingOrganizationUserTags": "Erreur lors du chargement des étiquettes de l'organisation", + "errorOccurredWhileLoadingSubTags": "Une erreur s'est produite lors du chargement des sous-étiquettes", + "removeUserTag": "Supprimer l'étiquette", + "removeUserTagMessage": "Voulez-vous supprimer cette étiquette ? Cela supprimera toutes les sous-étiquettes et toutes les associations.", + "tagDetails": "Détails de l'étiquette", + "tagName": "Nom de l'étiquette", + "tagUpdationSuccess": "Étiquette mise à jour avec succès", + "tagRemovalSuccess": "Étiquette supprimée avec succès", + "noTagSelected": "Aucune étiquette sélectionnée", + "changeNameToEdit": "Modifiez le nom pour faire une mise à jour", + "selectTag": "Sélectionner l'étiquette", + "collapse": "Réduire", + "expand": "Développer", + "tagNamePlaceholder": "Écrire le nom de l'étiquette", + "allTags": "Toutes les étiquettes" }, "userListCard": { "addAdmin": "Ajouter un administrateur", diff --git a/public/locales/hi/translation.json b/public/locales/hi/translation.json index 58d0aa5e57c..4384648ca3a 100644 --- a/public/locales/hi/translation.json +++ b/public/locales/hi/translation.json @@ -335,11 +335,31 @@ "subTags": "उप-टैग्स", "assignedToAll": "सभी को टैग असाइन किया गया", "successfullyAssignedToPeople": "टैग सफलतापूर्वक असाइन किया गया", - "assignPeople": "असाइन करें", "errorOccurredWhileLoadingMembers": "सदस्यों को लोड करते समय त्रुटि हुई", "userName": "उपयोगकर्ता नाम", "actions": "क्रियाएँ", - "noOneSelected": "कोई चयनित नहीं" + "noOneSelected": "कोई चयनित नहीं", + "assignToTags": "टैग्स को असाइन करें", + "removeFromTags": "टैग्स से हटाएं", + "assign": "असाइन करें", + "remove": "हटाएं", + "successfullyAssignedToTags": "सफलतापूर्वक टैग्स को असाइन किया गया", + "successfullyRemovedFromTags": "सफलतापूर्वक टैग्स से हटाया गया", + "errorOccurredWhileLoadingOrganizationUserTags": "संगठन टैग्स को लोड करते समय त्रुटि हुई", + "errorOccurredWhileLoadingSubTags": "उप-टैग लोड करते समय त्रुटि हुई", + "removeUserTag": "टैग हटाएं", + "removeUserTagMessage": "क्या आप इस टैग को हटाना चाहते हैं? यह सभी उप-टैग्स और सभी संबंधों को हटा देगा।", + "tagDetails": "टैग विवरण", + "tagName": "नाम", + "tagUpdationSuccess": "टैग सफलतापूर्वक अपडेट की गई", + "tagRemovalSuccess": "टैग सफलतापूर्वक हटाई गई", + "noTagSelected": "कोई टैग चयनित नहीं", + "changeNameToEdit": "अपडेट करने के लिए नाम बदलें", + "selectTag": "टैग चुनें", + "collapse": "संक्षिप्त करें", + "expand": "विस्तारित करें", + "tagNamePlaceholder": "टैग का नाम लिखें", + "allTags": "सभी टैग" }, "userListCard": { "addAdmin": "व्यवस्थापक जोड़ें", diff --git a/public/locales/sp/translation.json b/public/locales/sp/translation.json index 48c8f9940d8..24ce0dbdec4 100644 --- a/public/locales/sp/translation.json +++ b/public/locales/sp/translation.json @@ -335,11 +335,31 @@ "subTags": "Subetiquetas", "assignedToAll": "Etiqueta asignada a todos", "successfullyAssignedToPeople": "Etiqueta asignada con éxito", - "assignPeople": "Asignar", "errorOccurredWhileLoadingMembers": "Error al cargar los miembros", "userName": "Nombre de usuario", "actions": "Acciones", - "noOneSelected": "Nadie seleccionado" + "noOneSelected": "Nadie seleccionado", + "assignToTags": "Asignar a etiquetas", + "removeFromTags": "Eliminar de etiquetas", + "assign": "Asignar", + "remove": "Eliminar", + "successfullyAssignedToTags": "Asignado a etiquetas con éxito", + "successfullyRemovedFromTags": "Eliminado de etiquetas con éxito", + "errorOccurredWhileLoadingOrganizationUserTags": "Error al cargar las etiquetas de la organización", + "errorOccurredWhileLoadingSubTags": "Ocurrió un error al cargar las subetiquetas", + "removeUserTag": "Eliminar etiqueta", + "removeUserTagMessage": "¿Desea eliminar esta etiqueta? Esto eliminará todas las subetiquetas y todas las asociaciones.", + "tagDetails": "Detalles de la etiqueta", + "tagName": "Nombre", + "tagUpdationSuccess": "Etiqueta actualizada con éxito", + "tagRemovalSuccess": "Etiqueta eliminada con éxito", + "noTagSelected": "Ninguna etiqueta seleccionada", + "changeNameToEdit": "Cambia el nombre para hacer una actualización", + "selectTag": "Seleccionar etiqueta", + "collapse": "Colapsar", + "expand": "Expandir", + "tagNamePlaceholder": "Escribe el nombre de la etiqueta", + "allTags": "Todas las etiquetas" }, "userListCard": { "joined": "Unido", diff --git a/public/locales/zh/translation.json b/public/locales/zh/translation.json index 9fb3965fd6b..0c070bcf8bd 100644 --- a/public/locales/zh/translation.json +++ b/public/locales/zh/translation.json @@ -335,11 +335,31 @@ "subTags": "子标签", "assignedToAll": "标签分配给所有人", "successfullyAssignedToPeople": "标签分配成功", - "assignPeople": "分配", "errorOccurredWhileLoadingMembers": "加载成员时出错", "userName": "用户名", "actions": "操作", - "noOneSelected": "未选择任何人" + "noOneSelected": "未选择任何人", + "assignToTags": "分配到标签", + "removeFromTags": "从标签中移除", + "assign": "分配", + "remove": "移除", + "successfullyAssignedToTags": "成功分配到标签", + "successfullyRemovedFromTags": "成功从标签中移除", + "errorOccurredWhileLoadingOrganizationUserTags": "加载组织标签时出错", + "errorOccurredWhileLoadingSubTags": "加载子标签时发生错误", + "removeUserTag": "删除标签", + "removeUserTagMessage": "您要删除此标签吗?这将删除所有子标签和所有关联。", + "tagDetails": "标签详情", + "tagName": "名称", + "tagUpdationSuccess": "标签更新成功", + "tagRemovalSuccess": "标签删除成功", + "noTagSelected": "未选择标签", + "changeNameToEdit": "更改名称以进行更新", + "selectTag": "选择标签", + "collapse": "收起", + "expand": "展开", + "tagNamePlaceholder": "输入标签名称", + "allTags": "所有标签" }, "userListCard": { "addAdmin": "添加管理员", diff --git a/src/GraphQl/Mutations/TagMutations.ts b/src/GraphQl/Mutations/TagMutations.ts index d97fefc246d..9f8ed1ec611 100644 --- a/src/GraphQl/Mutations/TagMutations.ts +++ b/src/GraphQl/Mutations/TagMutations.ts @@ -87,3 +87,37 @@ export const ADD_PEOPLE_TO_TAG = gql` } } `; + +/** + * GraphQL mutation to assign people to multiple tags. + * + * @param currentTagId - Id of the current tag. + * @param selectedTagIds - Ids of the selected tags to be assined. + */ + +export const ASSIGN_TO_TAGS = gql` + mutation AssignToUserTags($currentTagId: ID!, $selectedTagIds: [ID!]!) { + assignToUserTags( + input: { currentTagId: $currentTagId, selectedTagIds: $selectedTagIds } + ) { + _id + } + } +`; + +/** + * GraphQL mutation to remove people from multiple tags. + * + * @param currentTagId - Id of the current tag. + * @param selectedTagIds - Ids of the selected tags to be removed from. + */ + +export const REMOVE_FROM_TAGS = gql` + mutation RemoveFromUserTags($currentTagId: ID!, $selectedTagIds: [ID!]!) { + removeFromUserTags( + input: { currentTagId: $currentTagId, selectedTagIds: $selectedTagIds } + ) { + _id + } + } +`; diff --git a/src/components/AddPeopleToTag/AddPeopleToTag.tsx b/src/components/AddPeopleToTag/AddPeopleToTag.tsx index 73066d2f0f3..a43a47b006d 100644 --- a/src/components/AddPeopleToTag/AddPeopleToTag.tsx +++ b/src/components/AddPeopleToTag/AddPeopleToTag.tsx @@ -365,7 +365,7 @@ const AddPeopleToTag: React.FC = ({ variant="primary" data-testid="assignPeopleBtn" > - {t('assignPeople')} + {t('assign')} diff --git a/src/components/TagActions/TagActions.module.css b/src/components/TagActions/TagActions.module.css new file mode 100644 index 00000000000..e667adb96e6 --- /dev/null +++ b/src/components/TagActions/TagActions.module.css @@ -0,0 +1,174 @@ +.btnsContainer { + display: flex; + margin: 2rem 0; +} + +.btnsContainer .btnsBlock { + display: flex; + width: max-content; +} + +.btnsContainer .btnsBlock button { + margin-left: 1rem; + display: flex; + justify-content: center; + align-items: center; +} + +.btnsContainer .input { + flex: 1; + position: relative; + max-width: 60%; + justify-content: space-between; +} + +.btnsContainer input { + outline: 1px solid var(--bs-gray-400); +} + +.btnsContainer .input button { + width: 52px; +} + +@media (max-width: 1020px) { + .btnsContainer { + flex-direction: column; + margin: 1.5rem 0; + } + + .btnsContainer .btnsBlock { + margin: 1.5rem 0 0 0; + justify-content: space-between; + } + + .btnsContainer .btnsBlock button { + margin: 0; + } + + .btnsContainer .btnsBlock div button { + margin-right: 1.5rem; + } +} + +/* For mobile devices */ + +@media (max-width: 520px) { + .btnsContainer { + margin-bottom: 0; + } + + .btnsContainer .btnsBlock { + display: block; + margin-top: 1rem; + margin-right: 0; + } + + .btnsContainer .btnsBlock div { + flex: 1; + } + + .btnsContainer .btnsBlock div[title='Sort organizations'] { + margin-right: 0.5rem; + } + + .btnsContainer .btnsBlock button { + margin-bottom: 1rem; + margin-right: 0; + width: 100%; + } +} + +.errorContainer { + min-height: 100vh; +} + +.errorMessage { + margin-top: 25%; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +} + +.errorIcon { + transform: scale(1.5); + color: var(--bs-danger); + margin-bottom: 1rem; +} + +.tableHeader { + background-color: var(--bs-primary); + color: var(--bs-white); + font-size: 1rem; +} + +.rowBackground { + background-color: var(--bs-white); + max-height: 120px; +} + +.tagsBreadCrumbs { + color: var(--bs-gray); + cursor: pointer; +} + +.tagsBreadCrumbs:hover { + color: var(--bs-blue); + font-weight: 600; + text-decoration: underline; +} + +.scrollContainer { + max-height: 100px; /* Adjust as needed */ + overflow-y: auto; + margin-bottom: 1rem; +} + +.tagBadge { + display: flex; + align-items: center; + padding: 5px 10px; + border-radius: 12px; + background-color: #f8f9fa; /* Light background */ + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + max-width: calc(100% - 30px); /* Ensure it fits within the container */ +} + +.removeFilterIcon { + cursor: pointer; +} + +.scrContainer { + max-height: 300px; + overflow: scroll; + /* padding-right: 8px; */ +} + +.allTagsHeading { + color: rgb(77, 76, 76); + font-weight: 600; +} + +/* SimpleLoader.css */ +.simpleLoader { + display: flex; + justify-content: start; + align-items: center; + width: 100%; + height: 100%; +} + +.spinner { + width: 40px; + height: 40px; + border: 4px solid var(--bs-gray); + border-top-color: var(--bs-gray); + border-radius: 50%; + animation: spin 0.6s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} diff --git a/src/components/TagActions/TagActions.test.tsx b/src/components/TagActions/TagActions.test.tsx new file mode 100644 index 00000000000..39e287395bb --- /dev/null +++ b/src/components/TagActions/TagActions.test.tsx @@ -0,0 +1,320 @@ +import React from 'react'; +import { MockedProvider } from '@apollo/react-testing'; +import type { RenderResult } from '@testing-library/react'; +import { + render, + screen, + fireEvent, + cleanup, + waitFor, + act, +} from '@testing-library/react'; +import { Provider } from 'react-redux'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import 'jest-location-mock'; +import { I18nextProvider } from 'react-i18next'; + +import { store } from 'state/store'; +import userEvent from '@testing-library/user-event'; +import { StaticMockLink } from 'utils/StaticMockLink'; +import { toast } from 'react-toastify'; +import type { ApolloLink } from '@apollo/client'; +import type { InterfaceTagActionsProps } from './TagActions'; +import TagActions from './TagActions'; +import i18n from 'utils/i18nForTest'; +import { + MOCKS, + MOCKS_ERROR_ORGANIZATION_TAGS_QUERY, + MOCKS_ERROR_SUBTAGS_QUERY, +} from './TagActionsMocks'; + +const link = new StaticMockLink(MOCKS, true); +const link2 = new StaticMockLink(MOCKS_ERROR_ORGANIZATION_TAGS_QUERY, true); +const link3 = new StaticMockLink(MOCKS_ERROR_SUBTAGS_QUERY, true); + +async function wait(ms = 500): Promise { + await act(() => { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + }); +} + +jest.mock('react-toastify', () => ({ + toast: { + success: jest.fn(), + error: jest.fn(), + }, +})); + +const translations = { + ...JSON.parse( + JSON.stringify(i18n.getDataByLanguage('en')?.translation.manageTag ?? {}), + ), + ...JSON.parse(JSON.stringify(i18n.getDataByLanguage('en')?.common ?? {})), + ...JSON.parse(JSON.stringify(i18n.getDataByLanguage('en')?.errors ?? {})), +}; + +const props: InterfaceTagActionsProps[] = [ + { + assignToTagsModalIsOpen: true, + hideAssignToTagsModal: () => {}, + tagActionType: 'assignToTags', + t: (key: string) => translations[key], + tCommon: (key: string) => translations[key], + }, + { + assignToTagsModalIsOpen: true, + hideAssignToTagsModal: () => {}, + tagActionType: 'removeFromTags', + t: (key: string) => translations[key], + tCommon: (key: string) => translations[key], + }, +]; + +const renderTagActionsModal = ( + props: InterfaceTagActionsProps, + link: ApolloLink, +): RenderResult => { + return render( + + + + + + } + /> + + + + + , + ); +}; + +describe('Organisation Tags Page', () => { + beforeEach(() => { + jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ orgId: 'orgId' }), + })); + }); + + afterEach(() => { + jest.clearAllMocks(); + cleanup(); + }); + + test('Component loads correctly and opens assignToTags modal', async () => { + const { getByText } = renderTagActionsModal(props[0], link); + + await wait(); + + await waitFor(() => { + expect(getByText(translations.assign)).toBeInTheDocument(); + }); + }); + + test('Component loads correctly and opens removeFromTags modal', async () => { + const { getByText } = renderTagActionsModal(props[1], link); + + await wait(); + + await waitFor(() => { + expect(getByText(translations.remove)).toBeInTheDocument(); + }); + }); + + test('Renders error component when when query is unsuccessful', async () => { + const { queryByText } = renderTagActionsModal(props[0], link2); + + await wait(); + + await waitFor(() => { + expect(queryByText(translations.assign)).not.toBeInTheDocument(); + }); + }); + + test('Renders error component when when subTags query is unsuccessful', async () => { + const { getByText } = renderTagActionsModal(props[0], link3); + + await wait(); + + // expand tag 1 to list its subtags + await waitFor(() => { + expect(screen.getByTestId('expandSubTags1')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('expandSubTags1')); + + await waitFor(() => { + expect( + getByText(translations.errorOccurredWhileLoadingSubTags), + ).toBeInTheDocument(); + }); + }); + + test('Renders more members with infinite scroll', async () => { + const { getByText } = renderTagActionsModal(props[0], link); + + await wait(); + + await waitFor(() => { + expect(getByText(translations.assign)).toBeInTheDocument(); + }); + + // Find the infinite scroll div by test ID or another selector + const scrollableDiv = screen.getByTestId('scrollableDiv'); + + const initialTagsDataLength = screen.getAllByTestId('orgUserTag').length; + + // Set scroll position to the bottom + fireEvent.scroll(scrollableDiv, { + target: { scrollY: scrollableDiv.scrollHeight }, + }); + + await waitFor(() => { + const finalTagsDataLength = screen.getAllByTestId('orgUserTag').length; + expect(finalTagsDataLength).toBeGreaterThan(initialTagsDataLength); + + expect(getByText(translations.assign)).toBeInTheDocument(); + }); + }); + + test('Selects and deselects tags', async () => { + renderTagActionsModal(props[0], link); + + await wait(); + + await waitFor(() => { + expect(screen.getByTestId('checkTag1')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('checkTag1')); + + await waitFor(() => { + expect(screen.getByTestId('checkTag2')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('checkTag2')); + + await waitFor(() => { + expect(screen.getByTestId('checkTag1')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('checkTag1')); + + await waitFor(() => { + expect(screen.getByTestId('clearSelectedTag2')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('clearSelectedTag2')); + }); + + test('fetches and lists the child tags and then selects and deselects them', async () => { + renderTagActionsModal(props[0], link); + + await wait(); + + // expand tag 1 to list its subtags + await waitFor(() => { + expect(screen.getByTestId('expandSubTags1')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('expandSubTags1')); + + await waitFor(() => { + expect(screen.getByTestId('subTagsScrollableDiv1')).toBeInTheDocument(); + }); + // Find the infinite scroll div for subtags by test ID or another selector + const subTagsScrollableDiv1 = screen.getByTestId('subTagsScrollableDiv1'); + + const initialTagsDataLength = + screen.getAllByTestId('orgUserSubTags').length; + + // Set scroll position to the bottom + fireEvent.scroll(subTagsScrollableDiv1, { + target: { scrollY: subTagsScrollableDiv1.scrollHeight }, + }); + + await waitFor(() => { + const finalTagsDataLength = + screen.getAllByTestId('orgUserSubTags').length; + expect(finalTagsDataLength).toBeGreaterThan(initialTagsDataLength); + }); + + // select subtags 1 & 2 + await waitFor(() => { + expect(screen.getByTestId('checkTagsubTag1')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('checkTagsubTag1')); + + await waitFor(() => { + expect(screen.getByTestId('checkTagsubTag2')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('checkTagsubTag2')); + + await waitFor(() => { + expect(screen.getByTestId('checkTag1')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('checkTag1')); + + // deselect subtags 1 & 2 + await waitFor(() => { + expect(screen.getByTestId('checkTagsubTag1')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('checkTagsubTag1')); + + await waitFor(() => { + expect(screen.getByTestId('checkTagsubTag2')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('checkTagsubTag2')); + + // hide subtags of tag 1 + await waitFor(() => { + expect(screen.getByTestId('expandSubTags1')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('expandSubTags1')); + }); + + test('Successfully assigns to tags', async () => { + renderTagActionsModal(props[0], link); + + await wait(); + + // select userTags 2 & 3 and assign them + await waitFor(() => { + expect(screen.getByTestId('checkTag2')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('checkTag2')); + + await waitFor(() => { + expect(screen.getByTestId('checkTag3')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('checkTag3')); + + userEvent.click(screen.getByTestId('tagActionSubmitBtn')); + + await waitFor(() => { + expect(toast.success).toHaveBeenCalledWith( + translations.successfullyAssignedToTags, + ); + }); + }); + + test('Successfully removes from tags', async () => { + renderTagActionsModal(props[1], link); + + await wait(); + + // select userTag 2 and remove people from it + await waitFor(() => { + expect(screen.getByTestId('checkTag2')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('checkTag2')); + + userEvent.click(screen.getByTestId('tagActionSubmitBtn')); + + await waitFor(() => { + expect(toast.success).toHaveBeenCalledWith( + translations.successfullyRemovedFromTags, + ); + }); + }); +}); diff --git a/src/components/TagActions/TagActions.tsx b/src/components/TagActions/TagActions.tsx new file mode 100644 index 00000000000..0c3246f16ce --- /dev/null +++ b/src/components/TagActions/TagActions.tsx @@ -0,0 +1,414 @@ +import type { ApolloError } from '@apollo/client'; +import { useMutation, useQuery } from '@apollo/client'; +import Loader from 'components/Loader/Loader'; +import { USER_TAG_ANCESTORS } from 'GraphQl/Queries/userTagQueries'; +import type { FormEvent } from 'react'; +import React, { useEffect, useState } from 'react'; +import { Modal, Form, Button } from 'react-bootstrap'; +import { useParams } from 'react-router-dom'; +import type { + InterfaceQueryOrganizationUserTags, + InterfaceTagData, +} from 'utils/interfaces'; +import styles from './TagActions.module.css'; +import { ORGANIZATION_USER_TAGS_LIST } from 'GraphQl/Queries/OrganizationQueries'; +import { + ASSIGN_TO_TAGS, + REMOVE_FROM_TAGS, +} from 'GraphQl/Mutations/TagMutations'; +import { toast } from 'react-toastify'; +import type { TagActionType } from 'utils/organizationTagsUtils'; +import { TAGS_QUERY_LIMIT } from 'utils/organizationTagsUtils'; +import InfiniteScroll from 'react-infinite-scroll-component'; +import { WarningAmberRounded } from '@mui/icons-material'; +import TagNode from './TagNode'; + +interface InterfaceUserTagsAncestorData { + _id: string; + name: string; +} + +/** + * Props for the `AssignToTags` component. + */ +export interface InterfaceTagActionsProps { + assignToTagsModalIsOpen: boolean; + hideAssignToTagsModal: () => void; + tagActionType: TagActionType; + t: (key: string) => string; + tCommon: (key: string) => string; +} + +const TagActions: React.FC = ({ + assignToTagsModalIsOpen, + hideAssignToTagsModal, + tagActionType, + t, + tCommon, +}) => { + const { orgId, tagId: currentTagId } = useParams(); + + const { + data: orgUserTagsData, + loading: orgUserTagsLoading, + error: orgUserTagsError, + fetchMore: orgUserTagsFetchMore, + }: { + data?: { + organizations: InterfaceQueryOrganizationUserTags[]; + }; + loading: boolean; + error?: ApolloError; + refetch: () => void; + fetchMore: (options: { + variables: { + first: number; + after?: string; + }; + updateQuery: ( + previousResult: { organizations: InterfaceQueryOrganizationUserTags[] }, + options: { + fetchMoreResult?: { + organizations: InterfaceQueryOrganizationUserTags[]; + }; + }, + ) => { organizations: InterfaceQueryOrganizationUserTags[] }; + }) => void; + } = useQuery(ORGANIZATION_USER_TAGS_LIST, { + variables: { + id: orgId, + first: TAGS_QUERY_LIMIT, + }, + skip: !assignToTagsModalIsOpen, + }); + + const userTagsList = orgUserTagsData?.organizations[0]?.userTags.edges.map( + (edge) => edge.node, + ); + + const [checkedTagId, setCheckedTagId] = useState(null); + const [uncheckedTagId, setUncheckedTagId] = useState(null); + + // tags that we have selected to assigned + const [selectedTags, setSelectedTags] = useState([]); + + // tags that we have checked, it is there to differentiate between the selected tags and all the checked tags + // i.e. selected tags would only be the ones we select, but checked tags will also include the selected tag's ancestors + const [checkedTags, setCheckedTags] = useState>(new Set()); + + // next 3 states are there to keep track of the ancestor tags of the the tags that we have selected + // i.e. when we check a tag, all of it's ancestor tags will be checked too + // indicating that the users will be assigned all of the ancestor tags as well + const [addAncestorTagsData, setAddAncestorTagsData] = useState< + Set + >(new Set()); + const [removeAncestorTagsData, setRemoveAncestorTagsData] = useState< + Set + >(new Set()); + const [ancestorTagsDataMap, setAncestorTagsDataMap] = useState(new Map()); + + useEffect(() => { + const newCheckedTags = new Set(checkedTags); + const newAncestorTagsDataMap = new Map(ancestorTagsDataMap); + /* istanbul ignore next */ + addAncestorTagsData.forEach( + (ancestorTag: InterfaceUserTagsAncestorData) => { + const prevAncestorTagValue = ancestorTagsDataMap.get(ancestorTag._id); + newAncestorTagsDataMap.set( + ancestorTag._id, + prevAncestorTagValue ? prevAncestorTagValue + 1 : 1, + ); + newCheckedTags.add(ancestorTag._id); + }, + ); + + setCheckedTags(newCheckedTags); + setAncestorTagsDataMap(newAncestorTagsDataMap); + }, [addAncestorTagsData]); + + useEffect(() => { + const newCheckedTags = new Set(checkedTags); + const newAncestorTagsDataMap = new Map(ancestorTagsDataMap); + /* istanbul ignore next */ + removeAncestorTagsData.forEach( + (ancestorTag: InterfaceUserTagsAncestorData) => { + const prevAncestorTagValue = ancestorTagsDataMap.get(ancestorTag._id); + if (prevAncestorTagValue === 1) { + newCheckedTags.delete(ancestorTag._id); + newAncestorTagsDataMap.delete(ancestorTag._id); + } else { + newAncestorTagsDataMap.set(ancestorTag._id, prevAncestorTagValue - 1); + } + }, + ); + + setCheckedTags(newCheckedTags); + setAncestorTagsDataMap(newAncestorTagsDataMap); + }, [removeAncestorTagsData]); + + const addAncestorTags = (tagId: string): void => { + setCheckedTagId(tagId); + setUncheckedTagId(null); + }; + + const removeAncestorTags = (tagId: string): void => { + setUncheckedTagId(tagId); + setCheckedTagId(null); + }; + + const selectTag = (tag: InterfaceTagData): void => { + const newCheckedTags = new Set(checkedTags); + + setSelectedTags((selectedTags) => [...selectedTags, tag]); + newCheckedTags.add(tag._id); + addAncestorTags(tag._id); + + setCheckedTags(newCheckedTags); + }; + + const deSelectTag = (tag: InterfaceTagData): void => { + if (!selectedTags.some((selectedTag) => selectedTag._id === tag._id)) { + /* istanbul ignore next */ + return; + } + + const newCheckedTags = new Set(checkedTags); + + setSelectedTags( + selectedTags.filter((selectedTag) => selectedTag._id !== tag._id), + ); + newCheckedTags.delete(tag._id); + removeAncestorTags(tag._id); + + setCheckedTags(newCheckedTags); + }; + + const toggleTagSelection = ( + tag: InterfaceTagData, + isSelected: boolean, + ): void => { + if (isSelected) { + selectTag(tag); + } else { + deSelectTag(tag); + } + }; + + useQuery(USER_TAG_ANCESTORS, { + variables: { id: checkedTagId }, + onCompleted: /* istanbul ignore next */ (data) => { + setAddAncestorTagsData(data.getUserTagAncestors.slice(0, -1)); // Update the ancestor tags data, to check the ancestor tags + }, + }); + + useQuery(USER_TAG_ANCESTORS, { + variables: { id: uncheckedTagId }, + onCompleted: /* istanbul ignore next */ (data) => { + setRemoveAncestorTagsData(data.getUserTagAncestors.slice(0, -1)); // Update the ancestor tags data, to uncheck the ancestor tags + }, + }); + + const loadMoreUserTags = (): void => { + orgUserTagsFetchMore({ + variables: { + first: TAGS_QUERY_LIMIT, + after: orgUserTagsData?.organizations[0].userTags.pageInfo.endCursor, + }, + updateQuery: ( + prevResult: { organizations: InterfaceQueryOrganizationUserTags[] }, + { + fetchMoreResult, + }: { + fetchMoreResult?: { + organizations: InterfaceQueryOrganizationUserTags[]; + }; + }, + ) => { + if (!fetchMoreResult) return prevResult; + + return { + organizations: [ + { + ...prevResult.organizations[0], + userTags: { + ...prevResult.organizations[0].userTags, + edges: [ + ...prevResult.organizations[0].userTags.edges, + ...fetchMoreResult.organizations[0].userTags.edges, + ], + pageInfo: fetchMoreResult.organizations[0].userTags.pageInfo, + }, + }, + ], + }; + }, + }); + }; + + const [assignToTags] = useMutation(ASSIGN_TO_TAGS); + const [removeFromTags] = useMutation(REMOVE_FROM_TAGS); + + const handleTagAction = async ( + e: FormEvent, + ): Promise => { + e.preventDefault(); + + const mutationObject = { + variables: { + currentTagId, + selectedTagIds: selectedTags.map((selectedTag) => selectedTag._id), + }, + }; + + try { + const { data } = + tagActionType === 'assignToTags' + ? await assignToTags(mutationObject) + : await removeFromTags(mutationObject); + + if (data) { + if (tagActionType === 'assignToTags') { + toast.success(t('successfullyAssignedToTags')); + } else { + toast.success(t('successfullyRemovedFromTags')); + } + hideAssignToTagsModal(); + } + } catch (error: unknown) { + /* istanbul ignore next */ + if (error instanceof Error) { + toast.error(error.message); + } + } + }; + + if (orgUserTagsError) { + return ( +
+
+ +
+ {t('errorOccurredWhileLoadingOrganizationUserTags')} +
+
+
+ ); + } + + return ( + <> + + + + {tagActionType === 'assignToTags' + ? t('assignToTags') + : t('removeFromTags')} + + +
+ + {orgUserTagsLoading ? ( + + ) : ( + <> +
+ {selectedTags.length === 0 ? ( +
+ {t('noTagSelected')} +
+ ) : ( + selectedTags.map((tag: InterfaceTagData) => ( +
+ {tag.name} +
+ )) + )} +
+ +
+ {t('allTags')} +
+ +
+ +
+
+ } + scrollableTarget="scrollableDiv" + > + {userTagsList?.map((tag) => ( +
+ +
+ ))} +
+
+ + )} +
+ + + + + +
+
+ + ); +}; + +export default TagActions; diff --git a/src/components/TagActions/TagActionsMocks.ts b/src/components/TagActions/TagActionsMocks.ts new file mode 100644 index 00000000000..4e3a08f184b --- /dev/null +++ b/src/components/TagActions/TagActionsMocks.ts @@ -0,0 +1,533 @@ +import { + ASSIGN_TO_TAGS, + REMOVE_FROM_TAGS, +} from 'GraphQl/Mutations/TagMutations'; +import { ORGANIZATION_USER_TAGS_LIST } from 'GraphQl/Queries/OrganizationQueries'; +import { USER_TAG_SUB_TAGS } from 'GraphQl/Queries/userTagQueries'; +import { TAGS_QUERY_LIMIT } from 'utils/organizationTagsUtils'; + +export const MOCKS = [ + { + request: { + query: ORGANIZATION_USER_TAGS_LIST, + variables: { + id: '123', + first: TAGS_QUERY_LIMIT, + }, + }, + result: { + data: { + organizations: [ + { + userTags: { + edges: [ + { + node: { + _id: '1', + name: 'userTag 1', + usersAssignedTo: { + totalCount: 5, + }, + childTags: { + totalCount: 11, + }, + }, + cursor: '1', + }, + { + node: { + _id: '2', + name: 'userTag 2', + usersAssignedTo: { + totalCount: 5, + }, + childTags: { + totalCount: 0, + }, + }, + cursor: '2', + }, + { + node: { + _id: '3', + name: 'userTag 3', + usersAssignedTo: { + totalCount: 0, + }, + childTags: { + totalCount: 5, + }, + }, + cursor: '3', + }, + { + node: { + _id: '4', + name: 'userTag 4', + usersAssignedTo: { + totalCount: 0, + }, + childTags: { + totalCount: 0, + }, + }, + cursor: '4', + }, + { + node: { + _id: '5', + name: 'userTag 5', + usersAssignedTo: { + totalCount: 5, + }, + childTags: { + totalCount: 5, + }, + }, + cursor: '5', + }, + { + node: { + _id: '6', + name: 'userTag 6', + usersAssignedTo: { + totalCount: 6, + }, + childTags: { + totalCount: 6, + }, + }, + cursor: '6', + }, + { + node: { + _id: '7', + name: 'userTag 7', + usersAssignedTo: { + totalCount: 7, + }, + childTags: { + totalCount: 7, + }, + }, + cursor: '7', + }, + { + node: { + _id: '8', + name: 'userTag 8', + usersAssignedTo: { + totalCount: 8, + }, + childTags: { + totalCount: 8, + }, + }, + cursor: '8', + }, + { + node: { + _id: '9', + name: 'userTag 9', + usersAssignedTo: { + totalCount: 9, + }, + childTags: { + totalCount: 9, + }, + }, + cursor: '9', + }, + { + node: { + _id: '10', + name: 'userTag 10', + usersAssignedTo: { + totalCount: 10, + }, + childTags: { + totalCount: 10, + }, + }, + cursor: '10', + }, + ], + pageInfo: { + startCursor: '1', + endCursor: '10', + hasNextPage: true, + hasPreviousPage: false, + }, + totalCount: 12, + }, + }, + ], + }, + }, + }, + { + request: { + query: ORGANIZATION_USER_TAGS_LIST, + variables: { + id: '123', + first: TAGS_QUERY_LIMIT, + after: '10', + }, + }, + result: { + data: { + organizations: [ + { + userTags: { + edges: [ + { + node: { + _id: '11', + name: 'userTag 11', + usersAssignedTo: { + totalCount: 5, + }, + childTags: { + totalCount: 5, + }, + }, + cursor: '11', + }, + { + node: { + _id: '12', + name: 'userTag 12', + usersAssignedTo: { + totalCount: 5, + }, + childTags: { + totalCount: 0, + }, + }, + cursor: '12', + }, + ], + pageInfo: { + startCursor: '11', + endCursor: '12', + hasNextPage: false, + hasPreviousPage: true, + }, + totalCount: 12, + }, + }, + ], + }, + }, + }, + { + request: { + query: USER_TAG_SUB_TAGS, + variables: { + id: '1', + first: TAGS_QUERY_LIMIT, + }, + }, + result: { + data: { + getUserTag: { + name: 'userTag 1', + childTags: { + edges: [ + { + node: { + _id: 'subTag1', + name: 'subTag 1', + usersAssignedTo: { + totalCount: 5, + }, + childTags: { + totalCount: 5, + }, + }, + cursor: 'subTag1', + }, + { + node: { + _id: 'subTag2', + name: 'subTag 2', + usersAssignedTo: { + totalCount: 5, + }, + childTags: { + totalCount: 0, + }, + }, + cursor: 'subTag2', + }, + { + node: { + _id: 'subTag3', + name: 'subTag 3', + usersAssignedTo: { + totalCount: 0, + }, + childTags: { + totalCount: 5, + }, + }, + cursor: 'subTag3', + }, + { + node: { + _id: 'subTag4', + name: 'subTag 4', + usersAssignedTo: { + totalCount: 0, + }, + childTags: { + totalCount: 0, + }, + }, + cursor: 'subTag4', + }, + { + node: { + _id: 'subTag5', + name: 'subTag 5', + usersAssignedTo: { + totalCount: 5, + }, + childTags: { + totalCount: 5, + }, + }, + cursor: 'subTag5', + }, + { + node: { + _id: 'subTag6', + name: 'subTag 6', + usersAssignedTo: { + totalCount: 5, + }, + childTags: { + totalCount: 5, + }, + }, + cursor: 'subTag6', + }, + { + node: { + _id: 'subTag7', + name: 'subTag 7', + usersAssignedTo: { + totalCount: 5, + }, + childTags: { + totalCount: 5, + }, + }, + cursor: 'subTag7', + }, + { + node: { + _id: 'subTag8', + name: 'subTag 8', + usersAssignedTo: { + totalCount: 5, + }, + childTags: { + totalCount: 5, + }, + }, + cursor: 'subTag8', + }, + { + node: { + _id: 'subTag9', + name: 'subTag 9', + usersAssignedTo: { + totalCount: 5, + }, + childTags: { + totalCount: 5, + }, + }, + cursor: 'subTag9', + }, + { + node: { + _id: 'subTag10', + name: 'subTag 10', + usersAssignedTo: { + totalCount: 5, + }, + childTags: { + totalCount: 5, + }, + }, + cursor: 'subTag10', + }, + ], + pageInfo: { + startCursor: 'subTag1', + endCursor: 'subTag10', + hasNextPage: true, + hasPreviousPage: false, + }, + totalCount: 11, + }, + }, + }, + }, + }, + { + request: { + query: USER_TAG_SUB_TAGS, + variables: { + id: '1', + after: 'subTag10', + first: TAGS_QUERY_LIMIT, + }, + }, + result: { + data: { + getUserTag: { + name: 'userTag 1', + childTags: { + edges: [ + { + node: { + _id: 'subTag11', + name: 'subTag 11', + usersAssignedTo: { + totalCount: 0, + }, + childTags: { + totalCount: 0, + }, + }, + cursor: 'subTag11', + }, + ], + pageInfo: { + startCursor: 'subTag11', + endCursor: 'subTag11', + hasNextPage: false, + hasPreviousPage: true, + }, + totalCount: 11, + }, + }, + }, + }, + }, + { + request: { + query: ASSIGN_TO_TAGS, + variables: { + currentTagId: '1', + selectedTagIds: ['2', '3'], + }, + }, + result: { + data: { + assignToUserTags: { + _id: '1', + }, + }, + }, + }, + { + request: { + query: REMOVE_FROM_TAGS, + variables: { + currentTagId: '1', + selectedTagIds: ['2'], + }, + }, + result: { + data: { + removeFromUserTags: { + _id: '1', + }, + }, + }, + }, +]; + +export const MOCKS_ERROR_ORGANIZATION_TAGS_QUERY = [ + { + request: { + query: ORGANIZATION_USER_TAGS_LIST, + variables: { + id: '123', + first: TAGS_QUERY_LIMIT, + }, + }, + error: new Error('Mock Graphql Error for organization root tags query'), + }, +]; + +export const MOCKS_ERROR_SUBTAGS_QUERY = [ + { + request: { + query: ORGANIZATION_USER_TAGS_LIST, + variables: { + id: '123', + first: TAGS_QUERY_LIMIT, + }, + }, + result: { + data: { + organizations: [ + { + userTags: { + edges: [ + { + node: { + _id: '1', + name: 'userTag 1', + usersAssignedTo: { + totalCount: 5, + }, + childTags: { + totalCount: 11, + }, + }, + cursor: '1', + }, + { + node: { + _id: '2', + name: 'userTag 2', + usersAssignedTo: { + totalCount: 5, + }, + childTags: { + totalCount: 0, + }, + }, + cursor: '2', + }, + ], + pageInfo: { + startCursor: '1', + endCursor: '2', + hasNextPage: false, + hasPreviousPage: false, + }, + totalCount: 2, + }, + }, + ], + }, + }, + }, + { + request: { + query: USER_TAG_SUB_TAGS, + variables: { + id: '1', + first: TAGS_QUERY_LIMIT, + }, + }, + error: new Error('Mock Graphql Error for subTags query'), + }, +]; diff --git a/src/components/TagActions/TagNode.tsx b/src/components/TagActions/TagNode.tsx new file mode 100644 index 00000000000..db08c3b4517 --- /dev/null +++ b/src/components/TagActions/TagNode.tsx @@ -0,0 +1,219 @@ +import type { ApolloError } from '@apollo/client'; +import { useQuery } from '@apollo/client'; +import { USER_TAG_SUB_TAGS } from 'GraphQl/Queries/userTagQueries'; +import React, { useState } from 'react'; +import type { + InterfaceQueryUserTagChildTags, + InterfaceTagData, +} from 'utils/interfaces'; +import { TAGS_QUERY_LIMIT } from 'utils/organizationTagsUtils'; +import styles from './TagActions.module.css'; +import InfiniteScroll from 'react-infinite-scroll-component'; +import { WarningAmberRounded } from '@mui/icons-material'; + +/** + * Props for the `TagNode` component. + */ +interface InterfaceTagNodeProps { + tag: InterfaceTagData; + checkedTags: Set; + toggleTagSelection: (tag: InterfaceTagData, isSelected: boolean) => void; + t: (key: string) => string; +} + +/** + * Renders the Tags which can be expanded to list subtags. + */ +const TagNode: React.FC = ({ + tag, + checkedTags, + toggleTagSelection, + t, +}) => { + const [expanded, setExpanded] = useState(false); + + const { + data: subTagsData, + loading: subTagsLoading, + error: subTagsError, + fetchMore: fetchMoreSubTags, + }: { + data?: { + getUserTag: InterfaceQueryUserTagChildTags; + }; + loading: boolean; + error?: ApolloError; + refetch: () => void; + fetchMore: (options: { + variables: { + first: number; + after?: string; + }; + updateQuery: ( + previousResult: { getUserTag: InterfaceQueryUserTagChildTags }, + options: { + fetchMoreResult?: { getUserTag: InterfaceQueryUserTagChildTags }; + }, + ) => { getUserTag: InterfaceQueryUserTagChildTags }; + }) => void; + } = useQuery(USER_TAG_SUB_TAGS, { + variables: { + id: tag._id, + first: TAGS_QUERY_LIMIT, + }, + skip: !expanded, + }); + + if (subTagsError) { + return ( +
+
+ +
+ {t('errorOccurredWhileLoadingSubTags')} +
+
+
+ ); + } + + const subTagsList = subTagsData?.getUserTag.childTags.edges.map( + (edge) => edge.node, + ); + + const handleTagClick = (): void => { + setExpanded(!expanded); + }; + + const handleCheckboxChange = ( + e: React.ChangeEvent, + ): void => { + toggleTagSelection(tag, e.target.checked); + }; + + const loadMoreSubTags = (): void => { + fetchMoreSubTags({ + variables: { + first: TAGS_QUERY_LIMIT, + after: subTagsData?.getUserTag.childTags.pageInfo.endCursor, + }, + updateQuery: ( + prevResult: { getUserTag: InterfaceQueryUserTagChildTags }, + { + fetchMoreResult, + }: { + fetchMoreResult?: { getUserTag: InterfaceQueryUserTagChildTags }; + }, + ) => { + if (!fetchMoreResult) return prevResult; + + return { + getUserTag: { + ...fetchMoreResult.getUserTag, + childTags: { + ...fetchMoreResult.getUserTag.childTags, + edges: [ + ...prevResult.getUserTag.childTags.edges, + ...fetchMoreResult.getUserTag.childTags.edges, + ], + }, + }, + }; + }, + }); + }; + + return ( +
+
+ {tag.childTags.totalCount ? ( + <> + + {expanded ? '▼' : '▶'} + + + {' '} + + ) : ( + <> + + + {' '} + + )} + + {tag.name} +
+ + {expanded && subTagsLoading && ( +
+
+
+
+
+ )} + {expanded && subTagsList?.length && ( +
+
+ +
+
+ } + scrollableTarget={`subTagsScrollableDiv${tag._id}`} + > + {subTagsList.map((tag: InterfaceTagData) => ( +
+ +
+ ))} +
+
+
+ )} +
+ ); +}; + +export default TagNode; diff --git a/src/screens/ManageTag/ManageTag.test.tsx b/src/screens/ManageTag/ManageTag.test.tsx index d497d2a0a89..5de5e97c88e 100644 --- a/src/screens/ManageTag/ManageTag.test.tsx +++ b/src/screens/ManageTag/ManageTag.test.tsx @@ -49,6 +49,7 @@ async function wait(ms = 500): Promise { jest.mock('react-toastify', () => ({ toast: { success: jest.fn(), + info: jest.fn(), error: jest.fn(), }, })); @@ -59,8 +60,7 @@ const cache = new InMemoryCache({ fields: { getUserTag: { keyArgs: false, - merge(existing = {}, incoming) { - console.log(existing); + merge(_, incoming) { return incoming; }, }, @@ -188,6 +188,94 @@ describe('Manage Tag Page', () => { ); }); + test('opens and closes the assignToTags modal', async () => { + renderManageTag(link); + + await wait(); + + await waitFor(() => { + expect(screen.getByTestId('assignToTags')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('assignToTags')); + + await waitFor(() => { + return expect( + screen.findByTestId('closeTagActionsModalBtn'), + ).resolves.toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('closeTagActionsModalBtn')); + + await waitForElementToBeRemoved(() => + screen.queryByTestId('closeTagActionsModalBtn'), + ); + }); + + test('opens and closes the removeFromTags modal', async () => { + renderManageTag(link); + + await wait(); + + await waitFor(() => { + expect(screen.getByTestId('removeFromTags')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('removeFromTags')); + + await waitFor(() => { + return expect( + screen.findByTestId('closeTagActionsModalBtn'), + ).resolves.toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('closeTagActionsModalBtn')); + + await waitForElementToBeRemoved(() => + screen.queryByTestId('closeTagActionsModalBtn'), + ); + }); + + test('opens and closes the edit tag modal', async () => { + renderManageTag(link); + + await wait(); + + await waitFor(() => { + expect(screen.getByTestId('editTag')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('editTag')); + + await waitFor(() => { + return expect( + screen.findByTestId('closeEditTagModalBtn'), + ).resolves.toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('closeEditTagModalBtn')); + + await waitForElementToBeRemoved(() => + screen.queryByTestId('closeEditTagModalBtn'), + ); + }); + + test('opens and closes the remove tag modal', async () => { + renderManageTag(link); + + await wait(); + + await waitFor(() => { + expect(screen.getByTestId('removeTag')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('removeTag')); + + await waitFor(() => { + return expect( + screen.findByTestId('removeUserTagModalCloseBtn'), + ).resolves.toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('removeUserTagModalCloseBtn')); + + await waitForElementToBeRemoved(() => + screen.queryByTestId('removeUserTagModalCloseBtn'), + ); + }); + test("navigates to the member's profile after clicking the view option", async () => { renderManageTag(link); @@ -296,4 +384,57 @@ describe('Manage Tag Page', () => { ); }); }); + + test('successfully edits the tag name', async () => { + renderManageTag(link); + + await wait(); + + await waitFor(() => { + expect(screen.getByTestId('editTag')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('editTag')); + + userEvent.click(screen.getByTestId('editTagSubmitBtn')); + + await waitFor(() => { + expect(toast.info).toHaveBeenCalledWith(translations.changeNameToEdit); + }); + + const tagNameInput = screen.getByTestId('tagNameInput'); + await userEvent.clear(tagNameInput); + await userEvent.type(tagNameInput, 'tag 1 edited'); + expect(tagNameInput).toHaveValue('tag 1 edited'); + + userEvent.click(screen.getByTestId('editTagSubmitBtn')); + + await waitFor(() => { + expect(toast.success).toHaveBeenCalledWith( + translations.tagUpdationSuccess, + ); + }); + }); + + test('successfully removes the tag and redirects to orgTags page', async () => { + renderManageTag(link); + + await wait(); + + await waitFor(() => { + expect(screen.getByTestId('removeTag')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('removeTag')); + + userEvent.click(screen.getByTestId('removeUserTagSubmitBtn')); + + await waitFor(() => { + expect(toast.success).toHaveBeenCalledWith( + translations.tagRemovalSuccess, + ); + }); + + await waitFor(() => { + expect(screen.getByTestId('organizationTagsScreen')).toBeInTheDocument(); + }); + }); }); diff --git a/src/screens/ManageTag/ManageTag.tsx b/src/screens/ManageTag/ManageTag.tsx index 25b39e884d3..884b402a7c0 100644 --- a/src/screens/ManageTag/ManageTag.tsx +++ b/src/screens/ManageTag/ManageTag.tsx @@ -1,4 +1,5 @@ -import React, { useState } from 'react'; +import type { FormEvent } from 'react'; +import React, { useEffect, useState } from 'react'; import { useMutation, useQuery, type ApolloError } from '@apollo/client'; import { Search, WarningAmberRounded } from '@mui/icons-material'; import SortIcon from '@mui/icons-material/Sort'; @@ -15,15 +16,21 @@ import { toast } from 'react-toastify'; import type { InterfaceQueryUserTagsAssignedMembers } from 'utils/interfaces'; import styles from './ManageTag.module.css'; import { DataGrid } from '@mui/x-data-grid'; +import type { TagActionType } from 'utils/organizationTagsUtils'; import { dataGridStyle } from 'utils/organizationTagsUtils'; import type { GridCellParams, GridColDef } from '@mui/x-data-grid'; import { Stack } from '@mui/material'; -import { UNASSIGN_USER_TAG } from 'GraphQl/Mutations/TagMutations'; +import { + REMOVE_USER_TAG, + UNASSIGN_USER_TAG, + UPDATE_USER_TAG, +} from 'GraphQl/Mutations/TagMutations'; import { USER_TAG_ANCESTORS, USER_TAGS_ASSIGNED_MEMBERS, } from 'GraphQl/Queries/userTagQueries'; import AddPeopleToTag from 'components/AddPeopleToTag/AddPeopleToTag'; +import TagActions from 'components/TagActions/TagActions'; /** * Component that renders the Manage Tag screen when the app navigates to '/orgtags/:orgId/managetag/:tagId'. @@ -38,9 +45,14 @@ function ManageTag(): JSX.Element { }); const { t: tCommon } = useTranslation('common'); + const [unassignTagModalIsOpen, setUnassignTagModalIsOpen] = useState(false); + const [addPeopleToTagModalIsOpen, setAddPeopleToTagModalIsOpen] = useState(false); - const [unassignTagModalIsOpen, setUnassignTagModalIsOpen] = useState(false); + const [assignToTagsModalIsOpen, setAssignToTagsModalIsOpen] = useState(false); + + const [editTagModalIsOpen, setEditTagModalIsOpen] = useState(false); + const [removeTagModalIsOpen, setRemoveTagModalIsOpen] = useState(false); const { orgId, tagId: currentTagId } = useParams(); const navigate = useNavigate(); @@ -51,6 +63,14 @@ function ManageTag(): JSX.Element { const [unassignUserId, setUnassignUserId] = useState(null); + // a state to specify whether we're assigning to tags or removing from tags + const [tagActionType, setTagActionType] = + useState('assignToTags'); + + const toggleRemoveUserTagModal = (): void => { + setRemoveTagModalIsOpen(!removeTagModalIsOpen); + }; + const showAddPeopleToTagModal = (): void => { setAddPeopleToTagModalIsOpen(true); }; @@ -59,6 +79,22 @@ function ManageTag(): JSX.Element { setAddPeopleToTagModalIsOpen(false); }; + const showAssignToTagsModal = (): void => { + setAssignToTagsModalIsOpen(true); + }; + + const hideAssignToTagsModal = (): void => { + setAssignToTagsModalIsOpen(false); + }; + + const showEditTagModal = (): void => { + setEditTagModalIsOpen(true); + }; + + const hideEditTagModal = (): void => { + setEditTagModalIsOpen(false); + }; + const { data: userTagAssignedMembersData, loading: userTagAssignedMembersLoading, @@ -84,6 +120,7 @@ function ManageTag(): JSX.Element { const { data: orgUserTagAncestorsData, loading: orgUserTagsAncestorsLoading, + refetch: orgUserTagsAncestorsRefetch, error: orgUserTagsAncestorsError, }: { data?: { @@ -123,6 +160,65 @@ function ManageTag(): JSX.Element { } }; + const [edit] = useMutation(UPDATE_USER_TAG); + + const [newTagName, setNewTagName] = useState(''); + const currentTagName = userTagAssignedMembersData?.getUserTag.name ?? ''; + + useEffect(() => { + setNewTagName(userTagAssignedMembersData?.getUserTag.name ?? ''); + }, [userTagAssignedMembersData]); + + const editTag = async (e: FormEvent): Promise => { + e.preventDefault(); + + if (newTagName === currentTagName) { + toast.info(t('changeNameToEdit')); + return; + } + + try { + const { data } = await edit({ + variables: { + tagId: currentTagId, + name: newTagName, + }, + }); + + if (data) { + toast.success(t('tagUpdationSuccess')); + userTagAssignedMembersRefetch(); + orgUserTagsAncestorsRefetch(); + setEditTagModalIsOpen(false); + } + } catch (error: unknown) { + /* istanbul ignore next */ + if (error instanceof Error) { + toast.error(error.message); + } + } + }; + + const [removeUserTag] = useMutation(REMOVE_USER_TAG); + const handleRemoveUserTag = async (): Promise => { + try { + await removeUserTag({ + variables: { + id: currentTagId, + }, + }); + + navigate(`/orgtags/${orgId}`); + toggleRemoveUserTagModal(); + toast.success(t('tagRemovalSuccess') as string); + } catch (error: unknown) { + /* istanbul ignore next */ + if (error instanceof Error) { + toast.error(error.message); + } + } + }; + if (userTagAssignedMembersLoading || orgUserTagsAncestorsLoading) { return ; } @@ -413,18 +509,43 @@ function ManageTag(): JSX.Element {
{'Actions'}
-
-
- {'Email Users'} +
+
{ + setTagActionType('assignToTags'); + showAssignToTagsModal(); + }} + className="ms-5 mt-2 mb-2 btn btn-primary btn-sm w-75" + data-testid="assignToTags" + > + {t('assignToTags')}
+
{ + setTagActionType('removeFromTags'); + showAssignToTagsModal(); + }} + className="ms-5 mb-3 btn btn-danger btn-sm w-75" + data-testid="removeFromTags" + > + {t('removeFromTags')} +
+
-
-
-
- {'Add to tags'} + +
+ {tCommon('edit')}
-
- {'Remove from tags'} +
+ {tCommon('remove')}
@@ -441,6 +562,15 @@ function ManageTag(): JSX.Element { tCommon={tCommon} /> + {/* Assign People To Tags Modal */} + + {/* Unassign Tag Modal */} + + {/* Edit Tag Modal */} + + + {t('tagDetails')} + +
+ + {t('tagName')} + { + setNewTagName(e.target.value); + }} + /> + + + + + + +
+
+ + {/* Remove User Tag Modal */} + + + + {t('removeUserTag')} + + + {t('removeUserTagMessage')} + + + + + ); } diff --git a/src/screens/ManageTag/ManageTagMocks.ts b/src/screens/ManageTag/ManageTagMocks.ts index 27de4de676a..e90e8c58ed3 100644 --- a/src/screens/ManageTag/ManageTagMocks.ts +++ b/src/screens/ManageTag/ManageTagMocks.ts @@ -1,9 +1,15 @@ -import { UNASSIGN_USER_TAG } from 'GraphQl/Mutations/TagMutations'; +import { + REMOVE_USER_TAG, + UNASSIGN_USER_TAG, + UPDATE_USER_TAG, +} from 'GraphQl/Mutations/TagMutations'; +import { ORGANIZATION_USER_TAGS_LIST } from 'GraphQl/Queries/OrganizationQueries'; import { USER_TAG_ANCESTORS, USER_TAGS_ASSIGNED_MEMBERS, USER_TAGS_MEMBERS_TO_ASSIGN_TO, } from 'GraphQl/Queries/userTagQueries'; +import { TAGS_QUERY_LIMIT } from 'utils/organizationTagsUtils'; export const MOCKS = [ { @@ -301,6 +307,195 @@ export const MOCKS = [ }, }, }, + { + request: { + query: ORGANIZATION_USER_TAGS_LIST, + variables: { + id: '123', + first: TAGS_QUERY_LIMIT, + }, + }, + result: { + data: { + organizations: [ + { + userTags: { + edges: [ + { + node: { + _id: '1', + name: 'userTag 1', + usersAssignedTo: { + totalCount: 5, + }, + childTags: { + totalCount: 11, + }, + }, + cursor: '1', + }, + { + node: { + _id: '2', + name: 'userTag 2', + usersAssignedTo: { + totalCount: 5, + }, + childTags: { + totalCount: 0, + }, + }, + cursor: '2', + }, + { + node: { + _id: '3', + name: 'userTag 3', + usersAssignedTo: { + totalCount: 0, + }, + childTags: { + totalCount: 5, + }, + }, + cursor: '3', + }, + { + node: { + _id: '4', + name: 'userTag 4', + usersAssignedTo: { + totalCount: 0, + }, + childTags: { + totalCount: 0, + }, + }, + cursor: '4', + }, + { + node: { + _id: '5', + name: 'userTag 5', + usersAssignedTo: { + totalCount: 5, + }, + childTags: { + totalCount: 5, + }, + }, + cursor: '5', + }, + { + node: { + _id: '6', + name: 'userTag 6', + usersAssignedTo: { + totalCount: 6, + }, + childTags: { + totalCount: 6, + }, + }, + cursor: '6', + }, + { + node: { + _id: '7', + name: 'userTag 7', + usersAssignedTo: { + totalCount: 7, + }, + childTags: { + totalCount: 7, + }, + }, + cursor: '7', + }, + { + node: { + _id: '8', + name: 'userTag 8', + usersAssignedTo: { + totalCount: 8, + }, + childTags: { + totalCount: 8, + }, + }, + cursor: '8', + }, + { + node: { + _id: '9', + name: 'userTag 9', + usersAssignedTo: { + totalCount: 9, + }, + childTags: { + totalCount: 9, + }, + }, + cursor: '9', + }, + { + node: { + _id: '10', + name: 'userTag 10', + usersAssignedTo: { + totalCount: 10, + }, + childTags: { + totalCount: 10, + }, + }, + cursor: '10', + }, + ], + pageInfo: { + startCursor: '1', + endCursor: '10', + hasNextPage: true, + hasPreviousPage: false, + }, + totalCount: 12, + }, + }, + ], + }, + }, + }, + { + request: { + query: UPDATE_USER_TAG, + variables: { + tagId: '1', + name: 'tag 1 edited', + }, + }, + result: { + data: { + updateUserTag: { + _id: '1', + }, + }, + }, + }, + { + request: { + query: REMOVE_USER_TAG, + variables: { + id: '1', + }, + }, + result: { + data: { + removeUserTag: { + _id: '1', + }, + }, + }, + }, ]; export const MOCKS_ERROR_ASSIGNED_MEMBERS = [ diff --git a/src/utils/interfaces.ts b/src/utils/interfaces.ts index c08c9811ebf..3d3af2ac641 100644 --- a/src/utils/interfaces.ts +++ b/src/utils/interfaces.ts @@ -209,18 +209,20 @@ export interface InterfaceQueryOrganizationPostListItem { }; } -interface InterfaceTagData { +export interface InterfaceTagData { + _id: string; + name: string; + usersAssignedTo: { + totalCount: number; + }; + childTags: { + totalCount: number; + }; +} + +interface InterfaceTagNodeData { edges: { - node: { - _id: string; - name: string; - usersAssignedTo: { - totalCount: number; - }; - childTags: { - totalCount: number; - }; - }; + node: InterfaceTagData; cursor: string; }[]; pageInfo: { @@ -250,7 +252,12 @@ interface InterfaceTagMembersData { } export interface InterfaceQueryOrganizationUserTags { - userTags: InterfaceTagData; + userTags: InterfaceTagNodeData; +} + +export interface InterfaceQueryUserTagChildTags { + name: string; + childTags: InterfaceTagNodeData; } export interface InterfaceQueryUserTagsAssignedMembers { @@ -258,9 +265,9 @@ export interface InterfaceQueryUserTagsAssignedMembers { usersAssignedTo: InterfaceTagMembersData; } -export interface InterfaceQueryUserTagChildTags { +export interface InterfaceQueryUserTagsMembersToAssignTo { name: string; - childTags: InterfaceTagData; + usersToAssignTo: InterfaceTagMembersData; } export interface InterfaceQueryUserTagsMembersToAssignTo { diff --git a/src/utils/organizationTagsUtils.ts b/src/utils/organizationTagsUtils.ts index 6f33c26df7d..fc987f37b56 100644 --- a/src/utils/organizationTagsUtils.ts +++ b/src/utils/organizationTagsUtils.ts @@ -23,3 +23,6 @@ export const dataGridStyle = { }; export const ADD_PEOPLE_TO_TAGS_QUERY_LIMIT = 7; +export const TAGS_QUERY_LIMIT = 10; + +export type TagActionType = 'assignToTags' | 'removeFromTags';