feat: add blackout dates new UI and validation (#191)
* feat: implement blackout dates UI and validation * chore: bump paragon version to 16.14.2 * refactor: blackout dates collapsible card with reusable collapsible component Co-authored-by: Mehak Nasir <mehaknasir94@gmail.com>
This commit is contained in:
181
package-lock.json
generated
181
package-lock.json
generated
@@ -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=="
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -9,6 +9,7 @@ const CollapsableEditor = ({
|
||||
open,
|
||||
defaultOpen,
|
||||
onToggle,
|
||||
onClose,
|
||||
onDelete,
|
||||
children,
|
||||
expandAlt,
|
||||
@@ -19,6 +20,7 @@ const CollapsableEditor = ({
|
||||
<Collapsible.Advanced
|
||||
className="collapsible-card rounded mb-3 px-3 py-2"
|
||||
onToggle={onToggle}
|
||||
onClose={onClose}
|
||||
defaultOpen={defaultOpen}
|
||||
open={open}
|
||||
{...props}
|
||||
@@ -31,9 +33,11 @@ const CollapsableEditor = ({
|
||||
{title}
|
||||
</div>
|
||||
<Collapsible.Visible whenClosed>
|
||||
<Icon
|
||||
screenReaderText={expandAlt}
|
||||
<IconButton
|
||||
alt={expandAlt}
|
||||
src={ExpandMore}
|
||||
iconAs={Icon}
|
||||
onClick={() => {}}
|
||||
variant="dark"
|
||||
/>
|
||||
</Collapsible.Visible>
|
||||
@@ -53,9 +57,11 @@ const CollapsableEditor = ({
|
||||
</div>
|
||||
)}
|
||||
<div className="pl-4">
|
||||
<Icon
|
||||
screenReaderText={collapseAlt}
|
||||
<IconButton
|
||||
alt={collapseAlt}
|
||||
src={ExpandLess}
|
||||
iconAs={Icon}
|
||||
onClick={() => {}}
|
||||
variant="dark"
|
||||
/>
|
||||
</div>
|
||||
@@ -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;
|
||||
|
||||
38
src/generic/DeletePopup.jsx
Normal file
38
src/generic/DeletePopup.jsx
Normal file
@@ -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,
|
||||
}) => (
|
||||
<Card className="rounded mb-3 p-1">
|
||||
<Card.Body>
|
||||
<div className="text-primary-500 mb-2 h4">{label}</div>
|
||||
<Card.Text className="text-justify text-muted">{bodyText}</Card.Text>
|
||||
<div className="d-flex justify-content-end">
|
||||
<Button variant="tertiary" onClick={onCancel}>
|
||||
{cancelLabel}
|
||||
</Button>
|
||||
<Button variant="outline-brand" className="ml-2" onClick={onDelete}>
|
||||
{deleteLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
);
|
||||
|
||||
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;
|
||||
44
src/generic/FieldFeedback.jsx
Normal file
44
src/generic/FieldFeedback.jsx
Normal file
@@ -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 (
|
||||
<TransitionReplace className={transitionClasses}>
|
||||
{renderCondition ? (
|
||||
<React.Fragment key="open">
|
||||
<Form.Control.Feedback type={type} hasIcon={hasIcon} key={`${feedback}-feedback`}>
|
||||
<div className={`small ${feedbackClasses}`}>{feedback}</div>
|
||||
</Form.Control.Feedback>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment key="close" />
|
||||
)}
|
||||
</TransitionReplace>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -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: [],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<h5 className="my-4 text-gray-500">{intl.formatMessage(messages.blackoutDates)}</h5>
|
||||
<Form.Group
|
||||
controlId="blackoutDates"
|
||||
isInvalid={hasError && !inFocus}
|
||||
className="m-2"
|
||||
>
|
||||
<Form.Control
|
||||
value={appConfig.blackoutDates}
|
||||
onChange={handleChange}
|
||||
onBlur={(event) => handleFocusOut(event)}
|
||||
className="mb-1"
|
||||
floatingLabel={intl.formatMessage(messages.blackoutDatesLabel)}
|
||||
onFocus={() => setInFocus(true)}
|
||||
/>
|
||||
<TransitionReplace key="blackoutDates">
|
||||
{hasError && !inFocus ? (
|
||||
<React.Fragment key="open">
|
||||
<Form.Control.Feedback type="invalid" hasIcon={false}>
|
||||
<div className="small">{errors.blackoutDates}</div>
|
||||
</Form.Control.Feedback>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment key="closed" />
|
||||
<h5 className="text-gray-500 mt-4 mb-2">
|
||||
{intl.formatMessage(messages.blackoutDatesLabel)}
|
||||
</h5>
|
||||
<label className="text-primary-500 mb-1 h4">
|
||||
{intl.formatMessage(messages.blackoutDates)}
|
||||
</label>
|
||||
<div className="small mb-4 text-muted">
|
||||
{intl.formatMessage(messages.blackoutDatesHelp)}
|
||||
</div>
|
||||
<div>
|
||||
<FieldArray
|
||||
name="blackoutDates"
|
||||
render={({ push, remove }) => (
|
||||
<div>
|
||||
{blackoutDates.map((blackoutDate, index) => (
|
||||
<BlackoutDatesItem
|
||||
fieldNameCommonBase={`blackoutDates.${index}`}
|
||||
blackoutDate={blackoutDate}
|
||||
key={`date-${blackoutDate.id}`}
|
||||
id={blackoutDate.id}
|
||||
onDelete={() => remove(index)}
|
||||
onClose={() => handleOnClose(index)}
|
||||
hasError={Boolean(errors?.blackoutDates?.[index])}
|
||||
/>
|
||||
))}
|
||||
<div className="mb-4">
|
||||
<Button
|
||||
onClick={() => onAddNewItem(push)}
|
||||
variant="link"
|
||||
iconBefore={Add}
|
||||
className="text-primary-500 p-0"
|
||||
>
|
||||
{intl.formatMessage(messages.addBlackoutDatesButton)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</TransitionReplace>
|
||||
<Form.Text muted className="mt-3">
|
||||
{intl.formatMessage(messages.blackoutDatesHelp)}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<Form.Group
|
||||
controlId={`${fieldNameCommonBase}.${fieldName}`}
|
||||
className={`col ${formGroupClasses}`}
|
||||
isInvalid={isInvalidInput}
|
||||
>
|
||||
<Form.Control
|
||||
name={`${fieldNameCommonBase}.${fieldName}`}
|
||||
value={value}
|
||||
type={type}
|
||||
onChange={handleChange}
|
||||
floatingLabel={label}
|
||||
className={fieldClasses}
|
||||
onBlur={(event) => handleFocusOut(event)}
|
||||
onFocus={() => setInFocus(true)}
|
||||
/>
|
||||
<FieldFeedback
|
||||
renderCondition={inFocus}
|
||||
feedback={helpText}
|
||||
transitionClasses="mt-1"
|
||||
feedbackClasses={feedbackClasses}
|
||||
/>
|
||||
<FieldFeedback
|
||||
renderCondition={isInvalidInput}
|
||||
feedback={fieldError || ''}
|
||||
type="invalid"
|
||||
transitionClasses="mt-1"
|
||||
feedbackClasses={feedbackClasses}
|
||||
/>
|
||||
</Form.Group>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -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) => (
|
||||
<CollapseCardHeading
|
||||
isOpen={isOpen}
|
||||
expandHeadingText={intl.formatMessage(messages.configureBlackoutDates)}
|
||||
collapseHeadingText={formatBlackoutDates(blackoutDate)}
|
||||
badgeVariant={badgeVariant[blackoutDate.status]}
|
||||
badgeStatus={intl.formatMessage(messages.blackoutDatesStatus, {
|
||||
status: _.startCase(_.toLower(blackoutDate.status)),
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
||||
if (showDeletePopup) {
|
||||
return (
|
||||
<DeletePopup
|
||||
label={blackoutDate.status === constants.ACTIVE
|
||||
? intl.formatMessage(messages.activeBlackoutDatesDeletionLabel)
|
||||
: intl.formatMessage(messages.blackoutDatesDeletionLabel)}
|
||||
bodyText={intl.formatMessage(deleteHelperText[blackoutDate.status])}
|
||||
onDelete={onDelete}
|
||||
deleteLabel={intl.formatMessage(messages.deleteButton)}
|
||||
onCancel={() => setShowDeletePopup(false)}
|
||||
cancelLabel={intl.formatMessage(messages.cancelButton)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const handleOnClose = () => {
|
||||
['startDate', 'startTime', 'endDate', 'endTime'].forEach(field => (
|
||||
setFieldTouched(`${fieldNameCommonBase}.${field}`, true)
|
||||
));
|
||||
if (!hasError) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<CollapsableEditor
|
||||
open={collapseIsOpen}
|
||||
onToggle={handleToggle}
|
||||
title={getHeading(collapseIsOpen)}
|
||||
onDelete={() => setShowDeletePopup(true)}
|
||||
expandAlt={intl.formatMessage(messages.expandAltText)}
|
||||
collapseAlt={intl.formatMessage(messages.collapseAltText)}
|
||||
deleteAlt={intl.formatMessage(messages.deleteAltText)}
|
||||
data-testid={blackoutDate.id}
|
||||
onClose={() => handleOnClose()}
|
||||
>
|
||||
<Form.Row className="mx-2 pt-3">
|
||||
<BlackoutDatesInput
|
||||
value={blackoutDate.startDate}
|
||||
type="date"
|
||||
label={intl.formatMessage(messages.startDateLabel)}
|
||||
helpText={intl.formatMessage(messages.blackoutStartDateHelp)}
|
||||
fieldName="startDate"
|
||||
formGroupClasses="pl-md-0"
|
||||
fieldClasses="pr-md-2"
|
||||
fieldNameCommonBase={fieldNameCommonBase}
|
||||
/>
|
||||
<BlackoutDatesInput
|
||||
value={blackoutDate.startTime}
|
||||
type="time"
|
||||
label={intl.formatMessage(messages.startTimeLabel)}
|
||||
helpText={intl.formatMessage(messages.blackoutStartTimeHelp)}
|
||||
fieldName="startTime"
|
||||
formGroupClasses="pr-md-0"
|
||||
fieldClasses="ml-md-2"
|
||||
feedbackClasses="ml-md-2"
|
||||
fieldNameCommonBase={fieldNameCommonBase}
|
||||
/>
|
||||
</Form.Row>
|
||||
<hr className="mx-2 my-2 border-light-400" />
|
||||
<Form.Row className="mx-2 pt-4">
|
||||
<BlackoutDatesInput
|
||||
value={blackoutDate.endDate}
|
||||
type="date"
|
||||
label={intl.formatMessage(messages.endDateLabel)}
|
||||
helpText={intl.formatMessage(messages.blackoutEndDateHelp)}
|
||||
fieldName="endDate"
|
||||
formGroupClasses="pl-md-0"
|
||||
fieldClasses="pr-md-2"
|
||||
fieldNameCommonBase={fieldNameCommonBase}
|
||||
/>
|
||||
<BlackoutDatesInput
|
||||
value={blackoutDate.endTime}
|
||||
type="time"
|
||||
label={intl.formatMessage(messages.endTimeLabel)}
|
||||
helpText={intl.formatMessage(messages.blackoutEndTimeHelp)}
|
||||
fieldName="endTime"
|
||||
formGroupClasses="pr-md-0"
|
||||
fieldClasses="ml-md-2"
|
||||
feedbackClasses="ml-md-2"
|
||||
fieldNameCommonBase={fieldNameCommonBase}
|
||||
/>
|
||||
</Form.Row>
|
||||
</CollapsableEditor>
|
||||
);
|
||||
};
|
||||
|
||||
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);
|
||||
@@ -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 <span className="h4 py-2 mr-auto">{expandHeadingText}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-2">
|
||||
{badgeStatus && <Badge variant={badgeVariant}>{badgeStatus}</Badge>}
|
||||
<div>{collapseHeadingText}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -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 }) => {
|
||||
<h5 className="text-gray-500 mt-4 mb-2">
|
||||
{intl.formatMessage(messages.discussionTopics)}
|
||||
</h5>
|
||||
<label className="text-primary-500 mb-2 h4">
|
||||
<label className="text-primary-500 mb-1 h4">
|
||||
{intl.formatMessage(messages.discussionTopicsLabel)}
|
||||
</label>
|
||||
<div className="small mb-4 text-muted">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
act, fireEvent, queryAllByTestId, queryByTestId, queryByText, render, waitFor,
|
||||
act, fireEvent, queryAllByTestId, queryByTestId, queryByText, render, waitFor, queryByLabelText,
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
@@ -33,7 +33,7 @@ const appConfig = {
|
||||
allowAnonymousPosts: false,
|
||||
allowAnonymousPostsPeers: false,
|
||||
allowDivisionByUnit: false,
|
||||
blackoutDates: '[]',
|
||||
blackoutDates: [],
|
||||
};
|
||||
|
||||
const contextValue = {
|
||||
@@ -134,11 +134,11 @@ describe('DiscussionTopics', () => {
|
||||
createComponent(appConfig);
|
||||
const topicCard = queryByTestId(container, '13f106c6-6735-4e84-b097-0456cff55960');
|
||||
|
||||
await act(async () => userEvent.click(queryByText(topicCard, 'Expand')));
|
||||
await act(async () => userEvent.click(queryByLabelText(topicCard, 'Expand')));
|
||||
await act(async () => {
|
||||
fireEvent.change(topicCard.querySelector('input'), { target: { value: 'new name' } });
|
||||
});
|
||||
await act(async () => userEvent.click(queryByText(topicCard, 'Collapse')));
|
||||
await act(async () => userEvent.click(queryByLabelText(topicCard, 'Collapse')));
|
||||
|
||||
expect(queryByText(topicCard, 'new name')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -5,11 +5,12 @@ import { useFormikContext } from 'formik';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button, Card, Form, TransitionReplace,
|
||||
Button, Card, Form,
|
||||
} from '@edx/paragon';
|
||||
|
||||
import CollapsableEditor from '../../../../../../generic/CollapsableEditor';
|
||||
import messages from '../messages';
|
||||
import FieldFeedback from '../../../../../../generic/FieldFeedback';
|
||||
|
||||
const TopicItem = ({
|
||||
intl,
|
||||
@@ -63,12 +64,6 @@ const TopicItem = ({
|
||||
setShowDeletePopup(true);
|
||||
};
|
||||
|
||||
const renderFormFeedback = (message, messageType = 'default') => (
|
||||
<Form.Control.Feedback type={messageType} hasIcon={false}>
|
||||
<div className="small">{message}</div>
|
||||
</Form.Control.Feedback>
|
||||
);
|
||||
|
||||
const handleFocusOut = (event) => {
|
||||
handleBlur(event);
|
||||
setInFocus(false);
|
||||
@@ -125,24 +120,17 @@ const TopicItem = ({
|
||||
controlClassName="bg-white"
|
||||
onFocus={() => setInFocus(true)}
|
||||
/>
|
||||
<TransitionReplace key={id} className="mt-1">
|
||||
{inFocus ? (
|
||||
<React.Fragment key="open">
|
||||
{renderFormFeedback(intl.formatMessage(messages.addTopicHelpText))}
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment key="closed" />
|
||||
)}
|
||||
</TransitionReplace>
|
||||
<TransitionReplace key={`${name}-${id}`}>
|
||||
{hasError && !inFocus ? (
|
||||
<React.Fragment key="open">
|
||||
{renderFormFeedback(errors?.discussionTopics[index].name, 'invalid')}
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment key="closed" />
|
||||
)}
|
||||
</TransitionReplace>
|
||||
<FieldFeedback
|
||||
renderCondition={inFocus}
|
||||
feedback={intl.formatMessage(messages.addTopicHelpText)}
|
||||
transitionClasses="mt-1"
|
||||
/>
|
||||
<FieldFeedback
|
||||
renderCondition={hasError && !inFocus}
|
||||
feedback={errors?.discussionTopics?.[index]?.name || ''}
|
||||
type="invalid"
|
||||
transitionClasses="mt-1"
|
||||
/>
|
||||
</Form.Group>
|
||||
</CollapsableEditor>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
queryByRole,
|
||||
queryByTestId,
|
||||
queryByText,
|
||||
queryByLabelText,
|
||||
render,
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
@@ -106,8 +107,8 @@ describe('TopicItem', () => {
|
||||
createComponent(generalTopic);
|
||||
|
||||
const generalTopicNode = queryByTestId(container, 'course');
|
||||
expect(queryByText(generalTopicNode, 'Expand')).toBeInTheDocument();
|
||||
expect(queryByText(generalTopicNode, 'Collapse')).not.toBeInTheDocument();
|
||||
expect(queryByLabelText(generalTopicNode, 'Expand')).toBeInTheDocument();
|
||||
expect(queryByLabelText(generalTopicNode, 'Collapse')).not.toBeInTheDocument();
|
||||
expect(queryByText(generalTopicNode, 'General')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -116,10 +117,10 @@ describe('TopicItem', () => {
|
||||
createComponent(generalTopic);
|
||||
|
||||
const generalTopicNode = queryByTestId(container, 'course');
|
||||
userEvent.click(queryByText(generalTopicNode, 'Expand'));
|
||||
userEvent.click(queryByLabelText(generalTopicNode, 'Expand'));
|
||||
|
||||
expect(queryByText(generalTopicNode, 'Expand')).not.toBeInTheDocument();
|
||||
expect(queryByText(generalTopicNode, 'Collapse')).toBeInTheDocument();
|
||||
expect(queryByLabelText(generalTopicNode, 'Expand')).not.toBeInTheDocument();
|
||||
expect(queryByLabelText(generalTopicNode, 'Collapse')).toBeInTheDocument();
|
||||
expect(queryByRole(generalTopicNode, 'button', { name: 'Delete Topic' })).not.toBeInTheDocument();
|
||||
expect(generalTopicNode.querySelector('input')).toBeInTheDocument();
|
||||
});
|
||||
@@ -129,10 +130,10 @@ describe('TopicItem', () => {
|
||||
createComponent(additionalTopic);
|
||||
|
||||
const topicCard = queryByTestId(container, '13f106c6-6735-4e84-b097-0456cff55960');
|
||||
userEvent.click(queryByText(topicCard, 'Expand'));
|
||||
userEvent.click(queryByLabelText(topicCard, 'Expand'));
|
||||
|
||||
expect(queryByText(topicCard, 'Expand')).not.toBeInTheDocument();
|
||||
expect(queryByText(topicCard, 'Collapse')).toBeInTheDocument();
|
||||
expect(queryByLabelText(topicCard, 'Expand')).not.toBeInTheDocument();
|
||||
expect(queryByLabelText(topicCard, 'Collapse')).toBeInTheDocument();
|
||||
expect(queryByRole(topicCard, 'button', { name: 'Delete Topic' })).toBeInTheDocument();
|
||||
expect(topicCard.querySelector('input')).toBeInTheDocument();
|
||||
});
|
||||
@@ -142,7 +143,7 @@ describe('TopicItem', () => {
|
||||
createComponent(additionalTopic);
|
||||
|
||||
const topicCard = queryByTestId(container, '13f106c6-6735-4e84-b097-0456cff55960');
|
||||
userEvent.click(queryByText(topicCard, 'Expand'));
|
||||
userEvent.click(queryByLabelText(topicCard, 'Expand'));
|
||||
userEvent.click(queryByRole(topicCard, 'button', { name: 'Delete Topic' }));
|
||||
|
||||
expect(queryAllByText(container, messages.discussionTopicDeletionLabel.defaultMessage)).toHaveLength(1);
|
||||
@@ -158,7 +159,7 @@ describe('TopicItem', () => {
|
||||
createComponent(additionalTopic);
|
||||
|
||||
const topicCard = queryByTestId(container, '13f106c6-6735-4e84-b097-0456cff55960');
|
||||
userEvent.click(queryByText(topicCard, 'Expand'));
|
||||
userEvent.click(queryByLabelText(topicCard, 'Expand'));
|
||||
topicCard.querySelector('input').focus();
|
||||
|
||||
expect(queryByText(topicCard, messages.addTopicHelpText.defaultMessage)).toBeInTheDocument();
|
||||
|
||||
@@ -179,17 +179,96 @@ const messages = defineMessages({
|
||||
},
|
||||
blackoutDatesHelp: {
|
||||
id: 'authoring.discussions.builtIn.blackoutDates.help',
|
||||
defaultMessage:
|
||||
`Enter pairs of dates between which students cannot post to discussion forums. Inside the provided
|
||||
brackets, enter an additional set of square brackets surrounding each pair of dates you add.
|
||||
Format each pair of dates as ["YYYY-MM-DD", "YYYY-MM-DD"]. To specify times as well as dates,
|
||||
format each pair as ["YYYY-MM-DDTHH:MM", "YYYY-MM-DDTHH:MM"]. Be sure to include the "T" between
|
||||
the date and time. For example, an entry defining two blackout periods looks like this, including
|
||||
the outer pair of square brackets: [["2015-09-15", "2015-09-21"], ["2015-10-01", "2015-10-08"]]`,
|
||||
defaultMessage: 'If added, learners will not be able to post in discussions between these dates.',
|
||||
},
|
||||
blackoutDatesFormattingError: {
|
||||
id: 'authoring.discussions.builtIn.blackoutDates.formattingError',
|
||||
defaultMessage: "There's a formatting error in your blackout dates.",
|
||||
addBlackoutDatesButton: {
|
||||
id: 'authoring.discussions.addBlackoutDatesButton',
|
||||
defaultMessage: 'Add blackout date range',
|
||||
description: 'Button label when Add a new blackout date.',
|
||||
},
|
||||
configureBlackoutDates: {
|
||||
id: 'authoring.discussions.builtIn.configureBlackoutDates.label',
|
||||
defaultMessage: 'Configure blackout date range',
|
||||
description: 'Label for blockout dates allowing user to configure blackout dates',
|
||||
},
|
||||
blackoutStartDateHelp: {
|
||||
id: 'authoring.discussions.blackoutStartDate.help',
|
||||
defaultMessage: 'Enter a start date, e.g. 12/10/2023',
|
||||
},
|
||||
blackoutEndDateHelp: {
|
||||
id: 'authoring.discussions.blackoutEndDate.help',
|
||||
defaultMessage: 'Enter an end date, e.g. 12/17/2023',
|
||||
},
|
||||
blackoutStartTimeHelp: {
|
||||
id: 'authoring.discussions.blackoutStartTime.help',
|
||||
defaultMessage: 'Enter a start time, e.g. 09:00 AM',
|
||||
},
|
||||
blackoutEndTimeHelp: {
|
||||
id: 'authoring.discussions.blackoutEndTime.help',
|
||||
defaultMessage: 'Enter an end time, e.g. 05:00 PM',
|
||||
},
|
||||
activeBlackoutDatesDeletionHelp: {
|
||||
id: 'authoring.discussions.activeBlackoutDatesDeletion.help',
|
||||
defaultMessage: 'These blackout dates are currently active. If deleted, learners will be able to post in discussions during these dates. Are you sure you want to proceed?',
|
||||
description: 'Help text for delete a active blackout dates from blackout dates section.',
|
||||
},
|
||||
blackoutDatesDeletionHelp: {
|
||||
id: 'authoring.discussions.blackoutDatesDeletion.help',
|
||||
defaultMessage: 'If deleted, learners will be able to post in discussions during these dates.',
|
||||
description: 'Help text for delete a upcoming blackout dates from blackout dates section.',
|
||||
},
|
||||
completeBlackoutDatesDeletionHelp: {
|
||||
id: 'authoring.discussions.completeBlackoutDatesDeletion.help',
|
||||
defaultMessage: 'Are you sure you want to delete these blackout dates?',
|
||||
description: 'Help text for delete a complete blackout dates from blackout dates section.',
|
||||
},
|
||||
activeBlackoutDatesDeletionLabel: {
|
||||
id: 'authoring.discussions.activeBlackoutDatesDeletion.label',
|
||||
defaultMessage: 'Delete active blackout dates?',
|
||||
description: 'Label for active blackout dates delete popup allowing a user to delete a blackout date range.',
|
||||
},
|
||||
blackoutDatesDeletionLabel: {
|
||||
id: 'authoring.discussions.blackoutDatesDeletion.label',
|
||||
defaultMessage: 'Delete blackout dates?',
|
||||
description: 'Label for blackout dates delete popup allowing a user to delete a blackout date range.',
|
||||
},
|
||||
deleteBlackoutDatesAltText: {
|
||||
id: 'authoring.blackoutDates.delete',
|
||||
defaultMessage: 'Delete Blackout Dates',
|
||||
},
|
||||
blackoutDatesStatus: {
|
||||
id: 'authoring.blackoutDates.status',
|
||||
defaultMessage: '{status}',
|
||||
},
|
||||
blackoutStartDateRequired: {
|
||||
id: 'authoring.blackoutDates.startDate.required',
|
||||
defaultMessage: 'Start date is a required field',
|
||||
description: 'Tells the user that the blackout dates must have start date and it is required.',
|
||||
},
|
||||
blackoutEndDateRequired: {
|
||||
id: 'authoring.blackoutDates.endDate.required',
|
||||
defaultMessage: 'End date is a required field',
|
||||
description: 'Tells the user that the blackout dates must have end date and it is required.',
|
||||
},
|
||||
blackoutStartDateInPast: {
|
||||
id: 'authoring.blackoutDates.startDate.inPast',
|
||||
defaultMessage: 'Start date cannot be after end date',
|
||||
description: 'Tells the user that the blackout start date cannot be in past and cannot be after end date',
|
||||
},
|
||||
blackoutEndDateInPast: {
|
||||
id: 'authoring.blackoutDates.endDate.inPast',
|
||||
defaultMessage: 'End date cannot be before start date',
|
||||
description: 'Tells the user that the blackout end date cannot be in past and cannot be before start date',
|
||||
},
|
||||
blackoutStartTimeInPast: {
|
||||
id: 'authoring.blackoutDates.startTime.inPast',
|
||||
defaultMessage: 'Start time cannot be after end time',
|
||||
description: 'Tells the user that the blackout start time cannot be in past and cannot be after end time',
|
||||
},
|
||||
blackoutEndTimeInPast: {
|
||||
id: 'authoring.blackoutDates.endTime.inPast',
|
||||
defaultMessage: 'End time cannot be before start time',
|
||||
description: 'Tells the user that the blackout end time cannot be in past and cannot be before start time',
|
||||
},
|
||||
deleteAltText: {
|
||||
id: 'authoring.topics.delete',
|
||||
@@ -203,6 +282,26 @@ const messages = defineMessages({
|
||||
id: 'authoring.topics.collapse',
|
||||
defaultMessage: 'Collapse',
|
||||
},
|
||||
startDateLabel: {
|
||||
id: 'authoring.blackoutDates.start.date',
|
||||
defaultMessage: 'Start date',
|
||||
description: 'Label for start date field',
|
||||
},
|
||||
startTimeLabel: {
|
||||
id: 'authoring.blackoutDates.start.time',
|
||||
defaultMessage: 'Start time (optional)',
|
||||
description: 'label for start time field',
|
||||
},
|
||||
endDateLabel: {
|
||||
id: 'authoring.blackoutDates.end.date',
|
||||
defaultMessage: 'End date',
|
||||
description: 'label for end date field',
|
||||
},
|
||||
endTimeLabel: {
|
||||
id: 'authoring.blackoutDates.end.time',
|
||||
defaultMessage: 'End time (optional)',
|
||||
description: 'label for end time field',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1,5 +1,82 @@
|
||||
const filterItemFromObject = (array, key, value) => (
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
import { getIn } from 'formik';
|
||||
|
||||
import { blackoutDatesStatus as constants } from '../data/constants';
|
||||
|
||||
export const filterItemFromObject = (array, key, value) => (
|
||||
array.filter(item => item[key] !== value)
|
||||
);
|
||||
|
||||
export default filterItemFromObject;
|
||||
export const checkFieldErrors = (touched, errors, fieldPath, propertyName) => Boolean(
|
||||
getIn(errors, `${fieldPath}.${propertyName}`) && getIn(touched, `${fieldPath}.${propertyName}`),
|
||||
);
|
||||
|
||||
export const errorExists = (errors, fieldPath, propertyName) => getIn(errors, `${fieldPath}.${propertyName}`);
|
||||
|
||||
export const checkStatus = ([startDate, endDate]) => {
|
||||
const today = moment(); let status;
|
||||
|
||||
if (moment(endDate).isBefore(today, 'days')) {
|
||||
status = constants.COMPLETE;
|
||||
} else if (moment(startDate).isAfter(today, 'days')) {
|
||||
status = constants.UPCOMING;
|
||||
} else {
|
||||
status = constants.ACTIVE;
|
||||
}
|
||||
return status;
|
||||
};
|
||||
|
||||
export const formatDate = (date, time) => (time ? `${date}T${time}` : date);
|
||||
export const isSameDay = (startDate, endDate) => moment(startDate).isSame(endDate, 'day');
|
||||
export const isSameMonth = (startDate, endDate) => moment(startDate).isSame(endDate, 'month');
|
||||
export const isSameYear = (startDate, endDate) => moment(startDate).isSame(endDate, 'year');
|
||||
|
||||
export const sortBlackoutDatesByStatus = (data, status, order) => (
|
||||
_.orderBy(data.filter(date => date.status === status),
|
||||
[(obj) => moment(formatDate(obj.startDate, obj.startTime))], [order])
|
||||
);
|
||||
|
||||
export const formatBlackoutDates = ({
|
||||
startDate, startTime, endDate, endTime,
|
||||
}) => {
|
||||
let formattedDate;
|
||||
const hasSameDay = isSameDay(startDate, endDate);
|
||||
const hasSameMonth = isSameMonth(startDate, endDate);
|
||||
const hasSameYear = isSameYear(startDate, endDate);
|
||||
const isTimeAvailable = Boolean(startTime && endTime);
|
||||
const mStartDate = moment(startDate);
|
||||
const mEndDate = moment(endDate);
|
||||
const mStartDateTime = moment(`${startDate}T${startTime}`);
|
||||
const mEndDateTime = moment(`${endDate}T${endTime}`);
|
||||
|
||||
if (hasSameDay && !isTimeAvailable) {
|
||||
formattedDate = mStartDate.format('MMMM D, YYYY');
|
||||
} else if (hasSameDay && isTimeAvailable) {
|
||||
formattedDate = `
|
||||
${mStartDateTime.format('MMMM D, YYYY, h:mma')} -
|
||||
${mEndDateTime.format('h:mma')}
|
||||
`;
|
||||
} else if (hasSameMonth && !isTimeAvailable) {
|
||||
formattedDate = `
|
||||
${mStartDate.format('MMMM D')} -
|
||||
${mEndDate.format('D, YYYY')}
|
||||
`;
|
||||
} else if (hasSameMonth && isTimeAvailable) {
|
||||
formattedDate = `
|
||||
${mStartDateTime.format('MMMM D, YYYY, h:mma')} -
|
||||
${mEndDateTime.format('MMMM D, YYYY, h:mma')}
|
||||
`;
|
||||
} else if (!hasSameMonth && hasSameYear) {
|
||||
formattedDate = `
|
||||
${mStartDate.format('MMMM D')} -
|
||||
${mEndDate.format('MMMM D, YYYY')}
|
||||
`;
|
||||
} else if (!hasSameMonth && !hasSameYear) {
|
||||
formattedDate = `
|
||||
${mStartDate.format('MMMM D, YYYY')} -
|
||||
${mEndDate.format('MMMM D, YYYY')}
|
||||
`;
|
||||
}
|
||||
return formattedDate;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { checkStatus, sortBlackoutDatesByStatus, formatDate } from '../app-config-form/utils';
|
||||
import { blackoutDatesStatus as constants } from './constants';
|
||||
|
||||
function normalizeLtiConfig(data) {
|
||||
if (!data || Object.keys(data).length < 1) {
|
||||
@@ -33,6 +38,25 @@ function extractDiscussionTopicIds(data) {
|
||||
).map(([key, value]) => value.id);
|
||||
}
|
||||
|
||||
export function normalizeBlackoutDates(data) {
|
||||
if (!data.length) { return []; }
|
||||
|
||||
const normalizeData = data.map(([startDate, endDate]) => ({
|
||||
id: uuid(),
|
||||
startDate: moment(startDate).format('YYYY-MM-DD'),
|
||||
startTime: startDate.split('T')[1] || '',
|
||||
endDate: moment(endDate).format('YYYY-MM-DD'),
|
||||
endTime: endDate.split('T')[1] || '',
|
||||
status: checkStatus([startDate, endDate]),
|
||||
}));
|
||||
|
||||
return [
|
||||
...sortBlackoutDatesByStatus(normalizeData, constants.ACTIVE, 'desc'),
|
||||
...sortBlackoutDatesByStatus(normalizeData, constants.UPCOMING, 'asc'),
|
||||
...sortBlackoutDatesByStatus(normalizeData, constants.COMPLETE, 'desc'),
|
||||
];
|
||||
}
|
||||
|
||||
function normalizePluginConfig(data) {
|
||||
if (!data || Object.keys(data).length < 1) {
|
||||
return {};
|
||||
@@ -45,7 +69,7 @@ function normalizePluginConfig(data) {
|
||||
return {
|
||||
allowAnonymousPosts: data.allow_anonymous,
|
||||
allowAnonymousPostsPeers: data.allow_anonymous_to_peers,
|
||||
blackoutDates: JSON.stringify(data.discussion_blackouts),
|
||||
blackoutDates: normalizeBlackoutDates(data.discussion_blackouts),
|
||||
allowDivisionByUnit: false,
|
||||
divideByCohorts: discussionDividedTopicsCount > 0,
|
||||
divideCourseTopicsByCohorts: enableDivideCourseTopicsByCohorts,
|
||||
@@ -88,6 +112,13 @@ function normalizeApps(data) {
|
||||
};
|
||||
}
|
||||
|
||||
export function denormalizeBlackoutDate(date) {
|
||||
return [
|
||||
formatDate(date.startDate, date.startTime),
|
||||
formatDate(date.endDate, date.endTime),
|
||||
];
|
||||
}
|
||||
|
||||
function denormalizeData(courseId, appId, data) {
|
||||
const pluginConfiguration = {};
|
||||
|
||||
@@ -97,8 +128,12 @@ function denormalizeData(courseId, appId, data) {
|
||||
if (data.allowAnonymousPostsPeers) {
|
||||
pluginConfiguration.allow_anonymous_to_peers = data.allowAnonymousPostsPeers;
|
||||
}
|
||||
if (data.blackoutDates) {
|
||||
pluginConfiguration.discussion_blackouts = JSON.parse(data.blackoutDates);
|
||||
if (data.blackoutDates?.length) {
|
||||
pluginConfiguration.discussion_blackouts = data.blackoutDates.map((blackoutDates) => (
|
||||
denormalizeBlackoutDate(blackoutDates)
|
||||
));
|
||||
} else if (data.blackoutDates?.length === 0) {
|
||||
pluginConfiguration.discussion_blackouts = [];
|
||||
}
|
||||
if (data.discussionTopics?.length) {
|
||||
pluginConfiguration.discussion_topics = data.discussionTopics.reduce((topics, currentTopic) => {
|
||||
|
||||
29
src/pages-and-resources/discussions/data/constants.js
Normal file
29
src/pages-and-resources/discussions/data/constants.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import moment from 'moment';
|
||||
|
||||
import messages from '../app-config-form/apps/shared/messages';
|
||||
|
||||
export const blackoutDatesStatus = {
|
||||
UPCOMING: 'UPCOMING',
|
||||
COMPLETE: 'COMPLETE',
|
||||
ACTIVE: 'ACTIVE',
|
||||
NEW: 'NEW',
|
||||
};
|
||||
|
||||
export const badgeVariant = {
|
||||
UPCOMING: 'primary',
|
||||
COMPLETE: 'light',
|
||||
ACTIVE: 'success',
|
||||
NEW: 'LIGHT',
|
||||
};
|
||||
|
||||
export const deleteHelperText = {
|
||||
UPCOMING: messages.blackoutDatesDeletionHelp,
|
||||
COMPLETE: messages.completeBlackoutDatesDeletionHelp,
|
||||
ACTIVE: messages.activeBlackoutDatesDeletionHelp,
|
||||
NEW: messages.completeBlackoutDatesDeletionHelp,
|
||||
};
|
||||
|
||||
export const today = moment();
|
||||
export const active = [today.format('YYYY-MM-DDTHH:mm'), today.add(5, 'hours').format('YYYY-MM-DDTHH:mm')];
|
||||
export const upcoming = [today.add(2, 'days').format('YYYY-MM-DD'), today.add(5, 'days').format('YYYY-MM-DD')];
|
||||
export const complete = [today.subtract(7, 'days').format('YYYY-MM-DD'), today.subtract(5, 'days').format('YYYY-MM-DD')];
|
||||
@@ -233,7 +233,7 @@ describe('Data layer integration tests', () => {
|
||||
id: 'legacy',
|
||||
allowAnonymousPosts: false,
|
||||
allowAnonymousPostsPeers: false,
|
||||
blackoutDates: '[]',
|
||||
blackoutDates: [],
|
||||
// TODO: Note! As of this writing, all the data below this line is NOT returned in the API
|
||||
// but we add it in during normalization.
|
||||
divideByCohorts: true,
|
||||
@@ -387,7 +387,7 @@ describe('Data layer integration tests', () => {
|
||||
plugin_configuration: {
|
||||
allow_anonymous: true,
|
||||
allow_anonymous_to_peers: true,
|
||||
discussion_blackouts: [['2015-09-15', '2015-09-21'], ['2015-10-01', '2015-10-08']],
|
||||
discussion_blackouts: [],
|
||||
discussion_topics: {
|
||||
Edx: { id: '13f106c6-6735-4e84-b097-0456cff55960' },
|
||||
General: { id: 'course' },
|
||||
@@ -404,7 +404,7 @@ describe('Data layer integration tests', () => {
|
||||
plugin_configuration: {
|
||||
allow_anonymous: true,
|
||||
allow_anonymous_to_peers: true,
|
||||
discussion_blackouts: [['2015-09-15', '2015-09-21'], ['2015-10-01', '2015-10-08']],
|
||||
discussion_blackouts: [],
|
||||
discussion_topics: {
|
||||
Edx: { id: '13f106c6-6735-4e84-b097-0456cff55960' },
|
||||
General: { id: 'course' },
|
||||
@@ -425,7 +425,7 @@ describe('Data layer integration tests', () => {
|
||||
{
|
||||
allowAnonymousPosts: true,
|
||||
allowAnonymousPostsPeers: true,
|
||||
blackoutDates: '[["2015-09-15","2015-09-21"],["2015-10-01","2015-10-08"]]',
|
||||
blackoutDates: [],
|
||||
// TODO: Note! As of this writing, all the data below this line is NOT returned in the API
|
||||
// but we technically send it to the thunk, so here it is.
|
||||
divideByCohorts: true,
|
||||
@@ -459,7 +459,7 @@ describe('Data layer integration tests', () => {
|
||||
// These three fields should be updated.
|
||||
allowAnonymousPosts: true,
|
||||
allowAnonymousPostsPeers: true,
|
||||
blackoutDates: '[["2015-09-15","2015-09-21"],["2015-10-01","2015-10-08"]]',
|
||||
blackoutDates: [],
|
||||
// TODO: Note! The values we tried to save were ignored, this test reflects what currently
|
||||
// happens, but NOT what we want to have happen!
|
||||
divideByCohorts: true,
|
||||
|
||||
37
src/utils.js
37
src/utils.js
@@ -3,6 +3,7 @@ import { useContext, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useMediaQuery } from 'react-responsive';
|
||||
import * as Yup from 'yup';
|
||||
import moment from 'moment';
|
||||
|
||||
import { RequestStatus } from './data/constants';
|
||||
import { getCourseAppSettingValue, getLoadingStatus } from './pages-and-resources/data/selectors';
|
||||
@@ -71,4 +72,40 @@ export function setupYupExtensions() {
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
Yup.addMethod(Yup.object, 'uniqueObjectProperty', function uniqueObjectProperty(propertyName, message) {
|
||||
return this.test('unique', message, function testUniqueness(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;
|
||||
});
|
||||
});
|
||||
|
||||
Yup.addMethod(Yup.string, 'compare', function compare(message) {
|
||||
return this.test('isGreater', message, function isGreater() {
|
||||
if (!this.parent || !this.parent.startTime || !this.parent.endTime) {
|
||||
return true;
|
||||
}
|
||||
const isInvalidStartDateTime = moment(`${moment(this.parent.startDate).format('YYYY-MM-DD')}T${this.parent.startTime}`)
|
||||
.isSameOrAfter(moment(`${moment(this.parent.endDate).format('YYYY-MM-DD')}T${this.parent.endTime}`));
|
||||
|
||||
if (isInvalidStartDateTime) {
|
||||
throw this.createError({
|
||||
path: `${this.path}`,
|
||||
error: message,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user