diff --git a/.eslintignore b/.eslintignore index 6e1aa5e..13893c5 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,5 @@ coverage/* dist/ node_modules/ -jest.config.js \ No newline at end of file +jest.config.js +babel.config.js diff --git a/.eslintrc.js b/.eslintrc.js index b4e4403..19afddc 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,4 +1,3 @@ -// eslint-disable-next-line import/no-extraneous-dependencies -const { createConfig } = require('@openedx/frontend-build'); - -module.exports = createConfig('eslint'); +module.exports = { + extends: '@edx/eslint-config', +}; diff --git a/package-lock.json b/package-lock.json index 4d7978b..c7827d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,14 +10,30 @@ "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { + "@hookstate/core": "^4.0.2", "@superset-ui/embedded-sdk": "^0.1.3" }, "devDependencies": { "@edx/browserslist-config": "^1.1.1", + "@edx/eslint-config": "^4.3.0", + "@edx/typescript-config": "^1.1.0", "@openedx/frontend-build": "14.3.2", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^14.3.1", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", + "eslint": "^8.57.1", + "eslint-config-airbnb": "^19.0.4", + "eslint-config-airbnb-typescript": "^17.1.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^4.6.2", "glob": "11.0.1", "husky": "9.1.7", - "jest": "29.7.0" + "jest": "29.7.0", + "react-test-renderer": "^18.3.1", + "typescript": "^4.9.5" }, "peerDependencies": { "@edx/frontend-app-authoring": "*", @@ -25,8 +41,8 @@ "@openedx/frontend-plugin-framework": "^1.2.1", "@openedx/paragon": "^22.8.1", "@tanstack/react-query": "4.36.1", - "react": "^17.0.2", - "react-dom": "^17.0.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-redux": "^7.2.9", "react-router-dom": "^6.27.0" }, @@ -44,6 +60,13 @@ "node": ">=0.10.0" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.2.tgz", + "integrity": "sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==", + "dev": true, + "license": "MIT" + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -2321,9 +2344,10 @@ } }, "node_modules/@eslint/js": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.44.0.tgz", - "integrity": "sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "license": "MIT", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -2661,12 +2685,23 @@ "postcss": "^8.0.0" } }, + "node_modules/@hookstate/core": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@hookstate/core/-/core-4.0.2.tgz", + "integrity": "sha512-DTyb4fKk8GCwKFCVoF/2v1qnvs9rEB2Rc9ZH5eh1LRNemUVnC41Vuy6d8UGqQSA0PIUk3DRPzlOA0QQKONpSmQ==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.6 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", + "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" }, @@ -2689,7 +2724,9 @@ "node_modules/@humanwhocodes/object-schema": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==" + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "license": "BSD-3-Clause" }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -3498,6 +3535,303 @@ "react": "^16.9.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/@openedx/frontend-build/node_modules/@eslint/js": { + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.44.0.tgz", + "integrity": "sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==", + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@openedx/frontend-build/node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@openedx/frontend-build/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/@openedx/frontend-build/node_modules/ast-types-flow": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==", + "license": "ISC" + }, + "node_modules/@openedx/frontend-build/node_modules/axobject-query": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.4.tgz", + "integrity": "sha512-aPTElBrbifBU1krmZxGZOlBkslORe7Ll7+BDnI50Wy4LgOt69luMgevkDfTq1O/ZgprooPCtWpjCwKSZw/iZ4A==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@openedx/frontend-build/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@openedx/frontend-build/node_modules/eslint": { + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.44.0.tgz", + "integrity": "sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.4.0", + "@eslint/eslintrc": "^2.1.0", + "@eslint/js": "8.44.0", + "@humanwhocodes/config-array": "^0.11.10", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.0", + "eslint-visitor-keys": "^3.4.1", + "espree": "^9.6.0", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@openedx/frontend-build/node_modules/eslint-plugin-jsx-a11y": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz", + "integrity": "sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.7", + "aria-query": "^5.1.3", + "array-includes": "^3.1.6", + "array.prototype.flatmap": "^1.3.1", + "ast-types-flow": "^0.0.7", + "axe-core": "^4.6.2", + "axobject-query": "^3.1.1", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "has": "^1.0.3", + "jsx-ast-utils": "^3.3.3", + "language-tags": "=1.0.5", + "minimatch": "^3.1.2", + "object.entries": "^1.1.6", + "object.fromentries": "^2.0.6", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/@openedx/frontend-build/node_modules/eslint-plugin-react": { + "version": "7.33.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", + "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==", + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flatmap": "^1.3.1", + "array.prototype.tosorted": "^1.1.1", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.12", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.6", + "object.fromentries": "^2.0.6", + "object.hasown": "^1.1.2", + "object.values": "^1.1.6", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.4", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.8" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/@openedx/frontend-build/node_modules/eslint-plugin-react-hooks": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.1.tgz", + "integrity": "sha512-Ck77j8hF7l9N4S/rzSLOWEKpn994YH6iwUK8fr9mXIaQvGpQYmOnQLbiue1u5kI5T1y+gdgqosnEAO9NCz0DBg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/@openedx/frontend-build/node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@openedx/frontend-build/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@openedx/frontend-build/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@openedx/frontend-build/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@openedx/frontend-build/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@openedx/frontend-build/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@openedx/frontend-build/node_modules/language-tags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", + "integrity": "sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==", + "license": "MIT", + "dependencies": { + "language-subtag-registry": "~0.3.2" + } + }, + "node_modules/@openedx/frontend-build/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@openedx/frontend-build/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@openedx/frontend-plugin-framework": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@openedx/frontend-plugin-framework/-/frontend-plugin-framework-1.6.0.tgz", @@ -4129,6 +4463,7 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz", "integrity": "sha512-INJYZQJP7g+IoDUh/475NlGiTeMfwTXUEr3tmRneckHIxNolGOW9CTq83S8cxq0CgJwwcMzMJFchxvlwe7Rk8Q==", + "license": "MIT", "peer": true, "peerDependencies": { "react": ">=16.3.2" @@ -4138,6 +4473,7 @@ "version": "0.4.16", "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", "integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==", + "license": "MIT", "peer": true, "dependencies": { "dequal": "^2.0.3" @@ -4480,6 +4816,132 @@ } } }, + "node_modules/@testing-library/dom": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", + "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", + "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "14.3.1", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.3.1.tgz", + "integrity": "sha512-H99XjUhWQw0lTgyMN05W3xQG1Nh4lq574D8keFf1dDoNTJgp66VbJozRaczoF+wsiaPJNt/TcnfpLGufGxSrZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^9.0.0", + "@types/react-dom": "^18.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -4496,6 +4958,13 @@ "node": ">=10.13.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -4676,6 +5145,7 @@ "version": "2.2.37", "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.37.tgz", "integrity": "sha512-IwpIMieE55oGWiXkQPSBY1nw1nFs6bsKXTFskNY8sdS17K24vyEBRQZEwlRS7ZmXCWnJcQtbxWzly+cODWGs2A==", + "license": "MIT", "peer": true }, "node_modules/@types/istanbul-lib-coverage": { @@ -4796,6 +5266,16 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-dom": { + "version": "18.3.6", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.6.tgz", + "integrity": "sha512-nf22//wEbKXusP6E9pfOCDwFdHAX4u172eaJI4YkDRQEZiorm6KfYnSC2SWLDMVWUOWPERmJnN0ujeAfTBLvrw==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, "node_modules/@types/react-redux": { "version": "7.1.33", "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz", @@ -4809,11 +5289,12 @@ } }, "node_modules/@types/react-transition-group": { - "version": "4.4.10", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", - "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", "peer": true, - "dependencies": { + "peerDependencies": { "@types/react": "*" } }, @@ -4889,6 +5370,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==", + "license": "MIT", "peer": true }, "node_modules/@types/ws": { @@ -4916,6 +5398,7 @@ "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", @@ -4979,6 +5462,7 @@ "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "license": "BSD-2-Clause", "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -5193,6 +5677,12 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -5642,11 +6132,12 @@ } }, "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dependencies": { - "dequal": "^2.0.3" + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" } }, "node_modules/array-buffer-byte-length": { @@ -5705,6 +6196,26 @@ "node": ">=0.10.0" } }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array.prototype.findlastindex": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", @@ -5743,14 +6254,15 @@ } }, "node_modules/array.prototype.flatmap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", - "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5760,15 +6272,19 @@ } }, "node_modules/array.prototype.tosorted": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz", - "integrity": "sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.1.0", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/arraybuffer.prototype.slice": { @@ -5799,9 +6315,10 @@ "peer": true }, "node_modules/ast-types-flow": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", - "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==" + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "license": "MIT" }, "node_modules/async-function": { "version": "1.0.0", @@ -5887,9 +6404,10 @@ } }, "node_modules/axe-core": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.9.0.tgz", - "integrity": "sha512-H5orY+M2Fr56DWmMFpMrq5Ge93qjNdPVqzBv5gWK3aD1OvjBEJlEzxf09z93dGVQeI0LiW+aCMIx1QtShC/zUw==", + "version": "4.10.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", + "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", + "license": "MPL-2.0", "engines": { "node": ">=4" } @@ -5928,11 +6446,12 @@ } }, "node_modules/axobject-query": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", - "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", - "dependencies": { - "dequal": "^2.0.3" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" } }, "node_modules/b4a": { @@ -6515,13 +7034,13 @@ } }, "node_modules/call-bound": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", - "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "get-intrinsic": "^1.2.6" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -7234,6 +7753,13 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -7500,12 +8026,52 @@ "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/deep-equal/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -7682,6 +8248,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "peer": true, "engines": { "node": ">=6" } @@ -7798,6 +8365,13 @@ "node": ">=6.0.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, "node_modules/dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", @@ -8154,6 +8728,34 @@ "node": ">= 0.4" } }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-get-iterator/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/es-iterator-helpers": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", @@ -8293,26 +8895,29 @@ } }, "node_modules/eslint": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.44.0.tgz", - "integrity": "sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.1.0", - "@eslint/js": "8.44.0", - "@humanwhocodes/config-array": "^0.11.10", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.6.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -8322,7 +8927,6 @@ "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", @@ -8334,7 +8938,6 @@ "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "bin": { @@ -8351,6 +8954,7 @@ "version": "19.0.4", "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz", "integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==", + "license": "MIT", "dependencies": { "eslint-config-airbnb-base": "^15.0.0", "object.assign": "^4.1.2", @@ -8389,6 +8993,7 @@ "version": "17.1.0", "resolved": "https://registry.npmjs.org/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-17.1.0.tgz", "integrity": "sha512-GPxI5URre6dDpJ0CtcthSZVBAfI+Uw7un5OYNVxP2EYi3H81Jw701yFP7AU+/vCE7xBtFmjge7kfhhk4+RAiig==", + "license": "MIT", "dependencies": { "eslint-config-airbnb-base": "^15.0.0" }, @@ -8687,32 +9292,32 @@ } }, "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz", - "integrity": "sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==", + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.20.7", - "aria-query": "^5.1.3", - "array-includes": "^3.1.6", - "array.prototype.flatmap": "^1.3.1", - "ast-types-flow": "^0.0.7", - "axe-core": "^4.6.2", - "axobject-query": "^3.1.1", + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", - "has": "^1.0.3", - "jsx-ast-utils": "^3.3.3", - "language-tags": "=1.0.5", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", "minimatch": "^3.1.2", - "object.entries": "^1.1.6", - "object.fromentries": "^2.0.6", - "semver": "^6.3.0" + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" }, "engines": { "node": ">=4.0" }, "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "node_modules/eslint-plugin-jsx-a11y/node_modules/emoji-regex": { @@ -8721,39 +9326,41 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, "node_modules/eslint-plugin-react": { - "version": "7.33.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", - "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==", + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "license": "MIT", "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flatmap": "^1.3.1", - "array.prototype.tosorted": "^1.1.1", + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.12", + "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", + "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", - "object.entries": "^1.1.6", - "object.fromentries": "^2.0.6", - "object.hasown": "^1.1.2", - "object.values": "^1.1.6", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.4", + "resolve": "^2.0.0-next.5", "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.8" + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.1.tgz", - "integrity": "sha512-Ck77j8hF7l9N4S/rzSLOWEKpn994YH6iwUK8fr9mXIaQvGpQYmOnQLbiue1u5kI5T1y+gdgqosnEAO9NCz0DBg==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", "license": "MIT", "engines": { "node": ">=10" @@ -10153,6 +10760,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", + "license": "MIT", "engines": { "node": ">= 0.4.0" } @@ -10848,6 +11456,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -10990,6 +11608,23 @@ "node": ">= 0.10" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -12567,16 +13202,21 @@ } }, "node_modules/language-subtag-registry": { - "version": "0.3.22", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", - "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==" + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "license": "CC0-1.0" }, "node_modules/language-tags": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", - "integrity": "sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "license": "MIT", "dependencies": { - "language-subtag-registry": "~0.3.2" + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" } }, "node_modules/launch-editor": { @@ -12772,6 +13412,16 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.10", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", @@ -12957,6 +13607,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/mini-css-extract-plugin": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.6.2.tgz", @@ -13302,6 +13962,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -13331,13 +14008,15 @@ } }, "node_modules/object.entries": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", - "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" + "es-object-atoms": "^1.1.1" }, "engines": { "node": ">= 0.4" @@ -13378,6 +14057,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.4.tgz", "integrity": "sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==", + "license": "MIT", "dependencies": { "define-properties": "^1.2.1", "es-abstract": "^1.23.2", @@ -13391,11 +14071,13 @@ } }, "node_modules/object.values": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", - "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" }, @@ -14668,6 +15350,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz", "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==", + "license": "MIT", "peer": true, "dependencies": { "react-is": "^16.3.2", @@ -14681,6 +15364,7 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT", "peer": true }, "node_modules/prop-types/node_modules/react-is": { @@ -14923,13 +15607,13 @@ } }, "node_modules/react": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", - "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", "peer": true, "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" }, "engines": { "node": ">=0.10.0" @@ -14939,6 +15623,7 @@ "version": "1.6.8", "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.6.8.tgz", "integrity": "sha512-yD6uN78XlFOkETQp6GRuVe0s5509x3XYx8PfPbirwFTYCj5/RfmSs9YZGCwkUrhZNFzj7tZPdpb+3k50mK1E4g==", + "license": "MIT", "peer": true, "dependencies": { "@babel/runtime": "^7.14.0", @@ -14980,6 +15665,7 @@ "version": "5.6.1", "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz", "integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==", + "license": "MIT", "peer": true, "peerDependencies": { "react": ">=16.8.0", @@ -15029,17 +15715,17 @@ } }, "node_modules/react-dom": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", - "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", "peer": true, "dependencies": { "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "scheduler": "^0.20.2" + "scheduler": "^0.23.2" }, "peerDependencies": { - "react": "17.0.2" + "react": "^18.3.1" } }, "node_modules/react-dropzone": { @@ -15081,6 +15767,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT", "peer": true }, "node_modules/react-focus-lock": { @@ -15223,9 +15910,10 @@ } }, "node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" }, "node_modules/react-lifecycles-compat": { "version": "3.0.4", @@ -15246,6 +15934,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.2.1.tgz", "integrity": "sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA==", + "license": "MIT", "peer": true, "dependencies": { "@babel/runtime": "^7.13.8", @@ -15266,6 +15955,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", + "license": "MIT", "peer": true, "dependencies": { "react-fast-compare": "^3.0.1", @@ -15422,6 +16112,20 @@ "react-dom": ">=16.8" } }, + "node_modules/react-shallow-renderer": { + "version": "16.15.0", + "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz", + "integrity": "sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.1", + "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-style-singleton": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", @@ -15458,10 +16162,26 @@ "react": "^16.8.3 || ^17.0.0-0 || ^18.0.0" } }, + "node_modules/react-test-renderer": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.3.1.tgz", + "integrity": "sha512-KkAgygexHUkQqtvvx/otwxtuFu5cVjfzTCtjXLH9boS19/Nbtg84zS7wIQn39G8IlrhThBpQsMKkq5ZHZIYFXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "react-is": "^18.3.1", + "react-shallow-renderer": "^16.15.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", "peer": true, "dependencies": { "@babel/runtime": "^7.5.5", @@ -15520,6 +16240,20 @@ "node": ">=6.0.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/redux": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", @@ -16057,13 +16791,12 @@ } }, "node_modules/scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", - "peer": true, + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" } }, "node_modules/schema-utils": { @@ -16722,6 +17455,20 @@ "node": ">= 0.8" } }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/streamx": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.16.1.tgz", @@ -16804,23 +17551,39 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, - "node_modules/string.prototype.matchall": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", - "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.7", - "regexp.prototype.flags": "^1.5.2", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -16829,6 +17592,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.10", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", @@ -16925,6 +17698,19 @@ "node": ">=6" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -17764,6 +18550,7 @@ "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index b2df97f..e3b9a6a 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,11 @@ ], "scripts": { "preinstall": "npm run build", - "build": "fedx-scripts babel -x .js,.jsx,.ts,.tsx src --out-dir dist --ignore **/*.test.jsx,**/*.test.js,**/setupTest.js", + "build": "fedx-scripts babel -x .js,.jsx,.ts,.tsx src --out-dir dist --ignore **/*.test.jsx,**/*.test.js,**/setupTest.js && cp src/*.css dist/", "i18n_extract": "fedx-scripts formatjs extract", - "lint": "fedx-scripts eslint --ext .js --ext .jsx .", - "lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .", - "test": "fedx-scripts jest --coverage --passWithNoTests" + "lint": "fedx-scripts eslint --ext .js --ext .jsx --ext .ts --ext .tsx .", + "lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx --ext .ts --ext .tsx .", + "test": "fedx-scripts jest --coverage --passWithNoTests src" }, "husky": { "hooks": { @@ -38,8 +38,8 @@ "@openedx/frontend-plugin-framework": "^1.2.1", "@openedx/paragon": "^22.8.1", "@tanstack/react-query": "4.36.1", - "react": "^17.0.2", - "react-dom": "^17.0.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-redux": "^7.2.9", "react-router-dom": "^6.27.0" }, @@ -54,13 +54,29 @@ } }, "dependencies": { + "@hookstate/core": "^4.0.2", "@superset-ui/embedded-sdk": "^0.1.3" }, "devDependencies": { "@edx/browserslist-config": "^1.1.1", + "@edx/eslint-config": "^4.3.0", + "@edx/typescript-config": "^1.1.0", "@openedx/frontend-build": "14.3.2", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^14.3.1", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", + "eslint": "^8.57.1", + "eslint-config-airbnb": "^19.0.4", + "eslint-config-airbnb-typescript": "^17.1.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^4.6.2", "glob": "11.0.1", "husky": "9.1.7", - "jest": "29.7.0" + "jest": "29.7.0", + "react-test-renderer": "^18.3.1", + "typescript": "^4.9.5" } } diff --git a/src/components/AspectsSidebar.tsx b/src/components/AspectsSidebar.tsx deleted file mode 100644 index 7136987..0000000 --- a/src/components/AspectsSidebar.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { useIntl } from '@edx/frontend-platform/i18n'; -import { - Button, Card, Icon, IconButton, IconButtonWithTooltip, Stack, -} from '@openedx/paragon'; -import { - ArrowBack, AutoGraph, ChevronRight, Close, ViewAgenda, VideoCamera, Edit as EditIcon, -} from '@openedx/paragon/icons'; -import * as React from 'react'; -import { useSelector } from 'react-redux'; -import { useParams } from 'react-router-dom'; -import { Block, useBlockData, useDashboardEmbed } from '../hooks'; -import messages from '../messages'; -import { AspectsSidebarContext } from './AspectsSidebarContext'; - -function* getGradedSubsections(sectionsList) { - for (const section of sectionsList) { - for (const subsection of section.childInfo.children) { - if (subsection.graded) { - yield subsection; - } - } - } -} - -interface CourseContentListProps { - title: string, - contentList?: Block[] | null, - icon: React.ReactNode, -} - -const CourseContentList = ({ title, contentList, icon }:CourseContentListProps) => { - const { - setLocation, - setSidebarTitle, - } = React.useContext(AspectsSidebarContext); - - return ( -
-

{title}

- {contentList?.map(block => ( - - ))} -
- ); -}; - -export const AspectsSidebar = () => { - const intl = useIntl(); - const { - sidebarOpen, - sidebarTitle, - location, - setSidebarOpen, - setLocation, - } = React.useContext(AspectsSidebarContext); - const { courseId } = useParams(); - const courseName = useSelector(state => state.models?.courseDetails?.[courseId]?.name); - const dashboardContainerId = 'dashboard-container'; - const { data } = useBlockData(courseId); - const { error } = useDashboardEmbed(dashboardContainerId, location ?? courseId, sidebarOpen); - const sectionsList = useSelector(state => state.courseOutline.sectionsList); - const gradedSubsections = sectionsList ? Array.from(getGradedSubsections(sectionsList)) : null; - - return sidebarOpen && ( - -
- -

- {intl.formatMessage(messages.analyticsLabel)} - { - setSidebarOpen(false); - }} - /> -

-
-
- {location && ( - setLocation(null)} - size="inline" - /> - )} - {location - ? sidebarTitle - : courseName} -
-
-
- {!location && ( - <> - - - - - )} - {error &&
Error: {error.message}
} - - ); -}; diff --git a/src/components/AspectsSidebar/CourseContentList.tsx b/src/components/AspectsSidebar/CourseContentList.tsx new file mode 100644 index 0000000..25818d1 --- /dev/null +++ b/src/components/AspectsSidebar/CourseContentList.tsx @@ -0,0 +1,60 @@ +import * as React from 'react'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { Button, Icon } from '@openedx/paragon'; +import { ArrowDropDown, ArrowDropUp, ChevronRight } from '@openedx/paragon/icons'; +import messages from '../../messages'; +import { ICON_MAP } from '../../constants'; +import { Block } from '../../types'; + +interface CourseContentListProps { + title: string, + blocks: Block[], + activateDashboard: (block: Block) => void, +} + +export function CourseContentList({ + title, blocks, activateDashboard, +}: CourseContentListProps) { + const intl = useIntl(); + // using undefined is useful for slicing the list + const [showCount, setShowCount] = React.useState(5); + + if (!blocks.length) { + return null; + } + + return ( +
+ {!!title &&

{title}

} + {blocks?.slice(0, showCount).map(block => ( + + ))} + {(blocks?.length > 5) && ( + + )} +
+ ); +} diff --git a/src/components/AspectsSidebar/Dashboard.tsx b/src/components/AspectsSidebar/Dashboard.tsx new file mode 100644 index 0000000..c7fdbf1 --- /dev/null +++ b/src/components/AspectsSidebar/Dashboard.tsx @@ -0,0 +1,139 @@ +import * as React from 'react'; +import { useParams } from 'react-router-dom'; +import { embedDashboard, EmbeddedDashboard, Size } from '@superset-ui/embedded-sdk'; +import { + Alert, Button, ModalDialog, Skeleton, useToggle, +} from '@openedx/paragon'; +import { Fullscreen } from '@openedx/paragon/icons'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { fetchGuestTokenFromBackend, useDashboardConfig } from '../../hooks'; +import '../../styles.css'; +import messages from '../../messages'; + +function Embed({ usageKey }: { usageKey: string }) { + const { loading, error: configError, config: dashboardConfig } = useDashboardConfig(usageKey); + const containerDiv = React.useRef(null); + const { courseId } = useParams(); + const [containerHeight, setContainerHeight] = React.useState(0); + const [count, setCount] = React.useState(0); + + React.useEffect(() => { + // Hide the dashboard when navigating + setContainerHeight(0); + + if (!dashboardConfig?.supersetUrl || !dashboardConfig?.dashboardId || !courseId) { + return; + } + + (async () => { + let iframe: EmbeddedDashboard | null = null; + /** + * The Superset Embed SDK provides a `getScrollSize` method that can return + * the height and width of the iframe. However, the value is resolved as soon + * as the DOM finishes loading. The charts take a while to render fully making + * the value stale and the charts get hidden. + * + * The updateHeight method gets around this by using `setTimeout` to poll for + * the height for 10 seconds or longer if the height keeps changing. + */ + const updateHeight = async () => { + if (iframe) { + const size: Size = await iframe.getScrollSize(); + if ((count < 20) || (size.height !== containerHeight)) { + setContainerHeight(size.height); + setTimeout(updateHeight, 500); + setCount(count + 1); + } + } + }; + + if (!containerDiv.current) { + return; + } + + try { + iframe = await embedDashboard({ + id: dashboardConfig.dashboardId, + supersetDomain: dashboardConfig.supersetUrl, + mountPoint: containerDiv.current, + fetchGuestToken: async () => fetchGuestTokenFromBackend(courseId), + dashboardUiConfig: { + hideTitle: true, + hideTab: true, + filters: { + visible: false, + expanded: false, + }, + urlParams: { + native_filters: dashboardConfig.courseRuns[dashboardConfig.defaultCourseRun].native_filters, + }, + }, + }); + } catch (e) { + return; + } + if (iframe) { + await updateHeight(); + } + })(); + // `containerHeight` is skipped to prevent height changes from re-rendering iframes. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dashboardConfig, courseId]); + + if (loading) { + return ; + } + + if (configError) { + return ( + + {configError} + + ); + } + + return ( +
+ ); +} + +export function Dashboard({ usageKey, title }: { usageKey: string, title: string }) { + const [isModalOpen, openModal, closeModal] = useToggle(); + const intl = useIntl(); + return ( + <> + +
+ +
+ + + {title} + + + + + + + ); +} diff --git a/src/components/AspectsSidebar/index.tsx b/src/components/AspectsSidebar/index.tsx new file mode 100644 index 0000000..c25c8b9 --- /dev/null +++ b/src/components/AspectsSidebar/index.tsx @@ -0,0 +1,167 @@ +import { useIntl } from '@edx/frontend-platform/i18n'; +import * as React from 'react'; +import { + Alert, Icon, IconButton, IconButtonWithTooltip, Stack, Sticky, +} from '@openedx/paragon'; +import { + ArrowBack, AutoGraph, Close, Warning, +} from '@openedx/paragon/icons'; +import { BlockTypes, ICON_MAP } from '../../constants'; +import { useAspectsSidebarContext } from '../../hooks'; + +import messages from '../../messages'; +import { CourseContentList } from './CourseContentList'; +import { Dashboard } from './Dashboard'; +import { Block } from '../../types'; + +type ContentList = { + title: string; + blocks: Block[]; +}; + +interface AspectsSidebarProps { + title: string; + blockType: BlockTypes; + dashboardId: string; + contentLists: ContentList[]; + blockActivatedCallback?: (block: Block) => void; +} + +export function AspectsSidebar({ + title, + blockType, + dashboardId, + contentLists, + blockActivatedCallback, +}: AspectsSidebarProps) { + const intl = useIntl(); + const { + sidebarOpen, setSidebarOpen, setFilteredBlocks, activeBlock, setActiveBlock, + filterUnit, setFilterUnit, + } = useAspectsSidebarContext(); + + // The activeBlock is not reset during page navigations, leading to stale dashboards. + // This useEffect ensures things are reset. The "set" functions are not included in + // the dependency list as they are recreated everytime the "hookState" changes, thus + // resetting everything to null state on every re-render. Setting it to an empty + // array makes sure it is run only on the first render after navigation. + React.useEffect(() => { + setActiveBlock(null); + setFilterUnit(null); + setFilteredBlocks([]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (!sidebarOpen) { + return null; + } + + const hideDashboard: boolean = ( + (!!activeBlock && (activeBlock.type === 'vertical')) + || (!activeBlock && (blockType === 'vertical')) + ); + const topTitle = activeBlock?.displayName || title; + const activeBlockType = activeBlock?.type || blockType; + const contentListSize: number = contentLists.reduce((acc, list) => acc + list.blocks.length, 0); + + const activateDashboard = (block: Block) => { + setActiveBlock(block); + if (activeBlockType === BlockTypes.vertical && !!blockActivatedCallback) { + blockActivatedCallback(block); + } + }; + // reset all the active values to props + const goBack = () => { + // Currently viewing component dashboard of a filtered view of a specific unit + // Go back to the filtered view of the unit + if (filterUnit && (activeBlock?.id !== filterUnit.id)) { + setActiveBlock(filterUnit); + } else if (filterUnit?.id === activeBlock?.id) { + // Viewing the filtered view of a unit - go back to full course view + setActiveBlock(null); + setFilterUnit(null); + setFilteredBlocks([]); + } else { + // reset to default view for all other cases + setActiveBlock(null); + } + }; + + return ( +
+ +
+ + +
+ {intl.formatMessage(messages.analyticsLabel)} + +
+ { + setSidebarOpen(false); + }} + size="sm" + /> +
+

