diff --git a/package-lock.json b/package-lock.json index 6f8c9b913..876ead807 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3674,9 +3674,9 @@ } }, "@edx/paragon": { - "version": "16.6.4", - "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-16.6.4.tgz", - "integrity": "sha512-2RZg9bO/SzgA24EO9XD4KfA2mfAgb8Kpky4Bw3fUPKuU+xNuGpJNxbV4UhwyS0L3E3X63kXF40bguXOHdHrU0w==", + "version": "16.14.2", + "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-16.14.2.tgz", + "integrity": "sha512-mIq7jeWbZN3EO+Mif38BnpdYdAYd5UG7/O2QtxPAWUedQZ1O+tPUpEapeiq+RFny/miHdyp/LnfMNfy3a5lRhA==", "requires": { "@fortawesome/fontawesome-svg-core": "^1.2.30", "@fortawesome/free-solid-svg-icons": "^5.14.0", @@ -3702,30 +3702,30 @@ }, "dependencies": { "@fortawesome/fontawesome-common-types": { - "version": "0.2.35", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.35.tgz", - "integrity": "sha512-IHUfxSEDS9dDGqYwIW7wTN6tn/O8E0n5PcAHz9cAaBoZw6UpG20IG/YM3NNLaGPwPqgjBAFjIURzqoQs3rrtuw==" + "version": "0.2.36", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz", + "integrity": "sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg==" }, "@fortawesome/fontawesome-svg-core": { - "version": "1.2.35", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.35.tgz", - "integrity": "sha512-uLEXifXIL7hnh2sNZQrIJWNol7cTVIzwI+4qcBIq9QWaZqUblm0IDrtSqbNg+3SQf8SMGHkiSigD++rHmCHjBg==", + "version": "1.2.36", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.36.tgz", + "integrity": "sha512-YUcsLQKYb6DmaJjIHdDWpBIGCcyE/W+p/LMGvjQem55Mm2XWVAP5kWTMKWLv9lwpCVjpLxPyOMOyUocP1GxrtA==", "requires": { - "@fortawesome/fontawesome-common-types": "^0.2.35" + "@fortawesome/fontawesome-common-types": "^0.2.36" } }, "@fortawesome/free-solid-svg-icons": { - "version": "5.15.3", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.3.tgz", - "integrity": "sha512-XPeeu1IlGYqz4VWGRAT5ukNMd4VHUEEJ7ysZ7pSSgaEtNvSo+FLurybGJVmiqkQdK50OkSja2bfZXOeyMGRD8Q==", + "version": "5.15.4", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.4.tgz", + "integrity": "sha512-JLmQfz6tdtwxoihXLg6lT78BorrFyCf59SAwBM6qV/0zXyVeDygJVb3fk+j5Qat+Yvcxp1buLTY5iDh1ZSAQ8w==", "requires": { - "@fortawesome/fontawesome-common-types": "^0.2.35" + "@fortawesome/fontawesome-common-types": "^0.2.36" } }, "@fortawesome/react-fontawesome": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.14.tgz", - "integrity": "sha512-4wqNb0gRLVaBm/h+lGe8UfPPivcbuJ6ecI4hIgW0LjI7kzpYB9FkN0L9apbVzg+lsBdcTf0AlBtODjcSX5mmKA==", + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.15.tgz", + "integrity": "sha512-/HFHdcoLESxxMkqZAcZ6RXDJ69pVApwdwRos/B2kiMWxDSAX2dFK8Er2/+rG+RsrzWB/dsAyjefLmemgmfE18g==", "requires": { "prop-types": "^15.7.2" } @@ -4988,9 +4988,9 @@ } }, "@popperjs/core": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.9.2.tgz", - "integrity": "sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==" + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.10.1.tgz", + "integrity": "sha512-HnUhk1Sy9IuKrxEMdIRCxpIqPw6BFsbYSEUO9p/hNw5sMld/+3OLMWQP80F8/db9qsv3qUjs7ZR5bS/R+iinXw==" }, "@reduxjs/toolkit": { "version": "1.5.0", @@ -6133,9 +6133,9 @@ } }, "@types/invariant": { - "version": "2.2.34", - "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.34.tgz", - "integrity": "sha512-lYUtmJ9BqUN688fGY1U1HZoWT1/Jrmgigx2loq4ZcJpICECm/Om3V314BxdzypO0u5PORKGMM6x0OXaljV1YFg==" + "version": "2.2.35", + "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.35.tgz", + "integrity": "sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg==" }, "@types/istanbul-lib-coverage": { "version": "2.0.3", @@ -6295,9 +6295,9 @@ "dev": true }, "@types/react": { - "version": "17.0.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.14.tgz", - "integrity": "sha512-0WwKHUbWuQWOce61UexYuWTGuGY/8JvtUe/dtQ6lR4sZ3UiylHotJeWpf3ArP9+DSGUoLY3wbU59VyMrJps5VQ==", + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.24.tgz", + "integrity": "sha512-eIpyco99gTH+FTI3J7Oi/OH8MZoFMJuztNRimDOJwH4iGIsKV2qkGnk4M9VzlaVWeEEWLWSQRy0FEA0Kz218cg==", "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -6305,16 +6305,16 @@ }, "dependencies": { "csstype": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz", - "integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==" + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.9.tgz", + "integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==" } } }, "@types/react-transition-group": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.2.tgz", - "integrity": "sha512-KibDWL6nshuOJ0fu8ll7QnV/LVTo3PzQ9aCPnRUYPfX7eZohHwLIdNHj7pftanREzHNP4/nJa8oeM73uSiavMQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.3.tgz", + "integrity": "sha512-fUx5muOWSYP8Bw2BUQ9M9RK9+W1XBK/7FLJ8PTQpnpTEkn0ccyMffyEQvan4C3h53gHdx7KE5Qrxi/LnUGQtdg==", "requires": { "@types/react": "*" } @@ -11584,17 +11584,17 @@ "dev": true }, "focus-lock": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-0.9.1.tgz", - "integrity": "sha512-/2Nj60Cps6yOLSO+CkVbeSKfwfns5XbX6HOedIK9PdzODP04N9c3xqOcPXayN0WsT9YjJvAnXmI0NdqNIDf5Kw==", + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-0.9.2.tgz", + "integrity": "sha512-YtHxjX7a0IC0ZACL5wsX8QdncXofWpGPNoVMuI/nZUrPGp6LmNI6+D5j0pPj+v8Kw5EpweA+T5yImK0rnWf7oQ==", "requires": { "tslib": "^2.0.3" }, "dependencies": { "tslib": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", - "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" } } }, @@ -11788,21 +11788,23 @@ }, "dependencies": { "es-abstract": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz", - "integrity": "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==", + "version": "1.18.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.6.tgz", + "integrity": "sha512-kAeIT4cku5eNLNuUKhlmtuk1/TRZvQoYccn6TO0cSVdf1kzB0T7+dYuVK9MWM7l+/53W2Q8M7N2c6MQvhXFcUQ==", "requires": { "call-bind": "^1.0.2", "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", "get-intrinsic": "^1.1.1", + "get-symbol-description": "^1.0.0", "has": "^1.0.3", "has-symbols": "^1.0.2", - "is-callable": "^1.2.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.4", "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.3", - "is-string": "^1.0.6", - "object-inspect": "^1.10.3", + "is-regex": "^1.1.4", + "is-string": "^1.0.7", + "object-inspect": "^1.11.0", "object-keys": "^1.1.1", "object.assign": "^4.1.2", "string.prototype.trimend": "^1.0.4", @@ -11816,17 +11818,25 @@ "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" }, "is-callable": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", - "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==" + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==" }, "is-regex": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", - "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", "requires": { "call-bind": "^1.0.2", - "has-symbols": "^1.0.2" + "has-tostringtag": "^1.0.0" + } + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "requires": { + "has-tostringtag": "^1.0.0" } }, "object-inspect": { @@ -11935,6 +11945,15 @@ "pump": "^3.0.0" } }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, "get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", @@ -12281,6 +12300,21 @@ "has-symbol-support-x": "^1.4.1" } }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "requires": { + "has-symbols": "^1.0.2" + }, + "dependencies": { + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" + } + } + }, "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", @@ -13414,7 +13448,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", - "dev": true, "requires": { "get-intrinsic": "^1.1.0", "has": "^1.0.3", @@ -20858,9 +20891,9 @@ } }, "react-bootstrap": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.6.1.tgz", - "integrity": "sha512-ojEPQ6OtyIMdLg0Smofk+85PKN6MLKQX3bU0Vwmok/4yNa8DQ2vCGhO2IgHJvT+ERQZ4X+gAQcdn6msAHSwLBg==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.6.3.tgz", + "integrity": "sha512-zsd4l0g68pusOmJ/R5LhTfofT+9RniCwcZsMMNFGJo97d1vT1H2nGlbhLWp/j/pfeXXj9zzR8ugUtKkadcoWnA==", "requires": { "@babel/runtime": "^7.14.0", "@restart/context": "^2.1.4", @@ -20875,16 +20908,16 @@ "invariant": "^2.2.4", "prop-types": "^15.7.2", "prop-types-extra": "^1.1.0", - "react-overlays": "^5.0.1", + "react-overlays": "^5.1.1", "react-transition-group": "^4.4.1", "uncontrollable": "^7.2.1", "warning": "^4.0.3" }, "dependencies": { "@babel/runtime": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz", - "integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==", + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz", + "integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==", "requires": { "regenerator-runtime": "^0.13.4" } @@ -20895,9 +20928,9 @@ "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" }, "csstype": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz", - "integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==" + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.9.tgz", + "integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==" }, "dom-helpers": { "version": "5.2.1", @@ -20919,9 +20952,9 @@ }, "dependencies": { "@babel/runtime": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz", - "integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==", + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz", + "integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==", "requires": { "regenerator-runtime": "^0.13.4" } @@ -21158,17 +21191,17 @@ }, "dependencies": { "@babel/runtime": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz", - "integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==", + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz", + "integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==", "requires": { "regenerator-runtime": "^0.13.4" } }, "csstype": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz", - "integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==" + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.9.tgz", + "integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==" }, "dom-helpers": { "version": "5.2.1", @@ -22491,7 +22524,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, "requires": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -22501,8 +22533,7 @@ "object-inspect": { "version": "1.10.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", - "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==", - "dev": true + "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==" } } }, diff --git a/package.json b/package.json index 5a18c389a..6b202394d 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@edx/brand": "npm:@edx/brand-openedx@1.1.0", "@edx/frontend-component-footer": "10.0.11", "@edx/frontend-platform": "1.11.0", - "@edx/paragon": "16.6.4", + "@edx/paragon": "16.14.2", "@fortawesome/fontawesome-svg-core": "1.2.28", "@fortawesome/free-brands-svg-icons": "5.11.2", "@fortawesome/free-regular-svg-icons": "5.11.2", diff --git a/src/generic/CollapsableEditor.jsx b/src/generic/CollapsableEditor.jsx index ff696775d..71f35d7ad 100644 --- a/src/generic/CollapsableEditor.jsx +++ b/src/generic/CollapsableEditor.jsx @@ -9,6 +9,7 @@ const CollapsableEditor = ({ open, defaultOpen, onToggle, + onClose, onDelete, children, expandAlt, @@ -19,6 +20,7 @@ const CollapsableEditor = ({ - {}} variant="dark" /> @@ -53,9 +57,11 @@ const CollapsableEditor = ({ )}
- {}} variant="dark" />
@@ -77,12 +83,14 @@ CollapsableEditor.propTypes = { expandAlt: PropTypes.string.isRequired, deleteAlt: PropTypes.string.isRequired, collapseAlt: PropTypes.string.isRequired, + onClose: PropTypes.func, }; CollapsableEditor.defaultProps = { onDelete: null, defaultOpen: undefined, open: undefined, + onClose: () => {}, }; export default CollapsableEditor; diff --git a/src/generic/DeletePopup.jsx b/src/generic/DeletePopup.jsx new file mode 100644 index 000000000..60d1db62b --- /dev/null +++ b/src/generic/DeletePopup.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Card } from '@edx/paragon'; + +const DeletePopup = ({ + label, + bodyText, + onDelete, + deleteLabel, + onCancel, + cancelLabel, +}) => ( + + +
{label}
+ {bodyText} +
+ + +
+
+
+); + +DeletePopup.propTypes = { + label: PropTypes.string.isRequired, + bodyText: PropTypes.string.isRequired, + onDelete: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, + deleteLabel: PropTypes.string.isRequired, + cancelLabel: PropTypes.string.isRequired, +}; + +export default DeletePopup; diff --git a/src/generic/FieldFeedback.jsx b/src/generic/FieldFeedback.jsx new file mode 100644 index 000000000..01ca17bba --- /dev/null +++ b/src/generic/FieldFeedback.jsx @@ -0,0 +1,44 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Form, TransitionReplace } from '@edx/paragon'; + +function FieldFeedback({ + renderCondition, + feedback, + type, + hasIcon, + feedbackClasses, + transitionClasses, +}) { + return ( + + {renderCondition ? ( + + +
{feedback}
+
+
+ ) : ( + + )} +
+ ); +} + +FieldFeedback.propTypes = { + feedback: PropTypes.string.isRequired, + type: PropTypes.string, + renderCondition: PropTypes.bool.isRequired, + hasIcon: PropTypes.bool, + feedbackClasses: PropTypes.string, + transitionClasses: PropTypes.string, +}; + +FieldFeedback.defaultProps = { + type: 'default', + hasIcon: false, + feedbackClasses: '', + transitionClasses: '', +}; + +export default FieldFeedback; diff --git a/src/pages-and-resources/discussions/app-config-form/apps/legacy/LegacyConfigForm.jsx b/src/pages-and-resources/discussions/app-config-form/apps/legacy/LegacyConfigForm.jsx index 62b47ba4d..6c5a79064 100644 --- a/src/pages-and-resources/discussions/app-config-form/apps/legacy/LegacyConfigForm.jsx +++ b/src/pages-and-resources/discussions/app-config-form/apps/legacy/LegacyConfigForm.jsx @@ -8,45 +8,35 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import DivisionByGroupFields from '../shared/DivisionByGroupFields'; import AnonymousPostingFields from '../shared/AnonymousPostingFields'; import DiscussionTopics from '../shared/discussion-topics/DiscussionTopics'; -import BlackoutDatesField, { blackoutDatesRegex } from '../shared/BlackoutDatesField'; +import BlackoutDatesField from '../shared/BlackoutDatesField'; import LegacyConfigFormProvider from './LegacyConfigFormProvider'; - import messages from '../shared/messages'; import AppConfigFormDivider from '../shared/AppConfigFormDivider'; +import { checkFieldErrors } from '../../utils'; +import { setupYupExtensions } from '../../../../../utils'; -// eslint-disable-next-line func-names -Yup.addMethod(Yup.object, 'uniqueProperty', function (propertyName, message) { - // eslint-disable-next-line func-names - return this.test('unique', message, function (discussionTopic) { - if (!discussionTopic || !discussionTopic[propertyName]) { - return true; - } - const isDuplicate = this.parent.filter(topic => topic !== discussionTopic) - .some(topic => topic[propertyName]?.toLowerCase() === discussionTopic[propertyName].toLowerCase()); - - if (isDuplicate) { - throw this.createError({ - path: `${this.path}.${propertyName}`, - error: message, - }); - } - return true; - }); -}); +setupYupExtensions(); function LegacyConfigForm({ appConfig, onSubmit, formRef, intl, title, }) { const [validDiscussionTopics, setValidDiscussionTopics] = useState(appConfig.discussionTopics); const legacyFormValidationSchema = Yup.object().shape({ - blackoutDates: Yup.string().matches( - blackoutDatesRegex, - intl.formatMessage(messages.blackoutDatesFormattingError), + blackoutDates: Yup.array( + Yup.object().shape({ + startDate: Yup.date().required(intl.formatMessage(messages.blackoutStartDateRequired)), + endDate: Yup.date().required(intl.formatMessage(messages.blackoutEndDateRequired)).when('startDate', { + is: (startDate) => startDate, + then: Yup.date().min(Yup.ref('startDate'), intl.formatMessage(messages.blackoutEndDateInPast)), + }), + startTime: Yup.string(), + endTime: Yup.string().compare(intl.formatMessage(messages.blackoutEndTimeInPast)), + }), ), discussionTopics: Yup.array( Yup.object({ name: Yup.string().required(intl.formatMessage(messages.discussionTopicRequired)), - }).uniqueProperty('name', intl.formatMessage(messages.discussionTopicNameAlreadyExist)), + }).uniqueObjectProperty('name', intl.formatMessage(messages.discussionTopicNameAlreadyExist)), ), }); @@ -67,19 +57,22 @@ function LegacyConfigForm({ touched, }, ) => { - const { discussionTopics } = values; - const discussionTopicErrors = discussionTopics.map((value, index) => Boolean( - touched.discussionTopics - && touched.discussionTopics[index]?.name - && errors.discussionTopics - && errors?.discussionTopics[index]?.name, + const { discussionTopics, blackoutDates } = values; + const discussionTopicErrors = discussionTopics.map((value, index) => ( + checkFieldErrors(touched, errors, `discussionTopics.${index}`, 'name') )); + const blackoutDatesErrors = blackoutDates.map((value, index) => ( + checkFieldErrors(touched, errors, `blackoutDates.${index}`, 'startDate') + || checkFieldErrors(touched, errors, `blackoutDates.${index}`, 'endDate') + || checkFieldErrors(touched, errors, `blackoutDates.${index}`, 'startTime') + || checkFieldErrors(touched, errors, `blackoutDates.${index}`, 'endTime'))); + const contextValue = { validDiscussionTopics, setValidDiscussionTopics, discussionTopicErrors, - isFormInvalid: discussionTopicErrors.some((error) => error === true) - || Boolean(touched.blackoutDates && errors.blackoutDates), + blackoutDatesErrors, + isFormInvalid: discussionTopicErrors.some((error) => error) || blackoutDatesErrors.some((error) => error), }; return ( @@ -114,7 +107,14 @@ LegacyConfigForm.propTypes = { divideCourseTopicsByCohorts: PropTypes.bool.isRequired, allowAnonymousPosts: PropTypes.bool.isRequired, allowAnonymousPostsPeers: PropTypes.bool.isRequired, - blackoutDates: PropTypes.string.isRequired, + blackoutDates: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.string, + startDate: PropTypes.string, + endDate: PropTypes.string, + startTime: PropTypes.string, + endTime: PropTypes.string, + status: PropTypes.string, + })), discussionTopics: PropTypes.arrayOf(PropTypes.shape({ name: PropTypes.string, id: PropTypes.string, @@ -134,7 +134,7 @@ LegacyConfigForm.defaultProps = { divideCourseTopicsByCohorts: false, allowAnonymousPosts: false, allowAnonymousPostsPeers: false, - blackoutDates: '[]', + blackoutDates: [], }, }; diff --git a/src/pages-and-resources/discussions/app-config-form/apps/legacy/LegacyConfigForm.test.jsx b/src/pages-and-resources/discussions/app-config-form/apps/legacy/LegacyConfigForm.test.jsx index 14c746744..612a64fa2 100644 --- a/src/pages-and-resources/discussions/app-config-form/apps/legacy/LegacyConfigForm.test.jsx +++ b/src/pages-and-resources/discussions/app-config-form/apps/legacy/LegacyConfigForm.test.jsx @@ -3,7 +3,6 @@ import React, { createRef } from 'react'; import { act, fireEvent, - getByText, queryByLabelText, queryByRole, queryByTestId, @@ -43,7 +42,7 @@ const defaultAppConfig = { allowAnonymousPosts: false, allowAnonymousPostsPeers: false, allowDivisionByUnit: false, - blackoutDates: '[]', + blackoutDates: [], }; describe('LegacyConfigForm', () => { let axiosMock; @@ -145,8 +144,7 @@ describe('LegacyConfigForm', () => { ).not.toBeInTheDocument(); // BlackoutDatesField - expect(container.querySelector('#blackoutDates')).toBeInTheDocument(); - expect(container.querySelector('#blackoutDates')).toHaveValue('[]'); + expect(queryByText(container, messages.blackoutDatesLabel.defaultMessage)).toBeInTheDocument(); }); test('folded sub-fields are in the DOM when parents are enabled', async () => { @@ -206,7 +204,7 @@ describe('LegacyConfigForm', () => { const updateTopicName = async (topicId, topicName) => { const topicCard = queryByTestId(container, topicId); - userEvent.click(queryByText(topicCard, 'Expand')); + userEvent.click(queryByLabelText(topicCard, 'Expand')); const topicInput = topicCard.querySelector('input'); topicInput.focus(); await act(async () => { fireEvent.change(topicInput, { target: { value: topicName } }); }); @@ -220,10 +218,7 @@ describe('LegacyConfigForm', () => { if (expectExists) { expect(error).toBeInTheDocument(); } else { expect(error).not.toBeInTheDocument(); } }; - const assertDuplicateTopicNameValidation = async (topicCard, waitForBlur = true, expectExists = true) => { - if (waitForBlur) { - await waitForElementToBeRemoved(queryByText(topicCard, messages.addTopicHelpText.defaultMessage)); - } + const assertDuplicateTopicNameValidation = async (topicCard, expectExists = true) => { const error = queryByText(topicCard, messages.discussionTopicNameAlreadyExist.defaultMessage); if (expectExists) { expect(error).toBeInTheDocument(); } else { expect(error).not.toBeInTheDocument(); } }; @@ -248,7 +243,7 @@ describe('LegacyConfigForm', () => { createComponent(defaultAppConfig); const topicCard = await updateTopicName('13f106c6-6735-4e84-b097-0456cff55960', ''); - const collapseButton = getByText(topicCard, 'Collapse'); + const collapseButton = queryByLabelText(topicCard, 'Collapse'); await act(async () => userEvent.click(collapseButton)); expect(collapseButton).toBeInTheDocument(); diff --git a/src/pages-and-resources/discussions/app-config-form/apps/shared/BlackoutDatesField.jsx b/src/pages-and-resources/discussions/app-config-form/apps/shared/BlackoutDatesField.jsx index b18e16141..6521b174b 100644 --- a/src/pages-and-resources/discussions/app-config-form/apps/shared/BlackoutDatesField.jsx +++ b/src/pages-and-resources/discussions/app-config-form/apps/shared/BlackoutDatesField.jsx @@ -1,118 +1,89 @@ -import React, { useState } from 'react'; +import React, { useCallback } from 'react'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { Form, TransitionReplace } from '@edx/paragon'; -import { useFormikContext } from 'formik'; -import messages from './messages'; +import { Button } from '@edx/paragon'; +import { Add } from '@edx/paragon/icons'; -/** - * Lets break this regex down. - * - * The goal is to accept arrays of dates like the following: - * - * [["2015-09-15", "2015-09-21"], ["2015-10-01T11:45", "2015-10-08"]] - * - * Any date can be YYYY-MM-DDTHH:MM or just YYYY-MM-DD like the above. - * The hours and minutes are optional, as illustrated. - * - * The regex errs on the side of being too loose, so you'll see things that are not perfect. It's - * better to be too liberal than to accidentally reject something that should be fine. - * - * So let multi-line this regex and explain the parts: - * - * Beginning of the string: - * ^ - * The outer square brackets: - * \[ - * Start of a group for a pair of dates with their square brackets: - * (\[ - * A group for the first date (YYYY-MM-DDTHH:MM) with its opening double quote: - * (" - * Any four digits for the YYYY year, and a dash: - * [0-9]{4}- - * MM Months 00 - 12 with either a 0 followed by a digit, 1-9, OR a 1 followed by a digit 0-2 - * Then a dash: - * (0[1-9]|1[0-2])- - * Finally, for days, accepts any digit 0-3 followed by any digit 0-9. Not a very exact regex. - * [0-3][0-9] - * A sub-group for the hours and minutes. T is just part of it: - * (T - * The hours HH are a 0 or 1 followed by 0-9, OR a 2 followed by 0-3. Captures digits 00-23: - * ([0-1][0-9]|2[0-3]) - * Then a colon! - * : - * The minutes MM are any digit 0-5 followed by any digit 0-9, capturing 00-59: - * ([0-5][0-9]) - * The THH:MM is optional, so end the group by allowing it to repeat 0 or 1 times - * ){0,1} - * Now end the first date group with its closing double quote, the group parenthesis, and a comma. - * The comma is a necessary separator between the first and second dates: - * "), - * Now start the second date of the pair: - * (" - * The second date is identical to the first, so here it is in all its glory: - * [0-9]{4}-(0[1-9]|1[0-2])-[0-3][0-9] // YYYY-MM-DD, identical to above - * (T([0-1][0-9]|2[0-3]):([0-5][0-9])){0,1} // THH:MM, identical to above - * Close out the second date with its closing double quotes: - * ") - * Close out the pair of dates with its closing square bracket: - * \] - * An optional comma after the pair of dates, in case there's another pair. If there isn't another - * date, there shouldn't be another comma, but this regex errors on the side of looseness. - * (,){0,1} - * This entire group, ["YYYY-MM-DDTHH:MM", "YYYY-MM-DDTHH:MM"], can be repeated zero or more times. - * )* - * Close out the last square bracket around all the groups: - * \] - * End of string: - * $ - */ -export const blackoutDatesRegex = /^\[(\[("[0-9]{4}-(0[1-9]|1[0-2])-[0-3][0-9](T([0-1][0-9]|2[0-3]):([0-5][0-9])){0,1}"),("[0-9]{4}-(0[1-9]|1[0-2])-[0-3][0-9](T([0-1][0-9]|2[0-3]):([0-5][0-9])){0,1}")\](,){0,1})*\]$/; +import { FieldArray, useFormikContext } from 'formik'; +import { v4 as uuid } from 'uuid'; + +import messages from './messages'; +import BlackoutDatesItem from './blackout-dates/BlackoutDatesItem'; +import { checkStatus } from '../../utils'; +import { denormalizeBlackoutDate } from '../../../data/api'; +import { blackoutDatesStatus as STATUS } from '../../../data/constants'; const BlackoutDatesField = ({ intl }) => { - const [inFocus, setInFocus] = useState(false); const { - handleChange, handleBlur, errors, - touched, values: appConfig, + values: appConfig, + setFieldValue, + errors, + validateForm, } = useFormikContext(); + const { blackoutDates } = appConfig; - const hasError = Boolean(touched.blackoutDates && errors.blackoutDates); + const handleOnClose = useCallback((index) => { + const updatedBlackoutDates = [...blackoutDates]; + updatedBlackoutDates[index] = { + ...updatedBlackoutDates[index], + status: checkStatus(denormalizeBlackoutDate(updatedBlackoutDates[index])), + }; + setFieldValue('blackoutDates', updatedBlackoutDates); + }, [blackoutDates]); - const handleFocusOut = (event) => { - handleBlur(event); - setInFocus(false); + const newBlackoutDateItem = { + id: uuid(), + startDate: '', + startTime: '', + endDate: '', + endTime: '', + status: STATUS.NEW, }; + const onAddNewItem = async (push) => { + await push(newBlackoutDateItem); + validateForm(); + }; return ( <> -
{intl.formatMessage(messages.blackoutDates)}
- - handleFocusOut(event)} - className="mb-1" - floatingLabel={intl.formatMessage(messages.blackoutDatesLabel)} - onFocus={() => setInFocus(true)} - /> - - {hasError && !inFocus ? ( - - -
{errors.blackoutDates}
-
-
- ) : ( - +
+ {intl.formatMessage(messages.blackoutDatesLabel)} +
+ +
+ {intl.formatMessage(messages.blackoutDatesHelp)} +
+
+ ( +
+ {blackoutDates.map((blackoutDate, index) => ( + remove(index)} + onClose={() => handleOnClose(index)} + hasError={Boolean(errors?.blackoutDates?.[index])} + /> + ))} +
+ +
+
)} - - - {intl.formatMessage(messages.blackoutDatesHelp)} - - + /> +
); }; diff --git a/src/pages-and-resources/discussions/app-config-form/apps/shared/blackout-dates/BlackoutDatesInput.jsx b/src/pages-and-resources/discussions/app-config-form/apps/shared/blackout-dates/BlackoutDatesInput.jsx new file mode 100644 index 000000000..531a66da4 --- /dev/null +++ b/src/pages-and-resources/discussions/app-config-form/apps/shared/blackout-dates/BlackoutDatesInput.jsx @@ -0,0 +1,85 @@ +import React, { useState } from 'react'; +import { Form } from '@edx/paragon'; +import { useFormikContext, getIn } from 'formik'; +import PropTypes from 'prop-types'; + +import FieldFeedback from '../../../../../../generic/FieldFeedback'; + +const BlackoutDatesInput = ({ + value, + type, + label, + fieldName, + helpText, + fieldClasses, + feedbackClasses, + formGroupClasses, + fieldNameCommonBase, +}) => { + const { + handleChange, handleBlur, errors, touched, + } = useFormikContext(); + + const [inFocus, setInFocus] = useState(false); + const fieldError = getIn(errors, `${fieldNameCommonBase}.${fieldName}`); + const fieldTouched = getIn(touched, `${fieldNameCommonBase}.${fieldName}`); + const isInvalidInput = Boolean(!inFocus && fieldError && fieldTouched); + + const handleFocusOut = (event) => { + handleBlur(event); + setInFocus(false); + }; + + return ( + + handleFocusOut(event)} + onFocus={() => setInFocus(true)} + /> + + + + ); +}; + +BlackoutDatesInput.propTypes = { + value: PropTypes.string.isRequired, + fieldName: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, + helpText: PropTypes.string, + feedbackClasses: PropTypes.string, + fieldClasses: PropTypes.string, + formGroupClasses: PropTypes.string, + fieldNameCommonBase: PropTypes.string.isRequired, +}; + +BlackoutDatesInput.defaultProps = { + fieldClasses: '', + helpText: '', + feedbackClasses: '', + formGroupClasses: '', +}; + +export default BlackoutDatesInput; diff --git a/src/pages-and-resources/discussions/app-config-form/apps/shared/blackout-dates/BlackoutDatesItem.jsx b/src/pages-and-resources/discussions/app-config-form/apps/shared/blackout-dates/BlackoutDatesItem.jsx new file mode 100644 index 000000000..6d6c96fe5 --- /dev/null +++ b/src/pages-and-resources/discussions/app-config-form/apps/shared/blackout-dates/BlackoutDatesItem.jsx @@ -0,0 +1,158 @@ +import React, { useEffect, useState } from 'react'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { Form } from '@edx/paragon'; +import { useFormikContext } from 'formik'; +import PropTypes from 'prop-types'; +import _ from 'lodash'; + +import messages from '../messages'; +import BlackoutDatesInput from './BlackoutDatesInput'; +import { formatBlackoutDates } from '../../../utils'; +import { + blackoutDatesStatus as constants, + deleteHelperText, + badgeVariant, +} from '../../../../data/constants'; +import CollapsableEditor from '../../../../../../generic/CollapsableEditor'; +import DeletePopup from '../../../../../../generic/DeletePopup'; +import CollapseCardHeading from './CollapseCardHeading'; + +const BlackoutDatesItem = ({ + intl, + blackoutDate, + onDelete, + hasError, + onClose, + fieldNameCommonBase, +}) => { + const [showDeletePopup, setShowDeletePopup] = useState(false); + const [collapseIsOpen, setCollapseOpen] = useState(hasError); + const { setFieldTouched } = useFormikContext(); + + const handleToggle = (isOpen) => { + if (!isOpen && hasError) { + return setCollapseOpen(true); + } + return setCollapseOpen(isOpen); + }; + + useEffect(() => { + setCollapseOpen(hasError); + }, [hasError]); + + const getHeading = (isOpen) => ( + + ); + + if (showDeletePopup) { + return ( + setShowDeletePopup(false)} + cancelLabel={intl.formatMessage(messages.cancelButton)} + /> + ); + } + + const handleOnClose = () => { + ['startDate', 'startTime', 'endDate', 'endTime'].forEach(field => ( + setFieldTouched(`${fieldNameCommonBase}.${field}`, true) + )); + if (!hasError) { + onClose(); + } + }; + + return ( + setShowDeletePopup(true)} + expandAlt={intl.formatMessage(messages.expandAltText)} + collapseAlt={intl.formatMessage(messages.collapseAltText)} + deleteAlt={intl.formatMessage(messages.deleteAltText)} + data-testid={blackoutDate.id} + onClose={() => handleOnClose()} + > + + + + +
+ + + + +
+ ); +}; + +BlackoutDatesItem.propTypes = { + intl: intlShape.isRequired, + onDelete: PropTypes.func.isRequired, + hasError: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + fieldNameCommonBase: PropTypes.string.isRequired, + blackoutDate: PropTypes.shape({ + id: PropTypes.string, + startDate: PropTypes.string, + endDate: PropTypes.string, + startTime: PropTypes.string, + endTime: PropTypes.string, + status: PropTypes.string, + }).isRequired, +}; + +export default injectIntl(BlackoutDatesItem); diff --git a/src/pages-and-resources/discussions/app-config-form/apps/shared/blackout-dates/CollapseCardHeading.jsx b/src/pages-and-resources/discussions/app-config-form/apps/shared/blackout-dates/CollapseCardHeading.jsx new file mode 100644 index 000000000..23649f61d --- /dev/null +++ b/src/pages-and-resources/discussions/app-config-form/apps/shared/blackout-dates/CollapseCardHeading.jsx @@ -0,0 +1,37 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Badge } from '@edx/paragon'; + +const CollapseCardHeading = ({ + isOpen, + expandHeadingText, + collapseHeadingText, + badgeVariant, + badgeStatus, +}) => { + if (isOpen) { + return {expandHeadingText}; + } + + return ( +
+ {badgeStatus && {badgeStatus}} +
{collapseHeadingText}
+
+ ); +}; + +CollapseCardHeading.propTypes = { + isOpen: PropTypes.bool.isRequired, + collapseHeadingText: PropTypes.string.isRequired, + expandHeadingText: PropTypes.string.isRequired, + badgeVariant: PropTypes.string, + badgeStatus: PropTypes.string, +}; + +CollapseCardHeading.defaultProps = { + badgeVariant: 'primary', + badgeStatus: '', +}; + +export default CollapseCardHeading; diff --git a/src/pages-and-resources/discussions/app-config-form/apps/shared/discussion-topics/DiscussionTopics.jsx b/src/pages-and-resources/discussions/app-config-form/apps/shared/discussion-topics/DiscussionTopics.jsx index a17337f7b..1185c7273 100644 --- a/src/pages-and-resources/discussions/app-config-form/apps/shared/discussion-topics/DiscussionTopics.jsx +++ b/src/pages-and-resources/discussions/app-config-form/apps/shared/discussion-topics/DiscussionTopics.jsx @@ -8,7 +8,7 @@ import _ from 'lodash'; import messages from '../messages'; import TopicItem from './TopicItem'; import { LegacyConfigFormContext } from '../../legacy/LegacyConfigFormProvider'; -import filterItemFromObject from '../../../utils'; +import { filterItemFromObject } from '../../../utils'; const DiscussionTopics = ({ intl }) => { const { @@ -53,7 +53,7 @@ const DiscussionTopics = ({ intl }) => {
{intl.formatMessage(messages.discussionTopics)}
-