diff --git a/package-lock.json b/package-lock.json index abd9f5397..5f734cd79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", "@edx/frontend-component-footer": "13.0.2", "@edx/frontend-component-header": "5.0.2", + "@edx/frontend-component-header-edx": "^8.1.0", "@edx/frontend-platform": "7.1.0", "@edx/openedx-atlas": "^0.6.0", "@fortawesome/fontawesome-svg-core": "1.2.36", @@ -2281,6 +2282,223 @@ "react-dom": "^16.9.0 || ^17.0.0" } }, + "node_modules/@edx/frontend-component-header-edx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@edx/frontend-component-header-edx/-/frontend-component-header-edx-8.1.0.tgz", + "integrity": "sha512-AKhSU4PpzNwaMDgUBXSmq8rSBSvMRWaaJEkewgGOn4AzPr4eA5dNXwzHYMQUFMVmAPiqg7fPYmLTE/P05wbbqg==", + "dependencies": { + "@edx/frontend-enterprise-utils": "5.0.0", + "@fortawesome/fontawesome-svg-core": "6.5.1", + "@fortawesome/free-brands-svg-icons": "6.5.1", + "@fortawesome/free-regular-svg-icons": "6.5.1", + "@fortawesome/free-solid-svg-icons": "6.5.1", + "@fortawesome/react-fontawesome": "^0.2.0", + "@openedx/paragon": "21.11.4", + "@reduxjs/toolkit": "1.9.7", + "axios-mock-adapter": "1.22.0", + "babel-polyfill": "6.26.0", + "classnames": "2.5.1", + "core-js": "3.35.1", + "lodash": "4.17.21", + "react-redux": "7.2.9", + "react-responsive": "9.0.2", + "react-transition-group": "4.4.5", + "regenerator-runtime": "0.14.1", + "rosie": "2.1.1", + "timeago.js": "4.0.2" + }, + "peerDependencies": { + "@edx/frontend-platform": "^7.0.1", + "@openedx/paragon": "^21.11.0", + "prop-types": "^15.5.10", + "react": "^16.9.0 || ^17.0.0", + "react-dom": "^16.9.0 || ^17.0.0" + } + }, + "node_modules/@edx/frontend-component-header-edx/node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz", + "integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@edx/frontend-component-header-edx/node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz", + "integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@edx/frontend-component-header-edx/node_modules/@fortawesome/free-brands-svg-icons": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.5.1.tgz", + "integrity": "sha512-093l7DAkx0aEtBq66Sf19MgoZewv1zeY9/4C7vSKPO4qMwEsW/2VYTUTpBtLwfb9T2R73tXaRDPmE4UqLCYHfg==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@edx/frontend-component-header-edx/node_modules/@fortawesome/free-regular-svg-icons": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.1.tgz", + "integrity": "sha512-m6ShXn+wvqEU69wSP84coxLbNl7sGVZb+Ca+XZq6k30SzuP3X4TfPqtycgUh9ASwlNh5OfQCd8pDIWxl+O+LlQ==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@edx/frontend-component-header-edx/node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz", + "integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@edx/frontend-component-header-edx/node_modules/@openedx/paragon": { + "version": "21.11.4", + "resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-21.11.4.tgz", + "integrity": "sha512-H6ugkFPtR6TvwBEdUKUDeJON1RYZq/K7luOfqZOoud3f9rzxJpeMexwViXNkG1l1YM4Vl9RAFVUNJknnNmzVNQ==", + "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.1.1", + "@fortawesome/react-fontawesome": "^0.1.18", + "@popperjs/core": "^2.11.4", + "bootstrap": "^4.6.2", + "chalk": "^4.1.2", + "child_process": "^1.0.2", + "classnames": "^2.3.1", + "email-prop-type": "^3.0.0", + "file-selector": "^0.6.0", + "font-awesome": "^4.7.0", + "glob": "^8.0.3", + "inquirer": "^8.2.5", + "lodash.uniqby": "^4.7.0", + "mailto-link": "^2.0.0", + "prop-types": "^15.8.1", + "react-bootstrap": "^1.6.5", + "react-colorful": "^5.6.1", + "react-dropzone": "^14.2.1", + "react-focus-on": "^3.5.4", + "react-imask": "^7.1.3", + "react-loading-skeleton": "^3.1.0", + "react-popper": "^2.2.5", + "react-proptype-conditional-require": "^1.0.4", + "react-responsive": "^8.2.0", + "react-table": "^7.7.0", + "react-transition-group": "^4.4.2", + "tabbable": "^5.3.3", + "uncontrollable": "^7.2.1", + "uuid": "^9.0.0" + }, + "bin": { + "paragon": "bin/paragon-scripts.js" + }, + "peerDependencies": { + "react": "^16.8.6 || ^17.0.0", + "react-dom": "^16.8.6 || ^17.0.0", + "react-intl": "^5.25.1 || ^6.4.0" + } + }, + "node_modules/@edx/frontend-component-header-edx/node_modules/@openedx/paragon/node_modules/@fortawesome/react-fontawesome": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.19.tgz", + "integrity": "sha512-Hyb+lB8T18cvLNX0S3llz7PcSOAJMLwiVKBuuzwM/nI5uoBw+gQjnf9il0fR1C3DKOI5Kc79pkJ4/xB0Uw9aFQ==", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~1 || ~6", + "react": ">=16.x" + } + }, + "node_modules/@edx/frontend-component-header-edx/node_modules/@openedx/paragon/node_modules/react-responsive": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-8.2.0.tgz", + "integrity": "sha512-iagCqVrw4QSjhxKp3I/YK6+ODkWY6G+YPElvdYKiUUbywwh9Ds0M7r26Fj2/7dWFFbOpcGnJE6uE7aMck8j5Qg==", + "dependencies": { + "hyphenate-style-name": "^1.0.0", + "matchmediaquery": "^0.3.0", + "prop-types": "^15.6.1", + "shallow-equal": "^1.1.0" + }, + "engines": { + "node": ">= 0.10" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@edx/frontend-component-header-edx/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@edx/frontend-component-header-edx/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@edx/frontend-component-header-edx/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@edx/frontend-component-header-edx/node_modules/react-responsive": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-9.0.2.tgz", + "integrity": "sha512-+4CCab7z8G8glgJoRjAwocsgsv6VA2w7JPxFWHRc7kvz8mec1/K5LutNC2MG28Mn8mu6+bu04XZxHv5gyfT7xQ==", + "dependencies": { + "hyphenate-style-name": "^1.0.0", + "matchmediaquery": "^0.3.0", + "prop-types": "^15.6.1", + "shallow-equal": "^1.2.1" + }, + "engines": { + "node": ">=0.10" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@edx/frontend-component-header/node_modules/@fortawesome/fontawesome-common-types": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz", @@ -2338,6 +2556,38 @@ "node": ">=6" } }, + "node_modules/@edx/frontend-enterprise-utils": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@edx/frontend-enterprise-utils/-/frontend-enterprise-utils-5.0.0.tgz", + "integrity": "sha512-r97Z0fLkkQlSxCa0B1+CwUI2PbXeMaUydVFamhgHcbgYIecgZ9mS6ey45vWhQ/9TODF30+eOwjyoykM5vYrS1w==", + "dependencies": { + "@testing-library/react": "12.1.4", + "history": "4.10.1" + }, + "peerDependencies": { + "@edx/frontend-platform": "^7.0.0", + "react": "^16.12.0 || ^17.0.0", + "react-dom": "^16.12.0 || ^17.0.0", + "react-router-dom": "^6.0.0" + } + }, + "node_modules/@edx/frontend-enterprise-utils/node_modules/@testing-library/react": { + "version": "12.1.4", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-12.1.4.tgz", + "integrity": "sha512-jiPKOm7vyUw311Hn/HlNQ9P8/lHNtArAx0PisXyFixDDvfl8DbD6EUdbshK5eqauvBSvzZd19itqQ9j3nferJA==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^8.0.0", + "@types/react-dom": "*" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, "node_modules/@edx/frontend-platform": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-7.1.0.tgz", @@ -4370,6 +4620,29 @@ "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.2.1.tgz", "integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==" }, + "node_modules/@reduxjs/toolkit": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.7.tgz", + "integrity": "sha512-t7v8ZPxhhKgOKtU+uyJT13lu4vL7az5aFi4IdoDs/eS548edn2M8Ik9h8fxgvMjGoAUVFSt6ZC1P5cWmQ014QQ==", + "dependencies": { + "immer": "^9.0.21", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.8" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.0.2" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@remix-run/router": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.0.tgz", @@ -4706,7 +4979,6 @@ "version": "8.20.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz", "integrity": "sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -4725,7 +4997,6 @@ "version": "5.1.3", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", - "dev": true, "dependencies": { "deep-equal": "^2.0.5" } @@ -4802,8 +5073,7 @@ "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 + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -5141,7 +5411,6 @@ "version": "17.0.25", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.25.tgz", "integrity": "sha512-urx7A7UxkZQmThYA4So0NelOVjx3V4rNFVJwp0WZlbIK5eM4rNJDiN3R/E9ix0MBh6kAEojk/9YL+Te6D9zHNA==", - "dev": true, "dependencies": { "@types/react": "^17" } @@ -7614,7 +7883,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", - "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.0", "call-bind": "^1.0.5", @@ -7645,8 +7913,7 @@ "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 + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" }, "node_modules/deep-is": { "version": "0.1.4", @@ -7928,8 +8195,7 @@ "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 + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==" }, "node_modules/dom-converter": { "version": "0.2.0", @@ -8243,7 +8509,6 @@ "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, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", @@ -8262,8 +8527,7 @@ "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 + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" }, "node_modules/es-module-lexer": { "version": "1.4.1", @@ -10879,7 +11143,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -11151,7 +11414,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -11278,7 +11540,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -11378,7 +11639,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -11398,7 +11658,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.1" @@ -13279,7 +13538,6 @@ "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, "bin": { "lz-string": "bin/bin.js" } @@ -13897,7 +14155,6 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.3" @@ -15137,7 +15394,6 @@ "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, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -15151,7 +15407,6 @@ "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, "engines": { "node": ">=10" }, @@ -16483,6 +16738,14 @@ "rimraf": "bin.js" } }, + "node_modules/rosie": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/rosie/-/rosie-2.1.1.tgz", + "integrity": "sha512-2AXB7WrIZXtKMZ6Q/PlozqPF5nu/x7NEvRJZOblrJuprrPfm5gL8JVvJPj9aaib9F8IUALnLUFhzXrwEtnI5cQ==", + "engines": { + "node": ">=10" + } + }, "node_modules/rsvp": { "version": "4.8.5", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", @@ -17798,7 +18061,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", - "dev": true, "dependencies": { "internal-slot": "^1.0.4" }, @@ -18349,6 +18611,11 @@ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, + "node_modules/timeago.js": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/timeago.js/-/timeago.js-4.0.2.tgz", + "integrity": "sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w==" + }, "node_modules/tiny-invariant": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", @@ -19586,7 +19853,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", - "dev": true, "dependencies": { "is-map": "^2.0.1", "is-set": "^2.0.1", diff --git a/package.json b/package.json index fd2f34157..ff382e759 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", "@edx/frontend-component-footer": "13.0.2", "@edx/frontend-component-header": "5.0.2", + "@edx/frontend-component-header-edx": "^8.1.0", "@edx/frontend-platform": "7.1.0", "@edx/openedx-atlas": "^0.6.0", "@fortawesome/fontawesome-svg-core": "1.2.36", diff --git a/src/account-settings/AccountSettingsPage.jsx b/src/account-settings/AccountSettingsPage.jsx index 8a6e54d05..a391c461d 100644 --- a/src/account-settings/AccountSettingsPage.jsx +++ b/src/account-settings/AccountSettingsPage.jsx @@ -52,6 +52,7 @@ import { fetchSiteLanguages } from './site-language'; import DemographicsSection from './demographics/DemographicsSection'; import { fetchCourseList } from '../notification-preferences/data/thunks'; import { withLocation, withNavigate } from './hoc'; +import NameField from './NameField'; class AccountSettingsPage extends React.Component { constructor(props, context) { @@ -167,6 +168,34 @@ class AccountSettingsPage extends React.Component { this.props.saveSettings(formId, values, extendedProfileObject); }; + handleSubmitFirstAndLastName = (formId, fullName, firstName, lastName) => { + const settingsToBeSaved = []; + + if (Object.keys(this.props.drafts).includes('useVerifiedNameForCerts')) { + settingsToBeSaved.push({ + formId: 'useVerifiedNameForCerts', + commitValues: this.props.formValues.useVerifiedNameForCerts, + }); + } + + settingsToBeSaved.push({ + formId: 'first_name', + commitValues: firstName, + }); + + settingsToBeSaved.push({ + formId: 'last_name', + commitValues: lastName, + }); + + settingsToBeSaved.push({ + formId: 'name', + commitValues: fullName, + }); + + this.props.saveMultipleSettings(settingsToBeSaved, formId, false); + }; + handleSubmitProfileName = (formId, values) => { if (Object.keys(this.props.drafts).includes('useVerifiedNameForCerts')) { this.props.saveMultipleSettings([ @@ -552,37 +581,71 @@ class AccountSettingsPage extends React.Component { isEditable={false} {...editableFieldProps} /> - + ) : ( + + onChange={this.handleEditableFieldChange} + onSubmit={this.handleSubmitProfileName} + /> + )} + {verifiedName && ( { + const { + name, + label, + emptyLabel, + type, + fullNameValue, + firstNameValue, + lastNameValue, + verifiedName, + pendingNameChange, + userSuppliedValue, + saveState, + error, + firstNameError, + lastNameError, + confirmationMessageDefinition, + confirmationValue, + helpText, + onEdit, + onCancel, + onSubmit, + onChange, + isEditing, + isEditable, + isGrayedOut, + intl, + ...others + } = props; + + const id = `field-${name}`; + + const firstNameFieldAttributes = { + name: 'first_name', + id: 'field-firstName', + label: intl.formatMessage(messages['account.settings.field.first.name']), + }; + + const lastNameFieldAttributes = { + name: 'last_name', + id: 'field-lastName', + label: intl.formatMessage(messages['account.settings.field.last.name']), + }; + + const [fullName, setFullName] = useState(''); + const [firstName, setFirstName] = useState(''); + const [lastName, setLastName] = useState(''); + const [fieldError, setFieldError] = useState(''); + + /** + * Concatenates first and last name and generates full name. + * @param first + * @param last + * @returns {`${string} ${string}`} + */ + const generateFullName = (first, last) => { + if (first && last) { + return `${first} ${last}`; + } + return first || last; + }; + + /** + * Splits a full name into first name and last name such that the first word + * is the firstName and rest of the name is last name. + * - If the full name is "John Doe Hamilton", the splitting will be + * e.g., fullName = John Doe => firstName = John, lastName = Doe Hamilton + * @param {string} nameValue The full name to split. + * @returns {object} An object containing the firstName and lastName. + */ + const splitFullName = (nameValue) => { + if (!nameValue) { return null; } + const [first, ...lastNameArr] = nameValue.trim().split(' '); + const last = lastNameArr.join(' '); + return { first, last }; + }; + + /** + * UseEffect for setting first or last name. + */ + useEffect(() => { + if (firstNameValue || lastNameValue) { + setFirstName(firstNameValue); + setLastName(lastNameValue); + } else { + const { first, last } = splitFullName(fullNameValue); + setFirstName(first); + setLastName(last); + } + }, [firstNameValue, fullNameValue, lastNameValue]); + + /** + * UseEffect for setting full name. + */ + useEffect(() => { + if (verifiedName?.status === 'submitted' && pendingNameChange) { + setFullName(pendingNameChange); + } else if (firstNameValue || lastNameValue) { + setFullName(generateFullName(firstNameValue, lastNameValue)); + } else { + setFullName(fullNameValue); + } + }, [firstNameValue, fullNameValue, lastNameValue, pendingNameChange, verifiedName?.status]); + + /** + * UseEffect for setting error + */ + useEffect(() => { + setFieldError(error || firstNameError || lastNameError); + }, [error, firstNameError, lastNameError]); + + const handleSubmit = (e) => { + e.preventDefault(); + const formData = new FormData(e.target); + const firstNameVal = formData.get(firstNameFieldAttributes.name).trim(); + const lastNameVal = formData.get(lastNameFieldAttributes.name).trim(); + const fullNameVal = generateFullName(firstName, lastName); + + onSubmit(name, fullNameVal, firstNameVal, lastNameVal); + }; + + const handleChange = (e, fieldName) => { + onChange(fieldName, e.target.value); + if (fieldName === firstNameFieldAttributes.name) { + onChange(name, generateFullName(e.target.value.trim(), lastNameValue)); + } else if (fieldName === lastNameFieldAttributes.name) { + onChange(name, generateFullName(firstNameValue, e.target.value.trim())); + } + }; + + const handleEdit = () => { + onEdit(name); + }; + + const handleCancel = () => { + onCancel(name); + }; + + const renderEmptyLabel = () => { + if (isEditable) { + return ; + } + return {emptyLabel}; + }; + + const renderValue = (rawValue) => { + if (!rawValue) { + return renderEmptyLabel(); + } + let finalValue = rawValue; + + if (userSuppliedValue) { + finalValue += `: ${userSuppliedValue}`; + } + + return finalValue; + }; + + const renderConfirmationMessage = () => { + if (!confirmationMessageDefinition || !confirmationValue) { + return null; + } + return intl.formatMessage(confirmationMessageDefinition, { + value: confirmationValue, + }); + }; + + return ( + +
+ + +
+ + {firstNameFieldAttributes.label} + + { handleChange(e, firstNameFieldAttributes.name); }} + {...others} + /> +
+
+ +
+ + {lastNameFieldAttributes.label} + + { handleChange(e, lastNameFieldAttributes.name); }} + {...others} + /> +
+
+ {!!helpText && {helpText}} + {fieldError != null && {fieldError}} + {others.children} +
+

