chore: update with master
This commit is contained in:
246
package-lock.json
generated
246
package-lock.json
generated
@@ -2396,9 +2396,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-footer": {
|
||||
"version": "14.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-14.0.5.tgz",
|
||||
"integrity": "sha512-r64SGM8wzYZCtAG/J8i+7S7c2XCylKdA4VesSU6Q/Ig8SMndJAhfLc9eo9H4dRrq1E0cYqV2+bQroEucXjzSuQ==",
|
||||
"version": "14.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-14.0.8.tgz",
|
||||
"integrity": "sha512-0H91yt9dgFdrjWMyAUI4cgtWqsutylHm1PHXH3sNHXNQcTMPeLyvFfknrA2OLVo0eeDDY0/P23cxmOXG3tAEng==",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "6.5.2",
|
||||
@@ -2418,81 +2418,6 @@
|
||||
"react-dom": "^16.9.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz",
|
||||
"integrity": "sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/fontawesome-svg-core": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.2.tgz",
|
||||
"integrity": "sha512-5CdaCBGl8Rh9ohNdxeeTMxIj8oc3KNBgIeLMvJosBMdslK/UnEB8rzyDRrbKdL1kDweqBPo4GT9wvnakHWucZw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/free-brands-svg-icons": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.5.2.tgz",
|
||||
"integrity": "sha512-zi5FNYdmKLnEc0jc0uuHH17kz/hfYTg4Uei0wMGzcoCL/4d3WM3u1VMc0iGGa31HuhV5i7ZK8ZlTCQrHqRHSGQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "(CC-BY-4.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/free-regular-svg-icons": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.2.tgz",
|
||||
"integrity": "sha512-iabw/f5f8Uy2nTRtJ13XZTS1O5+t+anvlamJ3zJGLEVE2pKsAWhPv2lq01uQlfgCX7VaveT3EVs515cCN9jRbw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "(CC-BY-4.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/free-solid-svg-icons": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.2.tgz",
|
||||
"integrity": "sha512-QWFZYXFE7O1Gr1dTIp+D6UcFUF0qElOnZptpi7PBUMylJh+vFmIedVe1Ir6RM1t2tEQLLSV1k7bR4o92M+uqlw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "(CC-BY-4.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/react-fontawesome": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz",
|
||||
"integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "~1 || ~6",
|
||||
"react": ">=16.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-footer/node_modules/jest-environment-jsdom": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz",
|
||||
@@ -2521,14 +2446,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-header": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-5.3.3.tgz",
|
||||
"integrity": "sha512-qOPU8YFg3VT4PbyjqFY0eOfRbLyi7jQFjPCsXn+EEwII4LVRbbg2bW3dtDvscMM1J1awaQZtqFHm2m5I/Rpxtg==",
|
||||
"version": "5.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-5.3.4.tgz",
|
||||
"integrity": "sha512-niuaXu0+qWPHud9Bs1pqmNXvZc9jpf8WS270/2YEH5owokd+BiDwQ6MWkvS9qbuQIVGPGTSZFFTttUKmQO5O0A==",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "6.5.2",
|
||||
"@fortawesome/free-brands-svg-icons": "6.5.2",
|
||||
"@fortawesome/free-regular-svg-icons": "6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.5.2",
|
||||
"@fortawesome/fontawesome-svg-core": "6.6.0",
|
||||
"@fortawesome/free-brands-svg-icons": "6.6.0",
|
||||
"@fortawesome/free-regular-svg-icons": "6.6.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.6.0",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"axios-mock-adapter": "1.22.0",
|
||||
"babel-polyfill": "6.26.0",
|
||||
@@ -2545,57 +2471,57 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz",
|
||||
"integrity": "sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==",
|
||||
"hasInstallScript": true,
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz",
|
||||
"integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/fontawesome-svg-core": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.2.tgz",
|
||||
"integrity": "sha512-5CdaCBGl8Rh9ohNdxeeTMxIj8oc3KNBgIeLMvJosBMdslK/UnEB8rzyDRrbKdL1kDweqBPo4GT9wvnakHWucZw==",
|
||||
"hasInstallScript": true,
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz",
|
||||
"integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.5.2"
|
||||
"@fortawesome/fontawesome-common-types": "6.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-brands-svg-icons": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.5.2.tgz",
|
||||
"integrity": "sha512-zi5FNYdmKLnEc0jc0uuHH17kz/hfYTg4Uei0wMGzcoCL/4d3WM3u1VMc0iGGa31HuhV5i7ZK8ZlTCQrHqRHSGQ==",
|
||||
"hasInstallScript": true,
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.6.0.tgz",
|
||||
"integrity": "sha512-1MPD8lMNW/earme4OQi1IFHtmHUwAKgghXlNwWi9GO7QkTfD+IIaYpIai4m2YJEzqfEji3jFHX1DZI5pbY/biQ==",
|
||||
"license": "(CC-BY-4.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.5.2"
|
||||
"@fortawesome/fontawesome-common-types": "6.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-regular-svg-icons": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.2.tgz",
|
||||
"integrity": "sha512-iabw/f5f8Uy2nTRtJ13XZTS1O5+t+anvlamJ3zJGLEVE2pKsAWhPv2lq01uQlfgCX7VaveT3EVs515cCN9jRbw==",
|
||||
"hasInstallScript": true,
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.6.0.tgz",
|
||||
"integrity": "sha512-Yv9hDzL4aI73BEwSEh20clrY8q/uLxawaQ98lekBx6t9dQKDHcDzzV1p2YtBGTtolYtNqcWdniOnhzB+JPnQEQ==",
|
||||
"license": "(CC-BY-4.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.5.2"
|
||||
"@fortawesome/fontawesome-common-types": "6.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-solid-svg-icons": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.2.tgz",
|
||||
"integrity": "sha512-QWFZYXFE7O1Gr1dTIp+D6UcFUF0qElOnZptpi7PBUMylJh+vFmIedVe1Ir6RM1t2tEQLLSV1k7bR4o92M+uqlw==",
|
||||
"hasInstallScript": true,
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz",
|
||||
"integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==",
|
||||
"license": "(CC-BY-4.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.5.2"
|
||||
"@fortawesome/fontawesome-common-types": "6.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -2605,6 +2531,7 @@
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz",
|
||||
"integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/environment": "^29.7.0",
|
||||
"@jest/fake-timers": "^29.7.0",
|
||||
@@ -2631,6 +2558,7 @@
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-8.2.0.tgz",
|
||||
"integrity": "sha512-iagCqVrw4QSjhxKp3I/YK6+ODkWY6G+YPElvdYKiUUbywwh9Ds0M7r26Fj2/7dWFFbOpcGnJE6uE7aMck8j5Qg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hyphenate-style-name": "^1.0.0",
|
||||
"matchmediaquery": "^0.3.0",
|
||||
@@ -2703,9 +2631,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/openedx-atlas": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/openedx-atlas/-/openedx-atlas-0.6.0.tgz",
|
||||
"integrity": "sha512-wZO7hA4VJ/bXjaQNNR7KXGYyTCNs1mBJd3HwQK2EmOwFZYFNX6nzSAm9S7HCfi/kb1PCRpmp3wJt+v/Eu9BEQg==",
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@edx/openedx-atlas/-/openedx-atlas-0.6.1.tgz",
|
||||
"integrity": "sha512-n5b2fN3usGqOHREji4QZDsXSzRwH7b6Bf9NiA49OcHKjbMYhaPNp4BVakIbu3f3wuPyyVY+bgUODx7wRB4OyIQ==",
|
||||
"license": "AGPL-3.0",
|
||||
"bin": {
|
||||
"atlas": "atlas"
|
||||
}
|
||||
@@ -3238,32 +3167,72 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"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==",
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz",
|
||||
"integrity": "sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==",
|
||||
"hasInstallScript": true,
|
||||
"peer": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-svg-core": {
|
||||
"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==",
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.2.tgz",
|
||||
"integrity": "sha512-5CdaCBGl8Rh9ohNdxeeTMxIj8oc3KNBgIeLMvJosBMdslK/UnEB8rzyDRrbKdL1kDweqBPo4GT9wvnakHWucZw==",
|
||||
"hasInstallScript": true,
|
||||
"peer": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.36"
|
||||
"@fortawesome/fontawesome-common-types": "6.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-brands-svg-icons": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.5.2.tgz",
|
||||
"integrity": "sha512-zi5FNYdmKLnEc0jc0uuHH17kz/hfYTg4Uei0wMGzcoCL/4d3WM3u1VMc0iGGa31HuhV5i7ZK8ZlTCQrHqRHSGQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "(CC-BY-4.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-regular-svg-icons": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.2.tgz",
|
||||
"integrity": "sha512-iabw/f5f8Uy2nTRtJ13XZTS1O5+t+anvlamJ3zJGLEVE2pKsAWhPv2lq01uQlfgCX7VaveT3EVs515cCN9jRbw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "(CC-BY-4.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-solid-svg-icons": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.2.tgz",
|
||||
"integrity": "sha512-QWFZYXFE7O1Gr1dTIp+D6UcFUF0qElOnZptpi7PBUMylJh+vFmIedVe1Ir6RM1t2tEQLLSV1k7bR4o92M+uqlw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "(CC-BY-4.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/react-fontawesome": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz",
|
||||
"integrity": "sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==",
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz",
|
||||
"integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
@@ -4329,9 +4298,9 @@
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
|
||||
},
|
||||
"node_modules/@openedx/paragon": {
|
||||
"version": "22.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-22.6.1.tgz",
|
||||
"integrity": "sha512-xblrspAfsYsiDzyLIh+tceiTPgx1HY6v0eceatTYSj/BINxN8Dcqh9uQOZi2eJc1os3w2dr0nZRGnTt8cYu2BA==",
|
||||
"version": "22.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-22.7.0.tgz",
|
||||
"integrity": "sha512-BWj4vYXUmLS0BinJckxbhNp0o1UPfwURinaSgTxxBkF0L2VUtAO+SldVWvKDqlltzoR062yjcBA5QSGq8Jxgeg==",
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
"example",
|
||||
@@ -4380,29 +4349,6 @@
|
||||
"react-intl": "^5.25.1 || ^6.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@openedx/paragon/node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz",
|
||||
"integrity": "sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@openedx/paragon/node_modules/@fortawesome/fontawesome-svg-core": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.2.tgz",
|
||||
"integrity": "sha512-5CdaCBGl8Rh9ohNdxeeTMxIj8oc3KNBgIeLMvJosBMdslK/UnEB8rzyDRrbKdL1kDweqBPo4GT9wvnakHWucZw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@openedx/paragon/node_modules/@fortawesome/react-fontawesome": {
|
||||
"version": "0.1.19",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.19.tgz",
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// @ts-check
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { getClipboard } from '../../data/api';
|
||||
import { updateClipboardData } from '../../data/slice';
|
||||
import { CLIPBOARD_STATUS, STRUCTURAL_XBLOCK_TYPES, STUDIO_CLIPBOARD_CHANNEL } from '../../../constants';
|
||||
import { getClipboardData } from '../../data/selectors';
|
||||
|
||||
@@ -14,6 +17,7 @@ import { getClipboardData } from '../../data/selectors';
|
||||
* @property {Object} sharedClipboardData - The shared clipboard data object.
|
||||
*/
|
||||
const useCopyToClipboard = (canEdit = true) => {
|
||||
const dispatch = useDispatch();
|
||||
const [clipboardBroadcastChannel] = useState(() => new BroadcastChannel(STUDIO_CLIPBOARD_CHANNEL));
|
||||
const [showPasteUnit, setShowPasteUnit] = useState(false);
|
||||
const [showPasteXBlock, setShowPasteXBlock] = useState(false);
|
||||
@@ -30,6 +34,22 @@ const useCopyToClipboard = (canEdit = true) => {
|
||||
setShowPasteUnit(!!isPasteableUnit);
|
||||
};
|
||||
|
||||
// Called on initial render to fetch and populate the initial clipboard data in redux state.
|
||||
// Without this, the initial clipboard data redux state is always null.
|
||||
useEffect(() => {
|
||||
const fetchInitialClipboardData = async () => {
|
||||
try {
|
||||
const userClipboard = await getClipboard();
|
||||
dispatch(updateClipboardData(userClipboard));
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Failed to fetch initial clipboard data: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
fetchInitialClipboardData();
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
// Handle updates to clipboard data
|
||||
if (canEdit) {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
@import "export-page/CourseExportPage";
|
||||
@import "import-page/CourseImportPage";
|
||||
@import "taxonomy";
|
||||
@import "library-authoring";
|
||||
@import "files-and-videos";
|
||||
@import "content-tags-drawer";
|
||||
@import "course-outline/CourseOutline";
|
||||
@@ -30,7 +31,6 @@
|
||||
@import "search-manager";
|
||||
@import "certificates/scss/Certificates";
|
||||
@import "group-configurations/GroupConfigurations";
|
||||
@import "library-authoring";
|
||||
|
||||
// To apply the glow effect to the selected Section/Subsection, in the Course Outline
|
||||
div.row:has(> div > div.highlight) {
|
||||
|
||||
@@ -83,6 +83,9 @@ const libraryData: ContentLibrary = {
|
||||
numBlocks: 2,
|
||||
version: 0,
|
||||
lastPublished: null,
|
||||
lastDraftCreated: '2024-07-22',
|
||||
publishedBy: 'staff',
|
||||
lastDraftCreatedBy: 'staff',
|
||||
allowLti: false,
|
||||
allowPublicLearning: false,
|
||||
allowPublicRead: false,
|
||||
@@ -90,8 +93,17 @@ const libraryData: ContentLibrary = {
|
||||
hasUnpublishedDeletes: false,
|
||||
canEditLibrary: true,
|
||||
license: '',
|
||||
created: '2024-06-26',
|
||||
updated: '2024-07-20',
|
||||
};
|
||||
|
||||
const clipboardBroadcastChannelMock = {
|
||||
postMessage: jest.fn(),
|
||||
close: jest.fn(),
|
||||
};
|
||||
|
||||
(global as any).BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
|
||||
|
||||
const RootWrapper = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
@@ -177,7 +189,7 @@ describe('<LibraryAuthoringPage />', () => {
|
||||
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);
|
||||
|
||||
const {
|
||||
getByRole, getByText, getAllByText, queryByText,
|
||||
getByRole, getByText, queryByText, findByText, findAllByText,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
// Ensure the search endpoint is called:
|
||||
@@ -185,15 +197,15 @@ describe('<LibraryAuthoringPage />', () => {
|
||||
// Call 2: To fetch the recently modified components only
|
||||
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); });
|
||||
|
||||
expect(getByText('Content library')).toBeInTheDocument();
|
||||
expect(getByText(libraryData.title)).toBeInTheDocument();
|
||||
expect(await findByText('Content library')).toBeInTheDocument();
|
||||
expect((await findAllByText(libraryData.title))[0]).toBeInTheDocument();
|
||||
|
||||
expect(queryByText('You have not added any content to this library yet.')).not.toBeInTheDocument();
|
||||
|
||||
expect(getByText('Recently Modified')).toBeInTheDocument();
|
||||
expect(getByText('Collections (0)')).toBeInTheDocument();
|
||||
expect(getByText('Components (6)')).toBeInTheDocument();
|
||||
expect(getAllByText('Test HTML Block')[0]).toBeInTheDocument();
|
||||
expect((await findAllByText('Test HTML Block'))[0]).toBeInTheDocument();
|
||||
|
||||
// Navigate to the components tab
|
||||
fireEvent.click(getByRole('tab', { name: 'Components' }));
|
||||
@@ -222,10 +234,10 @@ describe('<LibraryAuthoringPage />', () => {
|
||||
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);
|
||||
fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true });
|
||||
|
||||
const { findByText, getByText } = render(<RootWrapper />);
|
||||
const { findByText, getByText, findAllByText } = render(<RootWrapper />);
|
||||
|
||||
expect(await findByText('Content library')).toBeInTheDocument();
|
||||
expect(await findByText(libraryData.title)).toBeInTheDocument();
|
||||
expect((await findAllByText(libraryData.title))[0]).toBeInTheDocument();
|
||||
|
||||
// Ensure the search endpoint is called:
|
||||
// Call 1: To fetch searchable/filterable/sortable library data
|
||||
@@ -282,10 +294,15 @@ describe('<LibraryAuthoringPage />', () => {
|
||||
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);
|
||||
fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true });
|
||||
|
||||
const { findByText, getByRole, getByText } = render(<RootWrapper />);
|
||||
const {
|
||||
findByText,
|
||||
getByRole,
|
||||
getByText,
|
||||
findAllByText,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
expect(await findByText('Content library')).toBeInTheDocument();
|
||||
expect(await findByText(libraryData.title)).toBeInTheDocument();
|
||||
expect((await findAllByText(libraryData.title))[0]).toBeInTheDocument();
|
||||
|
||||
// Ensure the search endpoint is called:
|
||||
// Call 1: To fetch searchable/filterable/sortable library data
|
||||
@@ -329,12 +346,54 @@ describe('<LibraryAuthoringPage />', () => {
|
||||
expect(screen.queryByText(/add content/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should open Library Info by default', async () => {
|
||||
mockUseParams.mockReturnValue({ libraryId: libraryData.id });
|
||||
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);
|
||||
|
||||
render(<RootWrapper />);
|
||||
|
||||
expect(await screen.findByText('Content library')).toBeInTheDocument();
|
||||
expect((await screen.findAllByText(libraryData.title))[0]).toBeInTheDocument();
|
||||
expect((await screen.findAllByText(libraryData.title))[1]).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Draft')).toBeInTheDocument();
|
||||
expect(screen.getByText('(Never Published)')).toBeInTheDocument();
|
||||
expect(screen.getByText('July 22, 2024')).toBeInTheDocument();
|
||||
expect(screen.getByText('staff')).toBeInTheDocument();
|
||||
expect(screen.getByText(libraryData.org)).toBeInTheDocument();
|
||||
expect(screen.getByText('July 20, 2024')).toBeInTheDocument();
|
||||
expect(screen.getByText('June 26, 2024')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should close and open Library Info', async () => {
|
||||
mockUseParams.mockReturnValue({ libraryId: libraryData.id });
|
||||
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);
|
||||
|
||||
render(<RootWrapper />);
|
||||
|
||||
expect(await screen.findByText('Content library')).toBeInTheDocument();
|
||||
expect((await screen.findAllByText(libraryData.title))[0]).toBeInTheDocument();
|
||||
expect((await screen.findAllByText(libraryData.title))[1]).toBeInTheDocument();
|
||||
|
||||
const closeButton = screen.getByRole('button', { name: /close/i });
|
||||
fireEvent.click(closeButton);
|
||||
|
||||
expect(screen.queryByText('Draft')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('(Never Published)')).not.toBeInTheDocument();
|
||||
|
||||
const libraryInfoButton = screen.getByRole('button', { name: /library info/i });
|
||||
fireEvent.click(libraryInfoButton);
|
||||
|
||||
expect(screen.getByText('Draft')).toBeInTheDocument();
|
||||
expect(screen.getByText('(Never Published)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('show the "View All" button when viewing library with many components', async () => {
|
||||
mockUseParams.mockReturnValue({ libraryId: libraryData.id });
|
||||
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);
|
||||
|
||||
const {
|
||||
getByRole, getByText, queryByText, getAllByText,
|
||||
getByRole, getByText, queryByText, getAllByText, findAllByText,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
// Ensure the search endpoint is called:
|
||||
@@ -343,7 +402,7 @@ describe('<LibraryAuthoringPage />', () => {
|
||||
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); });
|
||||
|
||||
expect(getByText('Content library')).toBeInTheDocument();
|
||||
expect(getByText(libraryData.title)).toBeInTheDocument();
|
||||
expect((await findAllByText(libraryData.title))[0]).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => { expect(getByText('Recently Modified')).toBeInTheDocument(); });
|
||||
expect(getByText('Collections (0)')).toBeInTheDocument();
|
||||
@@ -376,7 +435,7 @@ describe('<LibraryAuthoringPage />', () => {
|
||||
fetchMock.post(searchEndpoint, returnLowNumberResults, { overwriteRoutes: true });
|
||||
|
||||
const {
|
||||
getByText, queryByText, getAllByText,
|
||||
getByText, queryByText, getAllByText, findAllByText,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
// Ensure the search endpoint is called:
|
||||
@@ -385,7 +444,7 @@ describe('<LibraryAuthoringPage />', () => {
|
||||
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); });
|
||||
|
||||
expect(getByText('Content library')).toBeInTheDocument();
|
||||
expect(getByText(libraryData.title)).toBeInTheDocument();
|
||||
expect((await findAllByText(libraryData.title))[0]).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => { expect(getByText('Recently Modified')).toBeInTheDocument(); });
|
||||
expect(getByText('Collections (0)')).toBeInTheDocument();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext } from 'react';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { StudioFooter } from '@edx/frontend-component-footer';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
@@ -6,8 +6,6 @@ import {
|
||||
Button,
|
||||
Col,
|
||||
Container,
|
||||
Icon,
|
||||
IconButton,
|
||||
Row,
|
||||
Stack,
|
||||
Tab,
|
||||
@@ -52,6 +50,7 @@ const HeaderActions = ({ canEditLibrary }: HeaderActionsProps) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
openAddContentSidebar,
|
||||
openInfoSidebar,
|
||||
} = useContext(LibraryContext);
|
||||
|
||||
if (!canEditLibrary) {
|
||||
@@ -59,30 +58,32 @@ const HeaderActions = ({ canEditLibrary }: HeaderActionsProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
iconBefore={Add}
|
||||
variant="primary rounded-0"
|
||||
onClick={() => openAddContentSidebar()}
|
||||
disabled={!canEditLibrary}
|
||||
>
|
||||
{intl.formatMessage(messages.newContentButton)}
|
||||
</Button>
|
||||
<>
|
||||
<Button
|
||||
iconBefore={InfoOutline}
|
||||
variant="outline-primary rounded-0"
|
||||
onClick={openInfoSidebar}
|
||||
>
|
||||
{intl.formatMessage(messages.libraryInfoButton)}
|
||||
</Button>
|
||||
<Button
|
||||
iconBefore={Add}
|
||||
variant="primary rounded-0"
|
||||
onClick={openAddContentSidebar}
|
||||
disabled={!canEditLibrary}
|
||||
>
|
||||
{intl.formatMessage(messages.newContentButton)}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const SubHeaderTitle = ({ title, canEditLibrary }: { title: string, canEditLibrary: boolean }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<Stack direction="vertical">
|
||||
<Stack direction="horizontal">
|
||||
{title}
|
||||
<IconButton
|
||||
src={InfoOutline}
|
||||
iconAs={Icon}
|
||||
alt={intl.formatMessage(messages.headingInfoAlt)}
|
||||
className="mr-2"
|
||||
/>
|
||||
</Stack>
|
||||
{title}
|
||||
{ !canEditLibrary && (
|
||||
<div>
|
||||
<Badge variant="primary" style={{ fontSize: '50%' }}>
|
||||
@@ -104,7 +105,14 @@ const LibraryAuthoringPage = () => {
|
||||
|
||||
const currentPath = location.pathname.split('/').pop();
|
||||
const activeKey = (currentPath && currentPath in TabList) ? TabList[currentPath] : TabList.home;
|
||||
const { sidebarBodyComponent } = useContext(LibraryContext);
|
||||
const {
|
||||
sidebarBodyComponent,
|
||||
openInfoSidebar,
|
||||
} = useContext(LibraryContext);
|
||||
|
||||
useEffect(() => {
|
||||
openInfoSidebar();
|
||||
}, []);
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
@@ -190,8 +198,8 @@ const LibraryAuthoringPage = () => {
|
||||
<StudioFooter />
|
||||
</Col>
|
||||
{ sidebarBodyComponent !== null && (
|
||||
<Col xs={6} md={4} className="box-shadow-left-1">
|
||||
<LibrarySidebar />
|
||||
<Col xs={3} md={3} className="box-shadow-left-1">
|
||||
<LibrarySidebar library={libraryData} />
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
@@ -10,7 +10,10 @@ import MockAdapter from 'axios-mock-adapter';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import AddContentContainer from './AddContentContainer';
|
||||
import initializeStore from '../../store';
|
||||
import { getCreateLibraryBlockUrl } from '../data/api';
|
||||
import { getCreateLibraryBlockUrl, getLibraryPasteClipboardUrl } from '../data/api';
|
||||
import { getClipboardUrl } from '../../generic/data/api';
|
||||
|
||||
import { clipboardXBlock } from '../../__mocks__';
|
||||
|
||||
const mockUseParams = jest.fn();
|
||||
let axiosMock;
|
||||
@@ -31,6 +34,13 @@ const queryClient = new QueryClient({
|
||||
},
|
||||
});
|
||||
|
||||
const clipboardBroadcastChannelMock = {
|
||||
postMessage: jest.fn(),
|
||||
close: jest.fn(),
|
||||
};
|
||||
|
||||
(global as any).BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
|
||||
|
||||
const RootWrapper = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
@@ -69,6 +79,7 @@ describe('<AddContentContainer />', () => {
|
||||
expect(screen.getByRole('button', { name: /drag drop/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /video/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /advanced \/ other/i })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: /copy from clipboard/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should create a content', async () => {
|
||||
@@ -82,4 +93,49 @@ describe('<AddContentContainer />', () => {
|
||||
|
||||
await waitFor(() => expect(axiosMock.history.post[0].url).toEqual(url));
|
||||
});
|
||||
|
||||
it('should render paste button if clipboard contains pastable xblock', async () => {
|
||||
const url = getClipboardUrl();
|
||||
axiosMock.onGet(url).reply(200, clipboardXBlock);
|
||||
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => expect(axiosMock.history.get[0].url).toEqual(url));
|
||||
|
||||
expect(screen.getByRole('button', { name: /paste from clipboard/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should paste content', async () => {
|
||||
const clipboardUrl = getClipboardUrl();
|
||||
axiosMock.onGet(clipboardUrl).reply(200, clipboardXBlock);
|
||||
|
||||
const pasteUrl = getLibraryPasteClipboardUrl(libraryId);
|
||||
axiosMock.onPost(pasteUrl).reply(200);
|
||||
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => expect(axiosMock.history.get[0].url).toEqual(clipboardUrl));
|
||||
|
||||
const pasteButton = screen.getByRole('button', { name: /paste from clipboard/i });
|
||||
fireEvent.click(pasteButton);
|
||||
|
||||
await waitFor(() => expect(axiosMock.history.post[0].url).toEqual(pasteUrl));
|
||||
});
|
||||
|
||||
it('should fail pasting content', async () => {
|
||||
const clipboardUrl = getClipboardUrl();
|
||||
axiosMock.onGet(clipboardUrl).reply(200, clipboardXBlock);
|
||||
|
||||
const pasteUrl = getLibraryPasteClipboardUrl(libraryId);
|
||||
axiosMock.onPost(pasteUrl).reply(400);
|
||||
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => expect(axiosMock.history.get[0].url).toEqual(clipboardUrl));
|
||||
|
||||
const pasteButton = screen.getByRole('button', { name: /paste from clipboard/i });
|
||||
fireEvent.click(pasteButton);
|
||||
|
||||
await waitFor(() => expect(axiosMock.history.post[0].url).toEqual(pasteUrl));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
Stack,
|
||||
Button,
|
||||
@@ -12,18 +13,25 @@ import {
|
||||
ThumbUpOutline,
|
||||
Question,
|
||||
VideoCamera,
|
||||
ContentPaste,
|
||||
} from '@openedx/paragon/icons';
|
||||
import { v4 as uuid4 } from 'uuid';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { ToastContext } from '../../generic/toast-context';
|
||||
import { useCreateLibraryBlock } from '../data/apiHooks';
|
||||
import { useCopyToClipboard } from '../../generic/clipboard';
|
||||
import { getCanEdit } from '../../course-unit/data/selectors';
|
||||
import { useCreateLibraryBlock, useLibraryPasteClipboard } from '../data/apiHooks';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const AddContentContainer = () => {
|
||||
const intl = useIntl();
|
||||
const { libraryId } = useParams();
|
||||
const createBlockMutation = useCreateLibraryBlock();
|
||||
const pasteClipboardMutation = useLibraryPasteClipboard();
|
||||
const { showToast } = useContext(ToastContext);
|
||||
const canEdit = useSelector(getCanEdit);
|
||||
const { showPasteXBlock } = useCopyToClipboard(canEdit);
|
||||
|
||||
const contentTypes = [
|
||||
{
|
||||
@@ -64,20 +72,47 @@ const AddContentContainer = () => {
|
||||
},
|
||||
];
|
||||
|
||||
// Include the 'Paste from Clipboard' button if there is an Xblock in the clipboard
|
||||
// that can be pasted
|
||||
if (showPasteXBlock) {
|
||||
const pasteButton = {
|
||||
name: intl.formatMessage(messages.pasteButton),
|
||||
disabled: false,
|
||||
icon: ContentPaste,
|
||||
blockType: 'paste',
|
||||
};
|
||||
contentTypes.push(pasteButton);
|
||||
}
|
||||
|
||||
const onCreateContent = (blockType: string) => {
|
||||
if (libraryId) {
|
||||
createBlockMutation.mutateAsync({
|
||||
libraryId,
|
||||
blockType,
|
||||
definitionId: `${uuid4()}`,
|
||||
}).then(() => {
|
||||
showToast(intl.formatMessage(messages.successCreateMessage));
|
||||
}).catch(() => {
|
||||
showToast(intl.formatMessage(messages.errorCreateMessage));
|
||||
});
|
||||
if (blockType === 'paste') {
|
||||
pasteClipboardMutation.mutateAsync({
|
||||
libraryId,
|
||||
blockId: `${uuid4()}`,
|
||||
}).then(() => {
|
||||
showToast(intl.formatMessage(messages.successPasteClipboardMessage));
|
||||
}).catch(() => {
|
||||
showToast(intl.formatMessage(messages.errorPasteClipboardMessage));
|
||||
});
|
||||
} else {
|
||||
createBlockMutation.mutateAsync({
|
||||
libraryId,
|
||||
blockType,
|
||||
definitionId: `${uuid4()}`,
|
||||
}).then(() => {
|
||||
showToast(intl.formatMessage(messages.successCreateMessage));
|
||||
}).catch(() => {
|
||||
showToast(intl.formatMessage(messages.errorCreateMessage));
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (pasteClipboardMutation.isLoading) {
|
||||
showToast(intl.formatMessage(messages.pastingClipboardMessage));
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack direction="vertical">
|
||||
<Button
|
||||
|
||||
11
src/library-authoring/add-content/AddContentHeader.tsx
Normal file
11
src/library-authoring/add-content/AddContentHeader.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
|
||||
const AddContentHeader = () => (
|
||||
<span className="font-weight-bold m-1.5">
|
||||
<FormattedMessage {...messages.addContentTitle} />
|
||||
</span>
|
||||
);
|
||||
|
||||
export default AddContentHeader;
|
||||
@@ -1,2 +1,2 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { default as AddContentContainer } from './AddContentContainer';
|
||||
export { default as AddContentHeader } from './AddContentHeader';
|
||||
|
||||
@@ -40,6 +40,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Advanced / Other',
|
||||
description: 'Content of button to create a Advanced / Other component.',
|
||||
},
|
||||
pasteButton: {
|
||||
id: 'course-authoring.library-authoring.add-content.buttons.paste',
|
||||
defaultMessage: 'Paste From Clipboard',
|
||||
description: 'Content of button to paste from clipboard.',
|
||||
},
|
||||
successCreateMessage: {
|
||||
id: 'course-authoring.library-authoring.add-content.success.text',
|
||||
defaultMessage: 'Content created successfully.',
|
||||
@@ -50,6 +55,26 @@ const messages = defineMessages({
|
||||
defaultMessage: 'There was an error creating the content.',
|
||||
description: 'Message when creation of content in library is on error',
|
||||
},
|
||||
addContentTitle: {
|
||||
id: 'course-authoring.library-authoring.sidebar.title.add-content',
|
||||
defaultMessage: 'Add Content',
|
||||
description: 'Title of add content in library container.',
|
||||
},
|
||||
successPasteClipboardMessage: {
|
||||
id: 'course-authoring.library-authoring.paste-clipboard.success.text',
|
||||
defaultMessage: 'Content pasted successfully.',
|
||||
description: 'Message when pasting clipboard in library is successful',
|
||||
},
|
||||
errorPasteClipboardMessage: {
|
||||
id: 'course-authoring.library-authoring.paste-clipboard.error.text',
|
||||
defaultMessage: 'There was an error pasting the content.',
|
||||
description: 'Message when pasting clipboard in library errors',
|
||||
},
|
||||
pastingClipboardMessage: {
|
||||
id: 'course-authoring.library-authoring.paste-clipboard.loading.text',
|
||||
defaultMessage: 'Pasting content from clipboard...',
|
||||
description: 'Message when in process of pasting content in library',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
/* eslint-disable react/require-default-props */
|
||||
import React from 'react';
|
||||
|
||||
enum SidebarBodyComponentId {
|
||||
export enum SidebarBodyComponentId {
|
||||
AddContent = 'add-content',
|
||||
Info = 'info',
|
||||
}
|
||||
|
||||
export interface LibraryContextData {
|
||||
sidebarBodyComponent: SidebarBodyComponentId | null;
|
||||
closeLibrarySidebar: () => void;
|
||||
openAddContentSidebar: () => void;
|
||||
openInfoSidebar: () => void;
|
||||
}
|
||||
|
||||
export const LibraryContext = React.createContext({
|
||||
sidebarBodyComponent: null,
|
||||
closeLibrarySidebar: () => {},
|
||||
openAddContentSidebar: () => {},
|
||||
openInfoSidebar: () => {},
|
||||
} as LibraryContextData);
|
||||
|
||||
/**
|
||||
@@ -25,12 +28,19 @@ export const LibraryProvider = (props: { children?: React.ReactNode }) => {
|
||||
|
||||
const closeLibrarySidebar = React.useCallback(() => setSidebarBodyComponent(null), []);
|
||||
const openAddContentSidebar = React.useCallback(() => setSidebarBodyComponent(SidebarBodyComponentId.AddContent), []);
|
||||
const openInfoSidebar = React.useCallback(() => setSidebarBodyComponent(SidebarBodyComponentId.Info), []);
|
||||
|
||||
const context = React.useMemo(() => ({
|
||||
sidebarBodyComponent,
|
||||
closeLibrarySidebar,
|
||||
openAddContentSidebar,
|
||||
}), [sidebarBodyComponent, closeLibrarySidebar, openAddContentSidebar]);
|
||||
openInfoSidebar,
|
||||
}), [
|
||||
sidebarBodyComponent,
|
||||
closeLibrarySidebar,
|
||||
openAddContentSidebar,
|
||||
openInfoSidebar,
|
||||
]);
|
||||
|
||||
return (
|
||||
<LibraryContext.Provider value={context}>
|
||||
|
||||
@@ -40,6 +40,13 @@ const contentHit: ContentHit = {
|
||||
lastPublished: null,
|
||||
};
|
||||
|
||||
const clipboardBroadcastChannelMock = {
|
||||
postMessage: jest.fn(),
|
||||
close: jest.fn(),
|
||||
};
|
||||
|
||||
(global as any).BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
|
||||
|
||||
const RootWrapper = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import React, { useContext, useMemo, useState } from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow,
|
||||
@@ -17,6 +17,7 @@ import TagCount from '../../generic/tag-count';
|
||||
import { ToastContext } from '../../generic/toast-context';
|
||||
import { type ContentHit, Highlight } from '../../search-manager';
|
||||
import messages from './messages';
|
||||
import { STUDIO_CLIPBOARD_CHANNEL } from '../../constants';
|
||||
|
||||
type ComponentCardProps = {
|
||||
contentHit: ContentHit,
|
||||
@@ -26,9 +27,13 @@ type ComponentCardProps = {
|
||||
const ComponentCardMenu = ({ usageKey }: { usageKey: string }) => {
|
||||
const intl = useIntl();
|
||||
const { showToast } = useContext(ToastContext);
|
||||
const [clipboardBroadcastChannel] = useState(() => new BroadcastChannel(STUDIO_CLIPBOARD_CHANNEL));
|
||||
const updateClipboardClick = () => {
|
||||
updateClipboard(usageKey)
|
||||
.then(() => showToast(intl.formatMessage(messages.copyToClipboardSuccess)))
|
||||
.then((clipboardData) => {
|
||||
clipboardBroadcastChannel.postMessage(clipboardData);
|
||||
showToast(intl.formatMessage(messages.copyToClipboardSuccess));
|
||||
})
|
||||
.catch(() => showToast(intl.formatMessage(messages.copyToClipboardError)));
|
||||
};
|
||||
|
||||
|
||||
@@ -84,6 +84,13 @@ jest.mock('../../search-manager', () => ({
|
||||
useSearchContext: () => mockUseSearchContext(),
|
||||
}));
|
||||
|
||||
const clipboardBroadcastChannelMock = {
|
||||
postMessage: jest.fn(),
|
||||
close: jest.fn(),
|
||||
};
|
||||
|
||||
(global as any).BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
|
||||
|
||||
const RootWrapper = (props) => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { createLibraryBlock, getCreateLibraryBlockUrl } from './api';
|
||||
import {
|
||||
commitLibraryChanges,
|
||||
createLibraryBlock,
|
||||
getCommitLibraryChangesUrl,
|
||||
getCreateLibraryBlockUrl,
|
||||
revertLibraryChanges,
|
||||
} from './api';
|
||||
|
||||
let axiosMock;
|
||||
|
||||
@@ -21,6 +27,7 @@ describe('library api calls', () => {
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
axiosMock.restore();
|
||||
});
|
||||
|
||||
it('should create library block', async () => {
|
||||
@@ -35,4 +42,24 @@ describe('library api calls', () => {
|
||||
|
||||
expect(axiosMock.history.post[0].url).toEqual(url);
|
||||
});
|
||||
|
||||
it('should commit library changes', async () => {
|
||||
const libraryId = 'lib:org:1';
|
||||
const url = getCommitLibraryChangesUrl(libraryId);
|
||||
axiosMock.onPost(url).reply(200);
|
||||
|
||||
await commitLibraryChanges(libraryId);
|
||||
|
||||
expect(axiosMock.history.post[0].url).toEqual(url);
|
||||
});
|
||||
|
||||
it('should revert library changes', async () => {
|
||||
const libraryId = 'lib:org:1';
|
||||
const url = getCommitLibraryChangesUrl(libraryId);
|
||||
axiosMock.onDelete(url).reply(200);
|
||||
|
||||
await revertLibraryChanges(libraryId);
|
||||
|
||||
expect(axiosMock.history.delete[0].url).toEqual(url);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,14 @@ export const getLibraryBlockTypesUrl = (libraryId: string) => `${getApiBaseUrl()
|
||||
*/
|
||||
export const getCreateLibraryBlockUrl = (libraryId: string) => `${getApiBaseUrl()}/api/libraries/v2/${libraryId}/blocks/`;
|
||||
export const getContentLibraryV2ListApiUrl = () => `${getApiBaseUrl()}/api/libraries/v2/`;
|
||||
/**
|
||||
* Get the URL for commit/revert changes in library.
|
||||
*/
|
||||
export const getCommitLibraryChangesUrl = (libraryId: string) => `${getApiBaseUrl()}/api/libraries/v2/${libraryId}/commit/`;
|
||||
/**
|
||||
* Get the URL for paste clipboard content into library.
|
||||
*/
|
||||
export const getLibraryPasteClipboardUrl = (libraryId: string) => `${getApiBaseUrl()}/api/libraries/v2/${libraryId}/paste_clipboard/`;
|
||||
|
||||
export interface ContentLibrary {
|
||||
id: string;
|
||||
@@ -26,7 +34,10 @@ export interface ContentLibrary {
|
||||
description: string;
|
||||
numBlocks: number;
|
||||
version: number;
|
||||
lastPublished: Date | null;
|
||||
lastPublished: string | null;
|
||||
lastDraftCreated: string | null;
|
||||
publishedBy: string | null;
|
||||
lastDraftCreatedBy: string | null;
|
||||
allowLti: boolean;
|
||||
allowPublicLearning: boolean;
|
||||
allowPublicRead: boolean;
|
||||
@@ -34,6 +45,8 @@ export interface ContentLibrary {
|
||||
hasUnpublishedDeletes: boolean;
|
||||
canEditLibrary: boolean;
|
||||
license: string;
|
||||
created: string | null;
|
||||
updated: string | null;
|
||||
}
|
||||
|
||||
export interface LibraryBlockType {
|
||||
@@ -41,18 +54,6 @@ export interface LibraryBlockType {
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch block types of a library
|
||||
*/
|
||||
export async function getLibraryBlockTypes(libraryId?: string): Promise<LibraryBlockType[]> {
|
||||
if (!libraryId) {
|
||||
throw new Error('libraryId is required');
|
||||
}
|
||||
|
||||
const { data } = await getAuthenticatedHttpClient().get(getLibraryBlockTypesUrl(libraryId));
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
export interface LibrariesV2Response {
|
||||
next: string | null,
|
||||
previous: string | null,
|
||||
@@ -94,6 +95,33 @@ export interface CreateBlockDataResponse {
|
||||
tagsCount: number;
|
||||
}
|
||||
|
||||
export interface UpdateLibraryDataRequest {
|
||||
id: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
allow_public_learning?: boolean;
|
||||
allow_public_read?: boolean;
|
||||
type?: string;
|
||||
license?: string;
|
||||
}
|
||||
|
||||
export interface LibraryPasteClipboardRequest {
|
||||
libraryId: string;
|
||||
blockId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch block types of a library
|
||||
*/
|
||||
export async function getLibraryBlockTypes(libraryId?: string): Promise<LibraryBlockType[]> {
|
||||
if (!libraryId) {
|
||||
throw new Error('libraryId is required');
|
||||
}
|
||||
|
||||
const { data } = await getAuthenticatedHttpClient().get(getLibraryBlockTypesUrl(libraryId));
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a content library by its ID.
|
||||
*/
|
||||
@@ -122,6 +150,16 @@ export async function createLibraryBlock({
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update library metadata.
|
||||
*/
|
||||
export async function updateLibraryMetadata(libraryData: UpdateLibraryDataRequest): Promise<ContentLibrary> {
|
||||
const client = getAuthenticatedHttpClient();
|
||||
const { data } = await client.patch(getContentLibraryApiUrl(libraryData.id), libraryData);
|
||||
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of content libraries.
|
||||
*/
|
||||
@@ -140,3 +178,36 @@ export async function getContentLibraryV2List(customParams: GetLibrariesV2Custom
|
||||
.get(getContentLibraryV2ListApiUrl(), { params: customParamsFormated });
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit library changes.
|
||||
*/
|
||||
export async function commitLibraryChanges(libraryId: string) {
|
||||
const client = getAuthenticatedHttpClient();
|
||||
await client.post(getCommitLibraryChangesUrl(libraryId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Revert library changes.
|
||||
*/
|
||||
export async function revertLibraryChanges(libraryId: string) {
|
||||
const client = getAuthenticatedHttpClient();
|
||||
await client.delete(getCommitLibraryChangesUrl(libraryId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Paste clipboard content into library.
|
||||
*/
|
||||
export async function libraryPasteClipboard({
|
||||
libraryId,
|
||||
blockId,
|
||||
}: LibraryPasteClipboardRequest): Promise<CreateBlockDataResponse> {
|
||||
const client = getAuthenticatedHttpClient();
|
||||
const { data } = await client.post(
|
||||
getLibraryPasteClipboardUrl(libraryId),
|
||||
{
|
||||
block_id: blockId,
|
||||
},
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { getCreateLibraryBlockUrl } from './api';
|
||||
import { useCreateLibraryBlock } from './apiHooks';
|
||||
import { getCommitLibraryChangesUrl, getCreateLibraryBlockUrl } from './api';
|
||||
import { useCommitLibraryChanges, useCreateLibraryBlock, useRevertLibraryChanges } from './apiHooks';
|
||||
|
||||
let axiosMock;
|
||||
|
||||
@@ -50,4 +50,24 @@ describe('library api hooks', () => {
|
||||
|
||||
expect(axiosMock.history.post[0].url).toEqual(url);
|
||||
});
|
||||
|
||||
it('should commit library changes', async () => {
|
||||
const libraryId = 'lib:org:1';
|
||||
const url = getCommitLibraryChangesUrl(libraryId);
|
||||
axiosMock.onPost(url).reply(200);
|
||||
const { result } = renderHook(() => useCommitLibraryChanges(), { wrapper });
|
||||
await result.current.mutateAsync(libraryId);
|
||||
|
||||
expect(axiosMock.history.post[0].url).toEqual(url);
|
||||
});
|
||||
|
||||
it('should revert library changes', async () => {
|
||||
const libraryId = 'lib:org:1';
|
||||
const url = getCommitLibraryChangesUrl(libraryId);
|
||||
axiosMock.onDelete(url).reply(200);
|
||||
const { result } = renderHook(() => useRevertLibraryChanges(), { wrapper });
|
||||
await result.current.mutateAsync(libraryId);
|
||||
|
||||
expect(axiosMock.history.delete[0].url).toEqual(url);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,11 @@ import {
|
||||
getLibraryBlockTypes,
|
||||
createLibraryBlock,
|
||||
getContentLibraryV2List,
|
||||
commitLibraryChanges,
|
||||
revertLibraryChanges,
|
||||
updateLibraryMetadata,
|
||||
ContentLibrary,
|
||||
libraryPasteClipboard,
|
||||
} from './api';
|
||||
|
||||
export const libraryAuthoringQueryKeys = {
|
||||
@@ -61,6 +66,35 @@ export const useCreateLibraryBlock = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateLibraryMetadata = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: updateLibraryMetadata,
|
||||
onMutate: async (data) => {
|
||||
const queryKey = libraryAuthoringQueryKeys.contentLibrary(data.id);
|
||||
const previousLibraryData = queryClient.getQueriesData(queryKey)[0][1] as ContentLibrary;
|
||||
|
||||
const newLibraryData = {
|
||||
...previousLibraryData,
|
||||
title: data.title,
|
||||
};
|
||||
|
||||
queryClient.setQueryData(queryKey, newLibraryData);
|
||||
|
||||
return { previousLibraryData, newLibraryData };
|
||||
},
|
||||
onError: (_err, data, context) => {
|
||||
queryClient.setQueryData(
|
||||
libraryAuthoringQueryKeys.contentLibrary(data.id),
|
||||
context?.previousLibraryData,
|
||||
);
|
||||
},
|
||||
onSettled: (_data, _error, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibrary(variables.id) });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds the query to fetch list of V2 Libraries
|
||||
*/
|
||||
@@ -71,3 +105,34 @@ export const useContentLibraryV2List = (customParams: GetLibrariesV2CustomParams
|
||||
keepPreviousData: true,
|
||||
})
|
||||
);
|
||||
|
||||
export const useCommitLibraryChanges = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: commitLibraryChanges,
|
||||
onSettled: (_data, _error, libraryId) => {
|
||||
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibrary(libraryId) });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useRevertLibraryChanges = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: revertLibraryChanges,
|
||||
onSettled: (_data, _error, libraryId) => {
|
||||
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibrary(libraryId) });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useLibraryPasteClipboard = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: libraryPasteClipboard,
|
||||
onSettled: (_data, _error, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibrary(variables.libraryId) });
|
||||
queryClient.invalidateQueries({ queryKey: ['content_search'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
@import "library-authoring/components/ComponentCard";
|
||||
@import "library-authoring/library-info/LibraryPublishStatus";
|
||||
|
||||
207
src/library-authoring/library-info/LibraryInfo.test.tsx
Normal file
207
src/library-authoring/library-info/LibraryInfo.test.tsx
Normal file
@@ -0,0 +1,207 @@
|
||||
import React from 'react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
fireEvent,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import LibraryInfo from './LibraryInfo';
|
||||
import { ToastProvider } from '../../generic/toast-context';
|
||||
import { ContentLibrary, getCommitLibraryChangesUrl } from '../data/api';
|
||||
import initializeStore from '../../store';
|
||||
|
||||
let store;
|
||||
let axiosMock;
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const libraryData: ContentLibrary = {
|
||||
id: 'lib:org1:lib1',
|
||||
type: 'complex',
|
||||
org: 'org1',
|
||||
slug: 'lib1',
|
||||
title: 'lib1',
|
||||
description: 'lib1',
|
||||
numBlocks: 2,
|
||||
version: 0,
|
||||
lastPublished: null,
|
||||
lastDraftCreated: '2024-07-22',
|
||||
publishedBy: 'staff',
|
||||
lastDraftCreatedBy: 'staff',
|
||||
allowLti: false,
|
||||
allowPublicLearning: false,
|
||||
allowPublicRead: false,
|
||||
hasUnpublishedChanges: true,
|
||||
hasUnpublishedDeletes: false,
|
||||
canEditLibrary: true,
|
||||
license: '',
|
||||
created: '2024-06-26',
|
||||
updated: '2024-07-20',
|
||||
};
|
||||
|
||||
interface WrapperProps {
|
||||
data: ContentLibrary,
|
||||
}
|
||||
|
||||
const RootWrapper = ({ data } : WrapperProps) => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ToastProvider>
|
||||
<LibraryInfo library={data} />
|
||||
</ToastProvider>
|
||||
</QueryClientProvider>
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
describe('<LibraryInfo />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
axiosMock.restore();
|
||||
});
|
||||
|
||||
it('should render Library info sidebar', () => {
|
||||
render(<RootWrapper data={libraryData} />);
|
||||
|
||||
expect(screen.getByText('Draft')).toBeInTheDocument();
|
||||
expect(screen.getByText('(Never Published)')).toBeInTheDocument();
|
||||
expect(screen.getByText('July 22, 2024')).toBeInTheDocument();
|
||||
expect(screen.getByText('staff')).toBeInTheDocument();
|
||||
expect(screen.getByText(libraryData.org)).toBeInTheDocument();
|
||||
expect(screen.getByText('July 20, 2024')).toBeInTheDocument();
|
||||
expect(screen.getByText('June 26, 2024')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render Library info in draft state without user', () => {
|
||||
const data = {
|
||||
...libraryData,
|
||||
lastDraftCreatedBy: null,
|
||||
};
|
||||
|
||||
render(<RootWrapper data={data} />);
|
||||
|
||||
expect(screen.getByText('Draft')).toBeInTheDocument();
|
||||
expect(screen.getByText('(Never Published)')).toBeInTheDocument();
|
||||
expect(screen.getByText('July 22, 2024')).toBeInTheDocument();
|
||||
expect(screen.queryByText('staff')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render Library creation date if last draft created date is null', () => {
|
||||
const data = {
|
||||
...libraryData,
|
||||
lastDraftCreated: null,
|
||||
};
|
||||
|
||||
render(<RootWrapper data={data} />);
|
||||
|
||||
expect(screen.getByText('Draft')).toBeInTheDocument();
|
||||
expect(screen.getByText('(Never Published)')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('June 26, 2024')[0]).toBeInTheDocument();
|
||||
expect(screen.getAllByText('June 26, 2024')[1]).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render library info in draft state without date', () => {
|
||||
const data = {
|
||||
...libraryData,
|
||||
lastDraftCreated: null,
|
||||
created: null,
|
||||
};
|
||||
|
||||
render(<RootWrapper data={data} />);
|
||||
|
||||
expect(screen.getByText('Draft')).toBeInTheDocument();
|
||||
expect(screen.getByText('(Never Published)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render draft library info sidebar', () => {
|
||||
const data = {
|
||||
...libraryData,
|
||||
lastPublished: '2024-07-26',
|
||||
};
|
||||
|
||||
render(<RootWrapper data={data} />);
|
||||
|
||||
expect(screen.getByText('Draft')).toBeInTheDocument();
|
||||
expect(screen.queryByText('(Never Published)')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('July 22, 2024')).toBeInTheDocument();
|
||||
expect(screen.getByText('staff')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render published library info sidebar', () => {
|
||||
const data = {
|
||||
...libraryData,
|
||||
lastPublished: '2024-07-26',
|
||||
hasUnpublishedChanges: false,
|
||||
};
|
||||
|
||||
render(<RootWrapper data={data} />);
|
||||
expect(screen.getByText('Published')).toBeInTheDocument();
|
||||
expect(screen.getByText('July 26, 2024')).toBeInTheDocument();
|
||||
expect(screen.getByText('staff')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render published library info without user', () => {
|
||||
const data = {
|
||||
...libraryData,
|
||||
lastPublished: '2024-07-26',
|
||||
hasUnpublishedChanges: false,
|
||||
publishedBy: null,
|
||||
};
|
||||
|
||||
render(<RootWrapper data={data} />);
|
||||
expect(screen.getByText('Published')).toBeInTheDocument();
|
||||
expect(screen.getByText('July 26, 2024')).toBeInTheDocument();
|
||||
expect(screen.queryByText('staff')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should publish library', async () => {
|
||||
const url = getCommitLibraryChangesUrl(libraryData.id);
|
||||
axiosMock.onPost(url).reply(200);
|
||||
render(<RootWrapper data={libraryData} />);
|
||||
|
||||
const publishButton = screen.getByRole('button', { name: /publish/i });
|
||||
fireEvent.click(publishButton);
|
||||
|
||||
expect(await screen.findByText('Library published successfully')).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => expect(axiosMock.history.post[0].url).toEqual(url));
|
||||
});
|
||||
|
||||
it('should show error on publish library', async () => {
|
||||
const url = getCommitLibraryChangesUrl(libraryData.id);
|
||||
axiosMock.onPost(url).reply(500);
|
||||
render(<RootWrapper data={libraryData} />);
|
||||
|
||||
const publishButton = screen.getByRole('button', { name: /publish/i });
|
||||
fireEvent.click(publishButton);
|
||||
|
||||
expect(await screen.findByText('There was an error publishing the library.')).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => expect(axiosMock.history.post[0].url).toEqual(url));
|
||||
});
|
||||
});
|
||||
61
src/library-authoring/library-info/LibraryInfo.tsx
Normal file
61
src/library-authoring/library-info/LibraryInfo.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import { Stack } from '@openedx/paragon';
|
||||
import { FormattedDate, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
import LibraryPublishStatus from './LibraryPublishStatus';
|
||||
import { ContentLibrary } from '../data/api';
|
||||
|
||||
type LibraryInfoProps = {
|
||||
library: ContentLibrary,
|
||||
};
|
||||
|
||||
const LibraryInfo = ({ library } : LibraryInfoProps) => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<Stack direction="vertical" gap={2.5}>
|
||||
<LibraryPublishStatus library={library} />
|
||||
<Stack gap={3} direction="vertical">
|
||||
<span className="font-weight-bold">
|
||||
{intl.formatMessage(messages.organizationSectionTitle)}
|
||||
</span>
|
||||
<span>
|
||||
{library.org}
|
||||
</span>
|
||||
</Stack>
|
||||
<Stack gap={3}>
|
||||
<span className="font-weight-bold">
|
||||
{intl.formatMessage(messages.libraryHistorySectionTitle)}
|
||||
</span>
|
||||
<Stack gap={1}>
|
||||
<span className="small text-gray-500">
|
||||
{intl.formatMessage(messages.lastModifiedLabel)}
|
||||
</span>
|
||||
<span className="small">
|
||||
<FormattedDate
|
||||
value={library.updated}
|
||||
year="numeric"
|
||||
month="long"
|
||||
day="2-digit"
|
||||
/>
|
||||
</span>
|
||||
</Stack>
|
||||
<Stack gap={1}>
|
||||
<span className="small text-gray-500">
|
||||
{intl.formatMessage(messages.createdLabel)}
|
||||
</span>
|
||||
<span className="small">
|
||||
<FormattedDate
|
||||
value={library.created}
|
||||
year="numeric"
|
||||
month="long"
|
||||
day="2-digit"
|
||||
/>
|
||||
</span>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default LibraryInfo;
|
||||
159
src/library-authoring/library-info/LibraryInfoHeader.test.tsx
Normal file
159
src/library-authoring/library-info/LibraryInfoHeader.test.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import React from 'react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
fireEvent,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { ContentLibrary, getContentLibraryApiUrl } from '../data/api';
|
||||
import initializeStore from '../../store';
|
||||
import { ToastProvider } from '../../generic/toast-context';
|
||||
import LibraryInfoHeader from './LibraryInfoHeader';
|
||||
|
||||
let store;
|
||||
let axiosMock;
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const libraryData: ContentLibrary = {
|
||||
id: 'lib:org1:lib1',
|
||||
type: 'complex',
|
||||
org: 'org1',
|
||||
slug: 'lib1',
|
||||
title: 'lib1',
|
||||
description: 'lib1',
|
||||
numBlocks: 2,
|
||||
version: 0,
|
||||
lastPublished: null,
|
||||
lastDraftCreated: '2024-07-22',
|
||||
publishedBy: 'staff',
|
||||
lastDraftCreatedBy: 'staff',
|
||||
allowLti: false,
|
||||
allowPublicLearning: false,
|
||||
allowPublicRead: false,
|
||||
hasUnpublishedChanges: true,
|
||||
hasUnpublishedDeletes: false,
|
||||
canEditLibrary: true,
|
||||
license: '',
|
||||
created: '2024-06-26',
|
||||
updated: '2024-07-20',
|
||||
};
|
||||
|
||||
interface WrapperProps {
|
||||
data: ContentLibrary,
|
||||
}
|
||||
|
||||
const RootWrapper = ({ data } : WrapperProps) => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ToastProvider>
|
||||
<LibraryInfoHeader library={data} />
|
||||
</ToastProvider>
|
||||
</QueryClientProvider>
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
describe('<LibraryInfoHeader />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
axiosMock.restore();
|
||||
});
|
||||
|
||||
it('should render Library info Header', () => {
|
||||
render(<RootWrapper data={libraryData} />);
|
||||
|
||||
expect(screen.getByText(libraryData.title)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /edit library name/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render edit title button without permission', () => {
|
||||
const data = {
|
||||
...libraryData,
|
||||
canEditLibrary: false,
|
||||
};
|
||||
|
||||
render(<RootWrapper data={data} />);
|
||||
|
||||
expect(screen.queryByRole('button', { name: /edit library name/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should edit library title', async () => {
|
||||
queryClient.getQueriesData = jest.fn().mockReturnValue([[null, { id: 1, title: 'Old Title' }]]);
|
||||
const url = getContentLibraryApiUrl(libraryData.id);
|
||||
axiosMock.onPatch(url).reply(200);
|
||||
render(<RootWrapper data={libraryData} />);
|
||||
|
||||
const editTitleButton = screen.getByRole('button', { name: /edit library name/i });
|
||||
fireEvent.click(editTitleButton);
|
||||
|
||||
const textBox = screen.getByRole('textbox', { name: /title input/i });
|
||||
|
||||
fireEvent.change(textBox, { target: { value: 'New Library Title' } });
|
||||
fireEvent.keyDown(textBox, { key: 'Enter', code: 'Enter', charCode: 13 });
|
||||
|
||||
expect(textBox).not.toBeInTheDocument();
|
||||
expect(await screen.findByText('Library updated successfully')).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => expect(axiosMock.history.patch[0].url).toEqual(url));
|
||||
});
|
||||
|
||||
it('should close edit library title on press Escape', async () => {
|
||||
const url = getContentLibraryApiUrl(libraryData.id);
|
||||
axiosMock.onPatch(url).reply(200);
|
||||
render(<RootWrapper data={libraryData} />);
|
||||
|
||||
const editTitleButton = screen.getByRole('button', { name: /edit library name/i });
|
||||
fireEvent.click(editTitleButton);
|
||||
|
||||
const textBox = screen.getByRole('textbox', { name: /title input/i });
|
||||
fireEvent.keyDown(textBox, { key: 'Escape', code: 'Escape', charCode: 27 });
|
||||
|
||||
expect(textBox).not.toBeInTheDocument();
|
||||
|
||||
await waitFor(() => expect(axiosMock.history.patch.length).toEqual(0));
|
||||
});
|
||||
|
||||
it('should show error on edit library tittle', async () => {
|
||||
const url = getContentLibraryApiUrl(libraryData.id);
|
||||
axiosMock.onPatch(url).reply(500);
|
||||
render(<RootWrapper data={libraryData} />);
|
||||
|
||||
const editTitleButton = screen.getByRole('button', { name: /edit library name/i });
|
||||
fireEvent.click(editTitleButton);
|
||||
|
||||
const textBox = screen.getByRole('textbox', { name: /title input/i });
|
||||
|
||||
fireEvent.change(textBox, { target: { value: 'New Library Title' } });
|
||||
fireEvent.keyDown(textBox, { key: 'Enter', code: 'Enter', charCode: 13 });
|
||||
|
||||
expect(await screen.findByText('There was an error updating the library')).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => expect(axiosMock.history.patch[0].url).toEqual(url));
|
||||
});
|
||||
});
|
||||
86
src/library-authoring/library-info/LibraryInfoHeader.tsx
Normal file
86
src/library-authoring/library-info/LibraryInfoHeader.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import React, { useState, useContext } from 'react';
|
||||
import {
|
||||
Icon,
|
||||
IconButton,
|
||||
Stack,
|
||||
Form,
|
||||
} from '@openedx/paragon';
|
||||
import { Edit } from '@openedx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
import { ContentLibrary } from '../data/api';
|
||||
import { useUpdateLibraryMetadata } from '../data/apiHooks';
|
||||
import { ToastContext } from '../../generic/toast-context';
|
||||
|
||||
type LibraryInfoHeaderProps = {
|
||||
library: ContentLibrary,
|
||||
};
|
||||
|
||||
const LibraryInfoHeader = ({ library } : LibraryInfoHeaderProps) => {
|
||||
const intl = useIntl();
|
||||
const [inputIsActive, setIsActive] = useState(false);
|
||||
const updateMutation = useUpdateLibraryMetadata();
|
||||
const { showToast } = useContext(ToastContext);
|
||||
|
||||
const handleSaveTitle = (event) => {
|
||||
const newTitle = event.target.value;
|
||||
if (newTitle && newTitle !== library.title) {
|
||||
updateMutation.mutateAsync({
|
||||
id: library.id,
|
||||
title: newTitle,
|
||||
}).then(() => {
|
||||
showToast(intl.formatMessage(messages.updateLibrarySuccessMsg));
|
||||
}).catch(() => {
|
||||
showToast(intl.formatMessage(messages.updateLibraryErrorMsg));
|
||||
});
|
||||
}
|
||||
setIsActive(false);
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
setIsActive(true);
|
||||
};
|
||||
|
||||
const hanldeOnKeyDown = (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
handleSaveTitle(event);
|
||||
} else if (event.key === 'Escape') {
|
||||
setIsActive(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack direction="horizontal">
|
||||
{ inputIsActive
|
||||
? (
|
||||
<Form.Control
|
||||
autoFocus
|
||||
name="title"
|
||||
id="title"
|
||||
type="text"
|
||||
aria-label="Title input"
|
||||
defaultValue={library.title}
|
||||
onBlur={handleSaveTitle}
|
||||
onKeyDown={hanldeOnKeyDown}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<span className="font-weight-bold m-1.5">
|
||||
{library.title}
|
||||
</span>
|
||||
{library.canEditLibrary && (
|
||||
<IconButton
|
||||
src={Edit}
|
||||
iconAs={Icon}
|
||||
alt={intl.formatMessage(messages.editNameButtonAlt)}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default LibraryInfoHeader;
|
||||
11
src/library-authoring/library-info/LibraryPublishStatus.scss
Normal file
11
src/library-authoring/library-info/LibraryPublishStatus.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
.library-publish-status {
|
||||
&.draft-status {
|
||||
background-color: #FDF3E9;
|
||||
border-top: 4px solid #F4B57B;
|
||||
}
|
||||
|
||||
&.published-status {
|
||||
background-color: $info-100;
|
||||
border-top: 4px solid $info-400;
|
||||
}
|
||||
}
|
||||
171
src/library-authoring/library-info/LibraryPublishStatus.tsx
Normal file
171
src/library-authoring/library-info/LibraryPublishStatus.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import React, { useCallback, useContext, useMemo } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Button, Container, Stack } from '@openedx/paragon';
|
||||
import { FormattedDate, FormattedTime, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useCommitLibraryChanges } from '../data/apiHooks';
|
||||
import { ContentLibrary } from '../data/api';
|
||||
import { ToastContext } from '../../generic/toast-context';
|
||||
import messages from './messages';
|
||||
|
||||
type LibraryPublishStatusProps = {
|
||||
library: ContentLibrary,
|
||||
};
|
||||
|
||||
const LibraryPublishStatus = ({ library } : LibraryPublishStatusProps) => {
|
||||
const intl = useIntl();
|
||||
const commitLibraryChanges = useCommitLibraryChanges();
|
||||
const { showToast } = useContext(ToastContext);
|
||||
|
||||
const commit = useCallback(() => {
|
||||
commitLibraryChanges.mutateAsync(library.id)
|
||||
.then(() => {
|
||||
showToast(intl.formatMessage(messages.publishSuccessMsg));
|
||||
}).catch(() => {
|
||||
showToast(intl.formatMessage(messages.publishErrorMsg));
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* TODO, the discard changes breaks the library.
|
||||
* Discomment this when discard changes is fixed.
|
||||
const revert = useCallback(() => {
|
||||
revertLibraryChanges.mutateAsync(library.id)
|
||||
.then(() => {
|
||||
showToast(intl.formatMessage(messages.revertSuccessMsg));
|
||||
}).catch(() => {
|
||||
showToast(intl.formatMessage(messages.revertErrorMsg));
|
||||
});
|
||||
}, []);
|
||||
*/
|
||||
|
||||
const {
|
||||
isPublished,
|
||||
statusMessage,
|
||||
extraStatusMessage,
|
||||
bodyMessage,
|
||||
} = useMemo(() => {
|
||||
let isPublishedResult: boolean;
|
||||
let statusMessageResult : string;
|
||||
let extraStatusMessageResult : string | undefined;
|
||||
let bodyMessageResult : string | undefined;
|
||||
|
||||
const buildDate = ((date : string) => (
|
||||
<b>
|
||||
<FormattedDate
|
||||
value={date}
|
||||
year="numeric"
|
||||
month="long"
|
||||
day="2-digit"
|
||||
/>
|
||||
</b>
|
||||
));
|
||||
|
||||
const buildTime = ((date: string) => (
|
||||
<b>
|
||||
<FormattedTime
|
||||
value={date}
|
||||
hour12={false}
|
||||
/>
|
||||
</b>
|
||||
));
|
||||
|
||||
const buildDraftBodyMessage = (() => {
|
||||
if (library.lastDraftCreatedBy && library.lastDraftCreated) {
|
||||
return intl.formatMessage(messages.lastDraftMsg, {
|
||||
date: buildDate(library.lastDraftCreated),
|
||||
time: buildTime(library.lastDraftCreated),
|
||||
user: <b>{library.lastDraftCreatedBy}</b>,
|
||||
});
|
||||
}
|
||||
if (library.lastDraftCreated) {
|
||||
return intl.formatMessage(messages.lastDraftMsgWithoutUser, {
|
||||
date: buildDate(library.lastDraftCreated),
|
||||
time: buildTime(library.lastDraftCreated),
|
||||
});
|
||||
}
|
||||
if (library.created) {
|
||||
return intl.formatMessage(messages.lastDraftMsgWithoutUser, {
|
||||
date: buildDate(library.created),
|
||||
time: buildTime(library.created),
|
||||
});
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
if (!library.lastPublished) {
|
||||
// Library is never published (new)
|
||||
isPublishedResult = false;
|
||||
statusMessageResult = intl.formatMessage(messages.draftStatusLabel);
|
||||
extraStatusMessageResult = intl.formatMessage(messages.neverPublishedLabel);
|
||||
bodyMessageResult = buildDraftBodyMessage();
|
||||
} else if (library.hasUnpublishedChanges || library.hasUnpublishedDeletes) {
|
||||
// Library is on Draft state
|
||||
isPublishedResult = false;
|
||||
statusMessageResult = intl.formatMessage(messages.draftStatusLabel);
|
||||
extraStatusMessageResult = intl.formatMessage(messages.unpublishedStatusLabel);
|
||||
bodyMessageResult = buildDraftBodyMessage();
|
||||
} else {
|
||||
// Library is published
|
||||
isPublishedResult = true;
|
||||
statusMessageResult = intl.formatMessage(messages.publishedStatusLabel);
|
||||
if (library.publishedBy) {
|
||||
bodyMessageResult = intl.formatMessage(messages.lastPublishedMsg, {
|
||||
date: buildDate(library.lastPublished),
|
||||
time: buildTime(library.lastPublished),
|
||||
user: <b>{library.publishedBy}</b>,
|
||||
});
|
||||
} else {
|
||||
bodyMessageResult = intl.formatMessage(messages.lastPublishedMsgWithoutUser, {
|
||||
date: buildDate(library.lastPublished),
|
||||
time: buildTime(library.lastPublished),
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
isPublished: isPublishedResult,
|
||||
statusMessage: statusMessageResult,
|
||||
extraStatusMessage: extraStatusMessageResult,
|
||||
bodyMessage: bodyMessageResult,
|
||||
};
|
||||
}, [library]);
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Container className={classNames('library-publish-status', {
|
||||
'draft-status': !isPublished,
|
||||
'published-status': isPublished,
|
||||
})}
|
||||
>
|
||||
<span className="font-weight-bold">
|
||||
{statusMessage}
|
||||
</span>
|
||||
{ extraStatusMessage && (
|
||||
<span className="ml-1">
|
||||
{extraStatusMessage}
|
||||
</span>
|
||||
)}
|
||||
</Container>
|
||||
<Container className="mt-3">
|
||||
<Stack gap={3}>
|
||||
<span>
|
||||
{bodyMessage}
|
||||
</span>
|
||||
<Button disabled={isPublished} onClick={commit}>
|
||||
{intl.formatMessage(messages.publishButtonLabel)}
|
||||
</Button>
|
||||
{ /*
|
||||
* TODO, the discard changes breaks the library.
|
||||
* Discomment this when discard changes is fixed.
|
||||
<div className="d-flex justify-content-end">
|
||||
<Button disabled={isPublished} variant="link" onClick={revert}>
|
||||
{intl.formatMessage(messages.discardChangesButtonLabel)}
|
||||
</Button>
|
||||
</div>
|
||||
*/ }
|
||||
</Stack>
|
||||
</Container>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default LibraryPublishStatus;
|
||||
2
src/library-authoring/library-info/index.ts
Normal file
2
src/library-authoring/library-info/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as LibraryInfo } from './LibraryInfo';
|
||||
export { default as LibraryInfoHeader } from './LibraryInfoHeader';
|
||||
111
src/library-authoring/library-info/messages.ts
Normal file
111
src/library-authoring/library-info/messages.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
editNameButtonAlt: {
|
||||
id: 'course-authoring.library-authoring.sidebar.info.edit-name.alt',
|
||||
defaultMessage: 'Edit library name',
|
||||
description: 'Alt text for edit library name icon button',
|
||||
},
|
||||
organizationSectionTitle: {
|
||||
id: 'course-authoring.library-authoring.sidebar.info.organization.title',
|
||||
defaultMessage: 'Organization',
|
||||
description: 'Title for Organization section in Library info sidebar.',
|
||||
},
|
||||
libraryHistorySectionTitle: {
|
||||
id: 'course-authoring.library-authoring.sidebar.info.history.title',
|
||||
defaultMessage: 'Library History',
|
||||
description: 'Title for Library History section in Library info sidebar.',
|
||||
},
|
||||
lastModifiedLabel: {
|
||||
id: 'course-authoring.library-authoring.sidebar.info.history.last-modified',
|
||||
defaultMessage: 'Last Modified',
|
||||
description: 'Last Modified label used in Library History section.',
|
||||
},
|
||||
createdLabel: {
|
||||
id: 'course-authoring.library-authoring.sidebar.info.history.created',
|
||||
defaultMessage: 'Created',
|
||||
description: 'Created label used in Library History section.',
|
||||
},
|
||||
draftStatusLabel: {
|
||||
id: 'course-authoring.library-authoring.sidebar.info.publish-status.draft',
|
||||
defaultMessage: 'Draft',
|
||||
description: 'Label in library info sidebar when the library is on draft status',
|
||||
},
|
||||
neverPublishedLabel: {
|
||||
id: 'course-authoring.library-authoring.sidebar.info.publish-status.never',
|
||||
defaultMessage: '(Never Published)',
|
||||
description: 'Label in library info sidebar when the library is never published',
|
||||
},
|
||||
unpublishedStatusLabel: {
|
||||
id: 'course-authoring.library-authoring.sidebar.info.publish-status.unpublished',
|
||||
defaultMessage: '(Unpublished Changes)',
|
||||
description: 'Label in library info sidebar when the library has unpublished changes',
|
||||
},
|
||||
publishedStatusLabel: {
|
||||
id: 'course-authoring.library-authoring.sidebar.info.publish-status.published',
|
||||
defaultMessage: 'Published',
|
||||
description: 'Label in library info sidebar when the library is on published status',
|
||||
},
|
||||
publishButtonLabel: {
|
||||
id: 'course-authoring.library-authoring.sidebar.info.publish-status.publish-button',
|
||||
defaultMessage: 'Publish',
|
||||
description: 'Label of publish button for a library.',
|
||||
},
|
||||
discardChangesButtonLabel: {
|
||||
id: 'course-authoring.library-authoring.sidebar.info.publish-status.discard-button',
|
||||
defaultMessage: 'Discard Changes',
|
||||
description: 'Label of discard changes button for a library.',
|
||||
},
|
||||
lastPublishedMsg: {
|
||||
id: 'course-authoring.library-authoring.sidebar.info.publish-status.last-published',
|
||||
defaultMessage: 'Last published on {date} at {time} UTC by {user}.',
|
||||
description: 'Body meesage of the library info sidebar when library is published.',
|
||||
},
|
||||
lastPublishedMsgWithoutUser: {
|
||||
id: 'course-authoring.library-authoring.sidebar.info.publish-status.last-published-no-user',
|
||||
defaultMessage: 'Last published on {date} at {time} UTC.',
|
||||
description: 'Body meesage of the library info sidebar when library is published.',
|
||||
},
|
||||
lastDraftMsg: {
|
||||
id: 'course-authoring.library-authoring.sidebar.info.publish-status.last-draft',
|
||||
defaultMessage: 'Draft saved on {date} at {time} UTC by {user}.',
|
||||
description: 'Body meesage of the library info sidebar when library is on draft status.',
|
||||
},
|
||||
lastDraftMsgWithoutUser: {
|
||||
id: 'course-authoring.library-authoring.sidebar.info.publish-status.last-draft-no-user',
|
||||
defaultMessage: 'Draft saved on {date} at {time} UTC.',
|
||||
description: 'Body meesage of the library info sidebar when library is on draft status.',
|
||||
},
|
||||
publishSuccessMsg: {
|
||||
id: 'course-authoring.library-authoring.publish.success',
|
||||
defaultMessage: 'Library published successfully',
|
||||
description: 'Message when the library is published successfully.',
|
||||
},
|
||||
publishErrorMsg: {
|
||||
id: 'course-authoring.library-authoring.publish.error',
|
||||
defaultMessage: 'There was an error publishing the library.',
|
||||
description: 'Message when there is an error when publishing the library.',
|
||||
},
|
||||
revertSuccessMsg: {
|
||||
id: 'course-authoring.library-authoring.revert.success',
|
||||
defaultMessage: 'Library changes reverted successfully',
|
||||
description: 'Message when the library changes are reverted successfully.',
|
||||
},
|
||||
revertErrorMsg: {
|
||||
id: 'course-authoring.library-authoring.publish.error',
|
||||
defaultMessage: 'There was an error reverting changes in the library.',
|
||||
description: 'Message when there is an error when reverting changes in the library.',
|
||||
},
|
||||
updateLibrarySuccessMsg: {
|
||||
id: 'course-authoring.library-authoring.library.update.success',
|
||||
defaultMessage: 'Library updated successfully',
|
||||
description: 'Message when the library is updated successfully',
|
||||
},
|
||||
updateLibraryErrorMsg: {
|
||||
id: 'course-authoring.library-authoring.library.update.error',
|
||||
defaultMessage: 'There was an error updating the library',
|
||||
description: 'Message when there is an error when updating the library',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -7,8 +7,14 @@ import {
|
||||
import { Close } from '@openedx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import messages from '../messages';
|
||||
import { AddContentContainer } from '../add-content';
|
||||
import { LibraryContext } from '../common/context';
|
||||
import { AddContentContainer, AddContentHeader } from '../add-content';
|
||||
import { LibraryContext, SidebarBodyComponentId } from '../common/context';
|
||||
import { LibraryInfo, LibraryInfoHeader } from '../library-info';
|
||||
import { ContentLibrary } from '../data/api';
|
||||
|
||||
type LibrarySidebarProps = {
|
||||
library: ContentLibrary,
|
||||
};
|
||||
|
||||
/**
|
||||
* Sidebar container for library pages.
|
||||
@@ -19,23 +25,29 @@ import { LibraryContext } from '../common/context';
|
||||
* You can add more components in `bodyComponentMap`.
|
||||
* Use the slice actions to open and close this sidebar.
|
||||
*/
|
||||
const LibrarySidebar = () => {
|
||||
const LibrarySidebar = ({ library }: LibrarySidebarProps) => {
|
||||
const intl = useIntl();
|
||||
const { sidebarBodyComponent, closeLibrarySidebar } = useContext(LibraryContext);
|
||||
|
||||
const bodyComponentMap = {
|
||||
'add-content': <AddContentContainer />,
|
||||
[SidebarBodyComponentId.AddContent]: <AddContentContainer />,
|
||||
[SidebarBodyComponentId.Info]: <LibraryInfo library={library} />,
|
||||
unknown: null,
|
||||
};
|
||||
|
||||
const headerComponentMap = {
|
||||
'add-content': <AddContentHeader />,
|
||||
info: <LibraryInfoHeader library={library} />,
|
||||
unknown: null,
|
||||
};
|
||||
|
||||
const buildBody = () : React.ReactNode | null => bodyComponentMap[sidebarBodyComponent || 'unknown'];
|
||||
const buildHeader = (): React.ReactNode | null => headerComponentMap[sidebarBodyComponent || 'unknown'];
|
||||
|
||||
return (
|
||||
<div className="p-2 vh-100">
|
||||
<Stack gap={4} className="p-2 vh-100 text-primary-700">
|
||||
<Stack direction="horizontal" className="d-flex justify-content-between">
|
||||
<span className="font-weight-bold m-1.5">
|
||||
{intl.formatMessage(messages.addContentTitle)}
|
||||
</span>
|
||||
{buildHeader()}
|
||||
<IconButton
|
||||
src={Close}
|
||||
iconAs={Icon}
|
||||
@@ -44,8 +56,10 @@ const LibrarySidebar = () => {
|
||||
variant="black"
|
||||
/>
|
||||
</Stack>
|
||||
{buildBody()}
|
||||
</div>
|
||||
<div>
|
||||
{buildBody()}
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -100,6 +100,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Close',
|
||||
description: 'Alt text of close button',
|
||||
},
|
||||
libraryInfoButton: {
|
||||
id: 'course-authoring.library-authoring.buttons.library-info.text',
|
||||
defaultMessage: 'Library Info',
|
||||
description: 'Text of button to open "Library Info sidebar"',
|
||||
},
|
||||
readOnlyBadge: {
|
||||
id: 'course-authoring.library-authoring.badge.read-only',
|
||||
defaultMessage: 'Read Only',
|
||||
|
||||
Reference in New Issue
Block a user