+ {(activeBlock) && ( + goBack()} + size="sm" + /> + )} + + {topTitle} +

+
+ { !hideDashboard && ( + + )} + {((activeBlockType === 'course') || (activeBlockType === 'vertical')) + && contentLists.map(({ title: listTitle, blocks }) => ( + + ))} + + {(hideDashboard && !contentListSize) + && ( + + { + blockType === 'course' + ? intl.formatMessage(messages.noAnalyticsForCourse) + : intl.formatMessage(messages.noAnalyticsForUnit) + } + + )} +
+
+
+ ); +} diff --git a/src/components/AspectsSidebarContext.tsx b/src/components/AspectsSidebarContext.tsx deleted file mode 100644 index 17b7e76..0000000 --- a/src/components/AspectsSidebarContext.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import * as React from 'react'; -import { ReactNode } from 'react'; - -export type SidebarContext = { - sidebarOpen: boolean, - setSidebarOpen: (value: boolean) => void, - location: string, - setLocation: (value: string) => void, - sidebarTitle: string, - setSidebarTitle: (value: string) => void, -} - -export const AspectsSidebarContext = React.createContext({ - sidebarOpen: false, - location: null, - sidebarTitle: null, - setSidebarOpen: (value: boolean) => {}, - setLocation: (value: string) => {}, - setSidebarTitle: (value: string) => {}, -}); - -export const AspectsSidebarProvider = ({ component }: { component: ReactNode }) => { - const [sidebarOpen, setSidebarOpen] = React.useState(false); - const [sidebarTitle, setSidebarTitle] = React.useState(null); - const [location, setLocation] = React.useState(null); - return ( - - {component} - - ); -}; diff --git a/src/components/CourseHeaderButton.tsx b/src/components/CourseHeaderButton.tsx index a52b5d0..41b3109 100644 --- a/src/components/CourseHeaderButton.tsx +++ b/src/components/CourseHeaderButton.tsx @@ -2,30 +2,30 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { Button } from '@openedx/paragon'; import { AutoGraph } from '@openedx/paragon/icons'; import * as React from 'react'; -import { useParams } from 'react-router-dom'; import messages from '../messages'; -import { AspectsSidebarContext, SidebarContext } from './AspectsSidebarContext'; +import { useAspectsSidebarContext } from '../hooks'; -export const CourseHeaderButton = ({ unitTitle = null }: { unitTitle: string | null }) => { +export function CourseHeaderButton() { const intl = useIntl(); - const { blockId } = useParams(); const { sidebarOpen, setSidebarOpen, - setLocation, - setSidebarTitle, - } = React.useContext(AspectsSidebarContext); + setActiveBlock, + setFilterUnit, + setFilteredBlocks, + } = useAspectsSidebarContext(); return ( ); -}; +} diff --git a/src/components/CourseOutlineSidebar.test.tsx b/src/components/CourseOutlineSidebar.test.tsx new file mode 100644 index 0000000..fe9718e --- /dev/null +++ b/src/components/CourseOutlineSidebar.test.tsx @@ -0,0 +1,432 @@ +import * as React from 'react'; +import { render, screen, within } from '@testing-library/react'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { CourseOutlineSidebar } from './CourseOutlineSidebar'; +import { useCourseBlocks, useAspectsSidebarContext } from '../hooks'; +import { AspectsSidebar } from './AspectsSidebar'; +import { BlockTypes } from '../constants'; +import messages from '../messages'; +import { Section, Block } from '../types'; +import '@testing-library/jest-dom'; + +// Mock the child component to check props passed to it +jest.mock('./AspectsSidebar', () => ({ + AspectsSidebar: jest.fn(({ title, contentLists }) => ( +
+ Mock AspectsSidebar + {contentLists.map((list: { title: string, blocks: Block[] }) => ( +
+

{list.title}

+
    + {list.blocks.map((block: Block) => ( +
  • {block.displayName || block.id}
  • + ))} +
+
+ ))} +
+ )), +})); + +// Mock hooks +jest.mock('@edx/frontend-platform/i18n', () => ({ + defineMessages: jest.fn((messageObject) => messageObject), + useIntl: jest.fn(), +})); +jest.mock('../hooks', () => ({ + useCourseBlocks: jest.fn(), + useAspectsSidebarContext: jest.fn(), +})); + +// Test Data +const mockCourseId = 'course-v1:TestX+TST101+2025'; +const mockCourseName = 'Test Course 101'; + +const mockSections: Section[] = [ + { + id: 'section1', + category: 'chapter', + displayName: 'Chapter 1', + graded: false, + childInfo: { + category: 'sequential', + displayName: 'SubSection', + children: [ + { + id: 'subsection1_1', + category: 'sequential', + displayName: 'Graded Sub 1', + graded: true, + childInfo: { + category: 'vertical', children: [], displayName: 'Unit', + }, + }, + { + id: 'subsection1_2', + category: 'sequential', + displayName: 'Non-Graded Sub 1', + graded: false, + childInfo: { + category: 'vertical', children: [], displayName: 'Unit', + }, + }, + ], + }, + }, + { + id: 'section2', + category: 'chapter', + displayName: 'Chapter 2', + graded: false, + childInfo: { + category: 'sequential', + displayName: 'SubSection', + children: [ + { + id: 'subsection2_1', + category: 'sequential', + displayName: 'Graded Sub 2', + graded: true, + childInfo: { + category: 'vertical', + children: [], + displayName: 'Unit', + }, + }, + ], + }, + }, +]; + +const mockProblems: Block[] = [ + { + id: 'problem1', type: BlockTypes.problem, displayName: 'Problem A', + }, + { + id: 'problem2', type: BlockTypes.problem, displayName: 'Problem B', + }, +]; + +const mockVideos: Block[] = [ + { + id: 'video1', type: BlockTypes.video, displayName: 'Video X', + }, + { + id: 'video2', type: BlockTypes.video, displayName: 'Video Y', + }, +]; + +// Test Suite +describe('CourseOutlineSidebar', () => { + // Mock implementations setup + const mockFormatMessage = jest.fn((message) => message.defaultMessage || message.id); + const mockUseIntl = useIntl as jest.Mock; + const mockUseCourseBlocks = useCourseBlocks as jest.Mock; + const mockUseAspectsSidebarContext = useAspectsSidebarContext as jest.Mock; + const MockAspectsSidebar = AspectsSidebar as jest.Mock; // Get the mock constructor + + beforeEach(() => { + // Reset mocks before each test + jest.clearAllMocks(); + + // Default mock implementations + mockUseIntl.mockReturnValue({ formatMessage: mockFormatMessage }); + mockUseCourseBlocks.mockReturnValue({ data: null, isLoading: true }); // Default to loading state + mockUseAspectsSidebarContext.mockReturnValue({ filteredBlocks: undefined }); // Default to no filtering + MockAspectsSidebar.mockClear(); // Clear calls specifically for the component mock + }); + + const renderComponent = (props: Partial> = {}) => { + const defaultProps = { + courseId: mockCourseId, + courseName: mockCourseName, + sections: mockSections, + }; + return render(); + }; + + // --- Test Cases --- + + it('renders AspectsSidebar with basic props', () => { + mockUseCourseBlocks.mockReturnValue({ data: null }); // No data yet + renderComponent(); + + expect(MockAspectsSidebar).toHaveBeenCalledTimes(1); + expect(MockAspectsSidebar).toHaveBeenCalledWith( + expect.objectContaining({ + title: mockCourseName, + blockType: BlockTypes.course, + dashboardId: mockCourseId, + contentLists: expect.any(Array), // Check contentLists in more detail below + }), + {}, // Second argument for context (usually empty for functional components) + ); + }); + + it('displays graded subsections when available and no filtering is active', () => { + mockUseCourseBlocks.mockReturnValue({ data: { problems: [], videos: [] } }); // No problems/videos + mockUseAspectsSidebarContext.mockReturnValue({ filteredBlocks: [] }); // No filtering + renderComponent({ sections: mockSections }); + + expect(mockFormatMessage).toHaveBeenCalledWith(messages.gradedSubsectionAnalytics); + const expectedGradedTitle = messages.gradedSubsectionAnalytics.defaultMessage; + + expect(MockAspectsSidebar).toHaveBeenCalledWith( + expect.objectContaining({ + contentLists: [ + { + title: expectedGradedTitle, + blocks: [ + { + id: 'subsection1_1', + type: 'sequential', + displayName: 'Graded Sub 1', + graded: true, + childInfo: { + category: 'vertical', children: [], displayName: 'Unit', + }, + }, + { + id: 'subsection2_1', + type: 'sequential', + displayName: 'Graded Sub 2', + graded: true, + childInfo: { + category: 'vertical', children: [], displayName: 'Unit', + }, + }, + ], + }, + ], + }), + {}, + ); + + // Also check rendered output from mock + const sidebar = screen.getByTestId('mock-aspects-sidebar'); + const gradedList = within(sidebar).getByTestId(`content-list-${expectedGradedTitle.replace(/\s+/g, '-')}`); + expect(within(gradedList).getByText(expectedGradedTitle)).toBeInTheDocument(); + expect(within(gradedList).getByText('Graded Sub 1')).toBeInTheDocument(); + expect(within(gradedList).getByText('Graded Sub 2')).toBeInTheDocument(); + }); + + it('displays problems when available', () => { + mockUseCourseBlocks.mockReturnValue({ data: { problems: mockProblems, videos: [] } }); + mockUseAspectsSidebarContext.mockReturnValue({ filteredBlocks: undefined }); + renderComponent({ sections: [] }); // No sections to avoid graded list + + expect(mockFormatMessage).toHaveBeenCalledWith(messages.problemAnalytics); + const expectedProblemTitle = messages.problemAnalytics.defaultMessage; + + expect(MockAspectsSidebar).toHaveBeenCalledWith( + expect.objectContaining({ + contentLists: [ + { + title: expectedProblemTitle, + blocks: mockProblems, + }, + ], + }), + {}, + ); + + // Check rendered output + const sidebar = screen.getByTestId('mock-aspects-sidebar'); + const problemList = within(sidebar).getByTestId(`content-list-${expectedProblemTitle.replace(/\s+/g, '-')}`); + expect(within(problemList).getByText(expectedProblemTitle)).toBeInTheDocument(); + expect(within(problemList).getByText('Problem A')).toBeInTheDocument(); + expect(within(problemList).getByText('Problem B')).toBeInTheDocument(); + }); + + it('displays videos when available', () => { + mockUseCourseBlocks.mockReturnValue({ data: { problems: [], videos: mockVideos } }); + mockUseAspectsSidebarContext.mockReturnValue({ filteredBlocks: undefined }); + renderComponent({ sections: [] }); // No sections + + expect(mockFormatMessage).toHaveBeenCalledWith(messages.videoAnalytics); + const expectedVideoTitle = messages.videoAnalytics.defaultMessage; + + expect(MockAspectsSidebar).toHaveBeenCalledWith( + expect.objectContaining({ + contentLists: [ + { + title: expectedVideoTitle, + blocks: mockVideos, + }, + ], + }), + {}, + ); + + // Check rendered output + const sidebar = screen.getByTestId('mock-aspects-sidebar'); + const videoList = within(sidebar).getByTestId(`content-list-${expectedVideoTitle.replace(/\s+/g, '-')}`); + expect(within(videoList).getByText(expectedVideoTitle)).toBeInTheDocument(); + expect(within(videoList).getByText('Video X')).toBeInTheDocument(); + expect(within(videoList).getByText('Video Y')).toBeInTheDocument(); + }); + + it('displays all available content lists in order (graded, problems, videos) without filtering', () => { + mockUseCourseBlocks.mockReturnValue({ data: { problems: mockProblems, videos: mockVideos } }); + mockUseAspectsSidebarContext.mockReturnValue({ filteredBlocks: [] }); + renderComponent({ sections: mockSections }); + + const expectedGradedTitle = messages.gradedSubsectionAnalytics.defaultMessage; + const expectedProblemTitle = messages.problemAnalytics.defaultMessage; + const expectedVideoTitle = messages.videoAnalytics.defaultMessage; + + expect(MockAspectsSidebar).toHaveBeenCalledWith( + expect.objectContaining({ + contentLists: [ + { + title: expectedGradedTitle, + blocks: expect.arrayContaining([ + expect.objectContaining({ id: 'subsection1_1' }), + expect.objectContaining({ id: 'subsection2_1' }), + ]), + }, + { + title: expectedProblemTitle, + blocks: mockProblems, + }, + { + title: expectedVideoTitle, + blocks: mockVideos, + }, + ], + }), + {}, + ); + // Check order and presence via rendered output + const sidebar = screen.getByTestId('mock-aspects-sidebar'); + const lists = within(sidebar).queryAllByRole('heading', { level: 2 }); + expect(lists).toHaveLength(3); + expect(lists[0]).toHaveTextContent(expectedGradedTitle); + expect(lists[1]).toHaveTextContent(expectedProblemTitle); + expect(lists[2]).toHaveTextContent(expectedVideoTitle); + }); + + it('does NOT display graded subsections when filtering is active', () => { + mockUseCourseBlocks.mockReturnValue({ data: { problems: mockProblems, videos: mockVideos } }); + mockUseAspectsSidebarContext.mockReturnValue({ filteredBlocks: ['problem1'] }); // Filtering active + renderComponent({ sections: mockSections }); + + const expectedGradedTitle = messages.gradedSubsectionAnalytics.defaultMessage; + const expectedProblemTitle = messages.problemAnalytics.defaultMessage; + const expectedVideoTitle = messages.videoAnalytics.defaultMessage; + + // Check that graded list is NOT present + expect(MockAspectsSidebar).toHaveBeenCalledWith( + expect.objectContaining({ + contentLists: [ + // Graded list should be absent + { + title: expectedProblemTitle, + blocks: [mockProblems[0]], // Only problem1 due to filter + }, + // No videos match filter + ], + }), + {}, + ); + + // Check rendered output + const sidebar = screen.getByTestId('mock-aspects-sidebar'); + expect(within(sidebar).queryByText(expectedGradedTitle)).not.toBeInTheDocument(); + expect(within(sidebar).getByText(expectedProblemTitle)).toBeInTheDocument(); + // no videos - don't show section + expect(within(sidebar).queryByText(expectedVideoTitle)).not.toBeInTheDocument(); + }); + + it('filters problems based on filteredBlocks from context', () => { + mockUseCourseBlocks.mockReturnValue({ data: { problems: mockProblems, videos: [] } }); + mockUseAspectsSidebarContext.mockReturnValue({ filteredBlocks: ['problem2'] }); // Filter for problem2 + renderComponent({ sections: [] }); + + const expectedProblemTitle = messages.problemAnalytics.defaultMessage; + + expect(MockAspectsSidebar).toHaveBeenCalledWith( + expect.objectContaining({ + contentLists: [ + { + title: expectedProblemTitle, + blocks: [mockProblems[1]], // Only problem2 + }, + ], + }), + {}, + ); + + // Check rendered output + const sidebar = screen.getByTestId('mock-aspects-sidebar'); + const problemList = within(sidebar).getByTestId(`content-list-${expectedProblemTitle.replace(/\s+/g, '-')}`); + expect(within(problemList).queryByText('Problem A')).not.toBeInTheDocument(); + expect(within(problemList).getByText('Problem B')).toBeInTheDocument(); + }); + + it('filters videos based on filteredBlocks from context', () => { + mockUseCourseBlocks.mockReturnValue({ data: { problems: [], videos: mockVideos } }); + mockUseAspectsSidebarContext.mockReturnValue({ filteredBlocks: ['video1'] }); // Filter for video1 + renderComponent({ sections: [] }); + + const expectedVideoTitle = messages.videoAnalytics.defaultMessage; + + expect(MockAspectsSidebar).toHaveBeenCalledWith( + expect.objectContaining({ + contentLists: [ + { + title: expectedVideoTitle, + blocks: [mockVideos[0]], // Only video1 + }, + ], + }), + {}, + ); + + // Check rendered output + const sidebar = screen.getByTestId('mock-aspects-sidebar'); + const videoList = within(sidebar).getByTestId(`content-list-${expectedVideoTitle.replace(/\s+/g, '-')}`); + expect(within(videoList).getByText('Video X')).toBeInTheDocument(); + expect(within(videoList).queryByText('Video Y')).not.toBeInTheDocument(); + }); + + it('handles empty data gracefully', () => { + mockUseCourseBlocks.mockReturnValue({ data: { problems: [], videos: [] } }); + mockUseAspectsSidebarContext.mockReturnValue({ filteredBlocks: undefined }); + renderComponent({ sections: [] }); // No sections either + + expect(MockAspectsSidebar).toHaveBeenCalledWith( + expect.objectContaining({ + contentLists: [], // Expect empty content list + }), + {}, + ); + + // Check rendered output + const sidebar = screen.getByTestId('mock-aspects-sidebar'); + expect(within(sidebar).queryAllByRole('heading', { level: 2 })).toHaveLength(0); + }); + + it('handles null sections gracefully', () => { + mockUseCourseBlocks.mockReturnValue({ data: { problems: mockProblems, videos: [] } }); + mockUseAspectsSidebarContext.mockReturnValue({ filteredBlocks: undefined }); + // @ts-expect-error Testing null case even if type doesn't strictly allow it + renderComponent({ sections: null }); + + const expectedProblemTitle = messages.problemAnalytics.defaultMessage; + + expect(MockAspectsSidebar).toHaveBeenCalledWith( + expect.objectContaining({ + contentLists: [ + { + title: expectedProblemTitle, + blocks: mockProblems, + }, + ], + }), + {}, + ); + // Ensure graded section title wasn't attempted + expect(mockFormatMessage).not.toHaveBeenCalledWith(messages.gradedSubsectionAnalytics); + }); +}); diff --git a/src/components/CourseOutlineSidebar.tsx b/src/components/CourseOutlineSidebar.tsx new file mode 100644 index 0000000..1a2a1f2 --- /dev/null +++ b/src/components/CourseOutlineSidebar.tsx @@ -0,0 +1,68 @@ +import * as React from 'react'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { useCourseBlocks, useAspectsSidebarContext } from '../hooks'; +import { BlockTypes } from '../constants'; +import { AspectsSidebar } from './AspectsSidebar'; +import messages from '../messages'; +import { Section, Block, castToBlock } from '../types'; + +interface Props { + courseId: string; + courseName: string; + sections: Section[]; +} + +function* getGradedSubsections(sections: Section[]) { + for (const section of sections) { + for (const subsection of section.childInfo.children) { + if (subsection.graded) { + yield subsection; + } + } + } +} + +export function CourseOutlineSidebar({ courseId, courseName, sections }: Props) { + const intl = useIntl(); + const { filteredBlocks } = useAspectsSidebarContext(); + const { data } = useCourseBlocks(courseId); + + const gradedSubsections = sections ? Array.from(getGradedSubsections(sections)) : null; + let problems = data?.problems; + let videos = data?.videos; + + if (filteredBlocks?.length) { + problems = problems?.filter(block => filteredBlocks.includes(block.id)); + videos = videos?.filter(block => filteredBlocks.includes(block.id)); + } + const contentLists: { title: string, blocks: Block[] }[] = []; + + // graded subsections are shown only when unit-filtering is off + if (!filteredBlocks?.length && gradedSubsections?.length) { + contentLists.push({ + title: intl.formatMessage(messages.gradedSubsectionAnalytics), + blocks: castToBlock(gradedSubsections) as Block[], + }); + } + if (problems?.length) { + contentLists.push({ + title: intl.formatMessage(messages.problemAnalytics), + blocks: problems, + }); + } + if (videos?.length) { + contentLists.push({ + title: intl.formatMessage(messages.videoAnalytics), + blocks: videos, + }); + } + + return ( + + ); +} diff --git a/src/components/SidebarToggleWrapper.tsx b/src/components/SidebarToggleWrapper.tsx index 52e49f5..74e55fd 100644 --- a/src/components/SidebarToggleWrapper.tsx +++ b/src/components/SidebarToggleWrapper.tsx @@ -1,12 +1,11 @@ -import * as React from 'react'; import { ReactNode } from 'react'; -import { AspectsSidebarContext, SidebarContext } from './AspectsSidebarContext'; +import { useAspectsSidebarContext } from '../hooks'; /** * This plugin component is meant to wrap the sidebar so that the other/default sidebars * are hidden when the Aspects sidebar is displayed. */ export const SidebarToggleWrapper = ({ component }: { component: ReactNode }) => { - const { sidebarOpen } = React.useContext(AspectsSidebarContext); + const { sidebarOpen } = useAspectsSidebarContext(); return !sidebarOpen && component; }; diff --git a/src/components/SubSectionAnalyticsButton.tsx b/src/components/SubSectionAnalyticsButton.tsx new file mode 100644 index 0000000..73182cf --- /dev/null +++ b/src/components/SubSectionAnalyticsButton.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import { IconButton } from '@openedx/paragon'; +import { AutoGraph } from '@openedx/paragon/icons'; +import { useAspectsSidebarContext } from '../hooks'; +import { Block, SubSection, castToBlock } from '../types'; + +export function SubSectionAnalyticsButton({ subsection }: { subsection: SubSection }) { + const { + activeBlock, sidebarOpen, setActiveBlock, setSidebarOpen, + } = useAspectsSidebarContext(); + if (!subsection.graded) { + return null; + } + return ( + { + setSidebarOpen(true); + setActiveBlock(castToBlock(subsection) as Block); + }} + /> + ); +} diff --git a/src/components/UnitActionsButton.tsx b/src/components/UnitActionsButton.tsx index 231995e..725df8a 100644 --- a/src/components/UnitActionsButton.tsx +++ b/src/components/UnitActionsButton.tsx @@ -3,48 +3,52 @@ import { Icon, IconButton } from '@openedx/paragon'; import { AutoGraph } from '@openedx/paragon/icons'; import * as React from 'react'; import messages from '../messages'; -import { AspectsSidebarContext, SidebarContext } from './AspectsSidebarContext'; +import { useAspectsSidebarContext, useChildBlockCounts } from '../hooks'; +import { Block, Unit, castToBlock } from '../types'; -interface UnitActionsButtonProps { - unit: { id: string, displayName: string } | undefined, - subsection: {id: string, displayName: string, graded: boolean}, -} - -export const UnitActionsButton = ({ +export function UnitActionsButton({ unit, - subsection, -}: UnitActionsButtonProps) => { +}: { unit: Unit }) { const intl = useIntl(); const { sidebarOpen, - location, setSidebarOpen, - setSidebarTitle, - setLocation, - } = React.useContext(AspectsSidebarContext); + setFilteredBlocks, + setActiveBlock, + filterUnit, + setFilterUnit, + activeBlock, + } = useAspectsSidebarContext(); + const { data, error } = useChildBlockCounts(unit?.id); - const buttonLocation = unit - ? unit.id - : subsection.id; - const title = unit - ? unit.displayName - : subsection.displayName; - return subsection?.graded && ( + if (error || !data || (data?.blocks && (Object.keys(data?.blocks).length === 0))) { + return null; + } + + return ( { - if (location === buttonLocation) { - setSidebarOpen(false); + setSidebarOpen(true); + if (filterUnit?.id === unit.id) { + setActiveBlock(null); + setFilteredBlocks([]); + setFilterUnit(null); } else { - setLocation(buttonLocation); - setSidebarOpen(true); - setSidebarTitle(title); + setActiveBlock(castToBlock(unit) as Block); + setFilteredBlocks(Object.keys(data.blocks)); + setFilterUnit(castToBlock(unit) as Block); } }} /> ); -}; +} diff --git a/src/components/UnitPageSidebar.tsx b/src/components/UnitPageSidebar.tsx new file mode 100644 index 0000000..79bfc7c --- /dev/null +++ b/src/components/UnitPageSidebar.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +// @ts-ignore +import { useIframe } from 'CourseAuthoring/generic/hooks/context/hooks'; +import { BlockTypes } from '../constants'; +import { AspectsSidebar } from './AspectsSidebar'; +import { castToBlock, XBlock, Block } from '../types'; + +interface Props { + blockId: string; + unitTitle: string; + xBlocks: XBlock[]; +} + +export function UnitPageSidebar({ + blockId, unitTitle, xBlocks, +}: Props) { + const { sendMessageToIframe } = useIframe(); + const contentList = { + title: '', + blocks: React.useMemo( + () => { + const blocks = castToBlock(xBlocks) as Block[]; + return blocks.filter(block => (block.type === 'problem') || (block.type === 'video')); + }, + [xBlocks], + ), + }; + + return ( + sendMessageToIframe('scrollToXBlock', { locator: block.id })} + /> + ); +} diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..f270619 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { + ChromeReaderMode, + HelpOutline, + SchoolOutline, + VideoCamera, + ViewAgenda, +} from '@openedx/paragon/icons'; + +export enum BlockTypes { + course = 'course', + sequential = 'sequential', + vertical = 'vertical', + problem = 'problem', + video = 'video', +} + +export const ICON_MAP: Record = { + [BlockTypes.course]: SchoolOutline, + [BlockTypes.sequential]: ViewAgenda, + [BlockTypes.vertical]: ChromeReaderMode, + [BlockTypes.problem]: HelpOutline, + [BlockTypes.video]: VideoCamera, +}; diff --git a/src/hooks.ts b/src/hooks.ts index 9fae2be..8b04914 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -1,89 +1,56 @@ -import { getConfig, camelCaseObject } from '@edx/frontend-platform'; +import { camelCaseObject, getConfig } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { embedDashboard } from '@superset-ui/embedded-sdk'; import { useQuery } from '@tanstack/react-query'; +import { hookstate, useHookstate } from '@hookstate/core'; import * as React from 'react'; -import { useParams } from 'react-router-dom'; - -export type UsageId = string; - -export type Block = { - id: UsageId; - type: string; - displayName: string; -}; - -export type BlockMap = { - [blockId: UsageId]: Block; -}; - -type BlockResponse = { - blocks: BlockMap; - root: UsageId; -}; +import type { Block, BlockResponse, UsageId } from './types'; const guestTokenUrl = (courseId: string) => `${getConfig().LMS_BASE_URL}/aspects/superset_guest_token/${courseId}`; const dashboardUrl = (usageKey: string) => `${getConfig().LMS_BASE_URL}/aspects/superset_in_context_dashboard/${usageKey}`; -const fetchGuestTokenFromBackend = async (courseId: string) => { +export const fetchGuestTokenFromBackend = async (courseId: string) => { const { data } = await getAuthenticatedHttpClient() .get(guestTokenUrl(courseId)); return data.guestToken; }; -type DashBoardConfig = { - supersetUrl: string, - dashboardId: string, -} +type CourseRunFilterConfig = { + native_filters: string, +}; -export const useDashboardEmbed = (containerId: string, usageKey: string, visible: boolean) => { - const { courseId } = useParams(); - const [dashboardConfig, setDashboardConfig] = React.useState(); - const [loaded, setLoaded] = React.useState(false); - const [error, setError] = React.useState(null); +export type DashBoardConfig = { + supersetUrl: string, + dashboardId: string, + defaultCourseRun: string, + courseRuns: CourseRunFilterConfig[], +}; + +export const useDashboardConfig = (usageKey: string) => { + const [config, setConfig] = React.useState(); + const [loading, setLoading] = React.useState(true); + const [error, setError] = React.useState(''); React.useEffect(() => { - if (!usageKey) { return; } + if (!usageKey) { + return; + } (async () => { - setError(null); + setLoading(true); + setError(''); try { const { data } = await getAuthenticatedHttpClient() .get(dashboardUrl(usageKey)); - setDashboardConfig(data); + setConfig(data); + setLoading(false); } catch (e) { - setError(e); + setLoading(false); + setConfig(null); + setError('Dashboard not found.'); } })(); }, [usageKey]); - React.useEffect(() => { - if (!visible || !dashboardConfig?.supersetUrl || !dashboardConfig?.dashboardId) { return; } - (async () => { - try { - await embedDashboard({ - id: dashboardConfig.dashboardId, - supersetDomain: dashboardConfig.supersetUrl, - mountPoint: document.getElementById(containerId), - fetchGuestToken: async () => fetchGuestTokenFromBackend(courseId), - dashboardUiConfig: { - hideTitle: true, - hideTab: true, - filters: { - visible: false, - expanded: false, - }, - urlParams: { - native_filters: dashboardConfig.courseRuns[dashboardConfig.defaultCourseRun].native_filters, - }, - }, - }); - setLoaded(true); - } catch (e) { - setError(e); - } - })(); - }, [dashboardConfig, containerId, visible, courseId]); - return { loaded, error }; + return { loading, error, config }; }; const getCourseBlocksUrl = (courseId: string) => { @@ -95,14 +62,16 @@ const getCourseBlocksUrl = (courseId: string) => { return url.toString(); }; -export const useBlockData = (courseId?: UsageId) => { +export const useCourseBlocks = (courseId?: UsageId) => { const { data, error } = useQuery({ queryKey: ['course-blocks', courseId], - queryFn: async () => getAuthenticatedHttpClient().get(getCourseBlocksUrl(courseId)), + queryFn: async () => ( + courseId ? getAuthenticatedHttpClient().get(getCourseBlocksUrl(courseId)) : Promise.resolve(null) + ), enabled: !!courseId, - select: (response: {data:BlockResponse}) => { - const videos = []; - const problems = []; + select: (response: { data: BlockResponse }) => { + const videos: Block[] = []; + const problems: Block[] = []; Object.values(response.data.blocks).forEach((block) => { if (block.type === 'video') { videos.push(camelCaseObject(block)); @@ -111,11 +80,67 @@ export const useBlockData = (courseId?: UsageId) => { problems.push(camelCaseObject(block)); } }); - return ({ - videos, - problems, - }); + return ({ videos, problems }); }, }); return { data, error }; }; + +export const useChildBlockCounts = (usageKey: string) : { data: BlockResponse | undefined, error: Error | null } => { + const url = new URL(`${getConfig().LMS_BASE_URL}/api/courses/v1/blocks/${usageKey}`); + url.searchParams.append('all_blocks', 'true'); + url.searchParams.append('block_types_filter', 'problem,video'); + url.searchParams.append('depth', '1'); + + return useQuery({ + queryKey: ['child-blocks', usageKey], + queryFn: async () => getAuthenticatedHttpClient().get(url.toString()), + select: (response: { data: BlockResponse }) => (response.data), + }); +}; + +type SidebarState = { + sidebarOpen: boolean, + activeBlock: Block | null, + filteredBlocks: string[], + filterUnit: Block | null, +}; + +const sidebarState = hookstate({ + sidebarOpen: false, + activeBlock: null, + filteredBlocks: [], + filterUnit: null, +}); + +interface SidebarContextFunctions { + setActiveBlock: (block: Block | null) => void, + setFilteredBlocks: (blocks: string[]) => void, + setSidebarOpen: (value: boolean) => void, + setFilterUnit: (block: Block | null) => void, +} + +interface SidebarContext extends SidebarState, SidebarContextFunctions {} + +export const useAspectsSidebarContext = (): SidebarContext => { + const state = useHookstate(sidebarState); + + return { + sidebarOpen: state.sidebarOpen.get(), + activeBlock: state.activeBlock.get(), + filteredBlocks: state.filteredBlocks.get() as string[], + filterUnit: state.filterUnit.get(), + setSidebarOpen: (value: boolean) => { + state.sidebarOpen.set(value); + }, + setActiveBlock: (value: Block | null) => { + state.activeBlock.set(value); + }, + setFilteredBlocks: (value: string[]) => { + state.filteredBlocks.set(value); + }, + setFilterUnit: (value: Block | null) => { + state.filterUnit.set(value); + }, + }; +}; diff --git a/src/index.ts b/src/index.ts index 7f8c6f3..d33ea89 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ -export { AspectsSidebar } from './components/AspectsSidebar'; -export { AspectsSidebarContext } from './components/AspectsSidebarContext'; -export { AspectsSidebarProvider } from './components/AspectsSidebarContext'; export { CourseHeaderButton } from './components/CourseHeaderButton'; export { SidebarToggleWrapper } from './components/SidebarToggleWrapper'; export { UnitActionsButton } from './components/UnitActionsButton'; export { pluginSlots } from './plugin-slots'; +export { CourseOutlineSidebar } from './components/CourseOutlineSidebar'; +export { UnitPageSidebar } from './components/UnitPageSidebar'; +export { SubSectionAnalyticsButton } from './components/SubSectionAnalyticsButton'; diff --git a/src/messages.ts b/src/messages.ts index 24b96ea..7eccc0c 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -31,6 +31,31 @@ const messages = defineMessages({ defaultMessage: 'Problem Analytics', description: 'Title for card showing list of problem blocks for analytics.', }, + showMore: { + id: 'aspects.button.show-more', + defaultMessage: 'Show more', + description: 'Label for the button that can expand a list of limited items to reveal all items.', + }, + showLess: { + id: 'aspects.button.show-less', + defaultMessage: 'Show less', + description: 'Label for the button that can hide items in a list and show limited items.', + }, + viewLarger: { + id: 'aspects.button.view-larger', + defaultMessage: 'View larger', + description: 'Label for the button under the Dashboard that opens the charts in a Modal.', + }, + noAnalyticsForCourse: { + id: 'aspects.message.no-analytics-for-course', + defaultMessage: 'No analytics available for course!', + description: 'Message to show when there is not analytics available for a course.', + }, + noAnalyticsForUnit: { + id: 'aspects.message.no-analytics-for-unit', + defaultMessage: 'No analytics available for unit!', + description: 'Message to show when there is analytics available for a unit.', + }, }); export default messages; diff --git a/src/plugin-slots.ts b/src/plugin-slots.ts index d8d1655..4326e8e 100644 --- a/src/plugin-slots.ts +++ b/src/plugin-slots.ts @@ -1,12 +1,12 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; -import { AspectsSidebar } from './components/AspectsSidebar'; -import { AspectsSidebarProvider } from './components/AspectsSidebarContext'; import { CourseHeaderButton } from './components/CourseHeaderButton'; import { SidebarToggleWrapper } from './components/SidebarToggleWrapper'; import { UnitActionsButton } from './components/UnitActionsButton'; +import { CourseOutlineSidebar } from './components/CourseOutlineSidebar'; +import { UnitPageSidebar } from './components/UnitPageSidebar'; export const pluginSlots = { - course_outline_sidebar: { + course_authoring_outline_sidebar_slot: { keepDefault: true, plugins: [ { @@ -15,7 +15,7 @@ export const pluginSlots = { id: 'outline-sidebar', priority: 1, type: DIRECT_PLUGIN, - RenderWidget: AspectsSidebar, + RenderWidget: CourseOutlineSidebar, }, }, { @@ -25,7 +25,7 @@ export const pluginSlots = { }, ], }, - course_unit_sidebar: { + course_authoring_unit_sidebar_slot: { keepDefault: true, plugins: [ { @@ -34,7 +34,7 @@ export const pluginSlots = { id: 'course-unit-sidebar', priority: 1, type: DIRECT_PLUGIN, - RenderWidget: AspectsSidebar, + RenderWidget: UnitPageSidebar, }, }, { @@ -100,14 +100,4 @@ export const pluginSlots = { }, ], }, - authoring_app_wrapper: { - keepDefault: true, - plugins: [ - { - op: PLUGIN_OPERATIONS.Wrap, - widgetId: 'default_contents', - wrapper: AspectsSidebarProvider, - }, - ], - }, }; diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..84b5627 --- /dev/null +++ b/src/styles.css @@ -0,0 +1,5 @@ +.aspects-sidebar-embed-container > iframe { + width: 100%; + border: 0; + border-radius: 0.375rem !important; +} diff --git a/src/types.test.ts b/src/types.test.ts new file mode 100644 index 0000000..cb03816 --- /dev/null +++ b/src/types.test.ts @@ -0,0 +1,80 @@ +import { + castToBlock, SubSection, Block, XBlock, +} from './types'; + +describe('castToBlock', () => { + it('converts subsections array to blocks array', () => { + const subSections: SubSection[] = [ + { + category: 'sequential', + childInfo: { + category: 'vertical', + children: [], + displayName: 'Units', + }, + displayName: 'Test SubSection One', + graded: false, + id: 'block-v1:subsection', + }, + ]; + + const blocks: Block[] = [{ + id: 'block-v1:subsection', + type: 'sequential', + displayName: 'Test SubSection One', + graded: false, + childInfo: { + category: 'vertical', + children: [], + displayName: 'Units', + }, + }]; + + expect(castToBlock(subSections)).toEqual(blocks); + }); + + it('converts subsection item to block item', () => { + const subSection: SubSection = { + category: 'sequential', + childInfo: { + category: 'vertical', + children: [], + displayName: 'Units', + }, + displayName: 'Test SubSection One', + graded: false, + id: 'block-v1:subsection', + }; + + const block: Block = { + id: 'block-v1:subsection', + type: 'sequential', + displayName: 'Test SubSection One', + graded: false, + childInfo: { + category: 'vertical', + children: [], + displayName: 'Units', + }, + }; + + expect(castToBlock(subSection)).toEqual(block); + }); + + it('converts XBlocks array to blocks array', () => { + const xblocks: XBlock[] = [{ + id: 'block-v1:problem-block', + blockType: 'problem', + name: 'Problem Block 1', + }]; + + const blocks: Block[] = [{ + id: 'block-v1:problem-block', + type: 'problem', + displayName: 'Problem Block 1', + graded: false, + }]; + + expect(castToBlock(xblocks)).toEqual(blocks); + }); +}); diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..3d972ff --- /dev/null +++ b/src/types.ts @@ -0,0 +1,92 @@ +export type UsageId = string; + +// types from the props +export type Unit = { + category: 'vertical'; + displayName: string; + graded: boolean; + id: UsageId; +}; +export type SubSection = { + category: 'sequential'; + childInfo: { + category: 'vertical'; + children?: Unit[]; + displayName: string; + } + displayName: string; + graded: boolean; + id: UsageId; +}; +export type Section = { + category: 'chapter'; + id: UsageId; + childInfo: { + category: 'sequential'; + displayName: string; + children: SubSection[]; + }, + displayName: string; + graded: boolean; +}; +export type XBlock = { + id: string; + name: string; + blockType: string; +}; + +// generic types used across this plugin +export type Block = { + id: UsageId; + type: string; + displayName: string; + graded?: boolean; + childInfo?: any +}; +export type BlockMap = { + [blockId: UsageId]: Block; +}; +export type BlockResponse = { + blocks: BlockMap; + root: UsageId; +}; + +function categoryItemToBlock(item: SubSection | Unit): Block { + const block: Block = { + id: item.id, + displayName: item.displayName, + type: item.category, + graded: item.graded, + }; + if ('childInfo' in item) { + block.childInfo = item.childInfo; + } + return block; +} +/** + * Converts multiple types of context blocks into a consistent type + * that can be used across the components. + * + * @param items - Various kinds of blocks received from API and Props + * @return Block[] + */ +export function castToBlock(items: SubSection | SubSection[] | Unit | XBlock[]): Block[] | Block { + if (!Array.isArray(items)) { + return categoryItemToBlock(items); + } + + const blocks: Block[] = []; + for (const item of items) { + if ('category' in item) { + blocks.push(categoryItemToBlock(item)); + } else if ('blockType' in item) { // XBlock + blocks.push({ + id: item.id, + type: item.blockType, + displayName: item.name, + graded: false, + }); + } + } + return blocks; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b9bf43b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@edx/typescript-config", + "compilerOptions": { + "rootDir": ".", + "outDir": "dist" + }, + "include": ["src/**/*", ".eslintrc.js"], + "exclude": ["dist", "node_modules"] +}