+ { + // Swallow clicks if the state is pending. + // We do this instead of disabling the button to prevent + // it from losing focus (disabled elements cannot have focus). + // Disabling it would causes upstream issues in focus management. + // Swallowing the onSubmit event on the form would be better, but + // we would have to add that logic for every field given our + // current structure of the application. + if (saveState === 'pending') { e.preventDefault(); } + }} + disabledStates={[]} + /> + +

+
+ {['name', 'verified_name'].includes(name) && } + + ), + default: ( +
+
+
{label}
+ {isEditable ? ( + + ) : null} +
+

+ {renderValue(fullName)} +

+

{renderConfirmationMessage() || helpText}

+
+ ), + }} + /> + ); +}; + +NameField.propTypes = { + name: PropTypes.string.isRequired, + label: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.node]), + emptyLabel: PropTypes.node, + type: PropTypes.string.isRequired, + fullNameValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + firstNameValue: PropTypes.string, + lastNameValue: PropTypes.string, + pendingNameChange: PropTypes.string, + verifiedName: PropTypes.shape({ + verified_name: PropTypes.string, + status: PropTypes.string, + proctored_exam_attempt_id: PropTypes.number, + }), + userSuppliedValue: PropTypes.string, + saveState: PropTypes.oneOf(['default', 'pending', 'complete', 'error']), + error: PropTypes.string, + firstNameError: PropTypes.string, + lastNameError: PropTypes.string, + confirmationMessageDefinition: PropTypes.shape({ + id: PropTypes.string.isRequired, + defaultMessage: PropTypes.string.isRequired, + description: PropTypes.string, + }), + confirmationValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + helpText: PropTypes.node, + onEdit: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + isEditing: PropTypes.bool, + isEditable: PropTypes.bool, + isGrayedOut: PropTypes.bool, + intl: intlShape.isRequired, +}; + +NameField.defaultProps = { + fullNameValue: undefined, + firstNameValue: undefined, + lastNameValue: undefined, + pendingNameChange: null, + verifiedName: null, + saveState: undefined, + label: undefined, + emptyLabel: undefined, + error: undefined, + firstNameError: undefined, + lastNameError: undefined, + confirmationMessageDefinition: undefined, + confirmationValue: undefined, + helpText: undefined, + isEditing: false, + isEditable: true, + isGrayedOut: false, + userSuppliedValue: undefined, +}; + +export default connect(nameFieldSelector, { + onEdit: openForm, + onCancel: closeForm, +})(injectIntl(NameField)); diff --git a/src/account-settings/data/actions.js b/src/account-settings/data/actions.js index 27d8f7682..0f80510bf 100644 --- a/src/account-settings/data/actions.js +++ b/src/account-settings/data/actions.js @@ -105,9 +105,9 @@ export const savePreviousSiteLanguage = previousSiteLanguage => ({ payload: { previousSiteLanguage }, }); -export const saveMultipleSettings = (settingsArray, form = null) => ({ +export const saveMultipleSettings = (settingsArray, form = null, saveInSeparateCalls = true) => ({ type: SAVE_MULTIPLE_SETTINGS.BASE, - payload: { settingsArray, form }, + payload: { settingsArray, form, saveInSeparateCalls }, }); export const saveMultipleSettingsBegin = () => ({ diff --git a/src/account-settings/data/sagas.js b/src/account-settings/data/sagas.js index c27cbe9ef..9eb9c13e8 100644 --- a/src/account-settings/data/sagas.js +++ b/src/account-settings/data/sagas.js @@ -124,14 +124,24 @@ export function* handleSaveMultipleSettings(action) { try { yield put(saveMultipleSettingsBegin()); const { username, userId } = getAuthenticatedUser(); - const { settingsArray, form } = action.payload; - for (let i = 0; i < settingsArray.length; i += 1) { - const { formId, commitValues } = settingsArray[i]; + const { settingsArray, form, saveInSeparateCalls } = action.payload; + if (saveInSeparateCalls) { + for (let i = 0; i < settingsArray.length; i += 1) { + const { formId, commitValues } = settingsArray[i]; + yield put(saveSettingsBegin()); + const commitData = { [formId]: commitValues }; + const savedSettings = yield call(patchSettings, username, commitData, userId); + yield put(saveSettingsSuccess(savedSettings, commitData)); + } + } else { + const commitData = settingsArray.reduce((data, setting) => ( + { ...data, [setting.formId]: setting.commitValues } + ), {}); yield put(saveSettingsBegin()); - const commitData = { [formId]: commitValues }; const savedSettings = yield call(patchSettings, username, commitData, userId); yield put(saveSettingsSuccess(savedSettings, commitData)); } + yield put(saveMultipleSettingsSuccess(action)); if (form) { yield delay(1000); diff --git a/src/account-settings/data/selectors.js b/src/account-settings/data/selectors.js index 7650aa01e..d683bfa1a 100644 --- a/src/account-settings/data/selectors.js +++ b/src/account-settings/data/selectors.js @@ -128,6 +128,16 @@ export const editableFieldSelector = createStructuredSelector({ isEditing: isEditingSelector, }); +export const nameFieldSelector = createSelector( + editableFieldSelector, + accountSettingsSelector, + (editableFieldSettings, accountSettings) => ({ + ...editableFieldSettings, + firstNameError: accountSettings.errors?.first_name, + lastNameError: accountSettings.errors?.last_name, + }), +); + export const profileDataManagerSelector = createSelector( accountSettingsSelector, accountSettings => accountSettings.profileDataManager, diff --git a/src/account-settings/name-change/NameChange.jsx b/src/account-settings/name-change/NameChange.jsx index d08a840c5..10d56a9fe 100644 --- a/src/account-settings/name-change/NameChange.jsx +++ b/src/account-settings/name-change/NameChange.jsx @@ -62,7 +62,10 @@ const NameChangeModal = ({ })); } else { const draftProfileName = targetFormId === 'name' ? formValues.name : null; - dispatch(requestNameChange(username, draftProfileName, verifiedNameInput)); + const draftFirstName = targetFormId === 'name' ? formValues?.first_name : null; + const draftLastName = targetFormId === 'name' ? formValues?.last_name : null; + + dispatch(requestNameChange(username, draftProfileName, verifiedNameInput, draftFirstName, draftLastName)); } }; @@ -190,6 +193,8 @@ NameChangeModal.propTypes = { errors: PropTypes.shape({}).isRequired, formValues: PropTypes.shape({ name: PropTypes.string, + first_name: PropTypes.string, + last_name: PropTypes.string, verified_name: PropTypes.string, }).isRequired, saveState: PropTypes.string, diff --git a/src/account-settings/name-change/data/actions.js b/src/account-settings/name-change/data/actions.js index f80fbbbf5..efd0a0935 100644 --- a/src/account-settings/name-change/data/actions.js +++ b/src/account-settings/name-change/data/actions.js @@ -2,9 +2,11 @@ import { AsyncActionType } from '../../data/utils'; export const REQUEST_NAME_CHANGE = new AsyncActionType('ACCOUNT_SETTINGS', 'REQUEST_NAME_CHANGE'); -export const requestNameChange = (username, profileName, verifiedName) => ({ +export const requestNameChange = (username, profileName, verifiedName, firstName, lastName) => ({ type: REQUEST_NAME_CHANGE.BASE, - payload: { username, profileName, verifiedName }, + payload: { + username, profileName, verifiedName, firstName, lastName, + }, }); export const requestNameChangeBegin = () => ({ diff --git a/src/account-settings/name-change/data/sagas.js b/src/account-settings/name-change/data/sagas.js index dfbae8e87..4971a8807 100644 --- a/src/account-settings/name-change/data/sagas.js +++ b/src/account-settings/name-change/data/sagas.js @@ -17,7 +17,7 @@ export function* handleRequestNameChange(action) { try { yield put(requestNameChangeBegin()); if (action.payload.profileName) { - yield call(postNameChange, action.payload.profileName); + yield call(postNameChange, action.payload.profileName, action.payload.firstName, action.payload.lastName); profileName = action.payload.profileName; } yield call(postVerifiedName, { diff --git a/src/account-settings/name-change/data/service.js b/src/account-settings/name-change/data/service.js index 70a6cfc15..565850589 100644 --- a/src/account-settings/name-change/data/service.js +++ b/src/account-settings/name-change/data/service.js @@ -4,13 +4,19 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { handleRequestError } from '../../data/utils'; // eslint-disable-next-line import/prefer-default-export -export async function postNameChange(name) { +export async function postNameChange(name, firstName, lastName) { // Requests a pending name change, rather than saving the account name immediately const requestConfig = { headers: { Accept: 'application/json' } }; const requestUrl = `${getConfig().LMS_BASE_URL}/api/user/v1/accounts/name_change/`; + const nameChangePayload = { name }; + if (firstName && lastName) { + nameChangePayload.first_name = firstName; + nameChangePayload.last_name = lastName; + } + const { data } = await getAuthenticatedHttpClient() - .post(requestUrl, { name }, requestConfig) + .post(requestUrl, nameChangePayload, requestConfig) .catch(error => handleRequestError(error)); return data;