diff --git a/package-lock.json b/package-lock.json index ad9dce24..108e9d5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1383,9 +1383,9 @@ } }, "@edx/frontend-lib-special-exams": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@edx/frontend-lib-special-exams/-/frontend-lib-special-exams-1.9.0.tgz", - "integrity": "sha512-dG9iW/vru/6Rh9+1kkqDu3gdiSVF49/9JvdjPb+7H1mdZPs4/FdP43HicPgaJ4r6PKfd9Ssam+3rq1NhxpWzvw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@edx/frontend-lib-special-exams/-/frontend-lib-special-exams-1.11.0.tgz", + "integrity": "sha512-T8b5caCjYgqVstnFlT5o50KnLsMmqIkBUXjwP9OCFlc8wDo2MUuc58nZ0xJUGaL2C0RGbtILPH+pzlXtPubrOw==", "requires": { "@fortawesome/fontawesome-svg-core": "1.2.34", "@fortawesome/free-brands-svg-icons": "5.11.2", @@ -1447,9 +1447,9 @@ } }, "@edx/paragon": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-15.2.2.tgz", - "integrity": "sha512-C4YMd4zjRalS8pPglpAvDnribrA/3x8XXcbyTq0Xwwotp9HSld2yndASczZGdjNcqG0b1gpmPdxkzx2kaogCiw==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-16.5.0.tgz", + "integrity": "sha512-oXZBL1eaBKtPnypVx3hAoVs/zwTjQP+jfj6Vk610+GlsoYzevho4H2CXvrym5HKYAvYvKwXuIibh/IOdzhk+tQ==", "requires": { "@fortawesome/fontawesome-svg-core": "^1.2.30", "@fortawesome/free-solid-svg-icons": "^5.14.0", @@ -1467,10 +1467,9 @@ "react-focus-on": "^3.5.0", "react-popper": "^2.2.4", "react-proptype-conditional-require": "^1.0.4", - "react-responsive": "^6.1.1", + "react-responsive": "^8.2.0", "react-table": "^7.6.1", "react-transition-group": "^4.0.0", - "sanitize-html": "^1.27.5", "tabbable": "^4.0.0", "uncontrollable": "7.2.1" }, @@ -3081,9 +3080,9 @@ } }, "@types/react-transition-group": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.1.tgz", - "integrity": "sha512-vIo69qKKcYoJ8wKCJjwSgCTM+z3chw3g18dkrDfVX665tMH7tmbDxEAnPdey4gTlwZz5QuHGzd+hul0OVZDqqQ==", + "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==", "requires": { "@types/react": "*" } @@ -3592,6 +3591,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -5420,6 +5420,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -5430,6 +5431,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, "requires": { "has-flag": "^3.0.0" } @@ -5809,6 +5811,7 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, "requires": { "color-name": "1.1.3" } @@ -5816,7 +5819,8 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true }, "color-string": { "version": "1.5.5", @@ -7084,37 +7088,12 @@ "csstype": "^3.0.2" } }, - "dom-serializer": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", - "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "dependencies": { - "domhandler": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz", - "integrity": "sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==", - "requires": { - "domelementtype": "^2.2.0" - } - } - } - }, "domain-browser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", "dev": true }, - "domelementtype": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", - "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" - }, "domexception": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", @@ -7132,34 +7111,6 @@ } } }, - "domhandler": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", - "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", - "requires": { - "domelementtype": "^2.0.1" - } - }, - "domutils": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz", - "integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==", - "requires": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "dependencies": { - "domhandler": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz", - "integrity": "sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==", - "requires": { - "domelementtype": "^2.2.0" - } - } - } - }, "dot-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", @@ -7605,7 +7556,8 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true }, "escodegen": { "version": "2.0.0", @@ -9417,7 +9369,8 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true }, "has-symbol-support-x": { "version": "1.4.2", @@ -9718,17 +9671,6 @@ } } }, - "htmlparser2": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.1.0.tgz", - "integrity": "sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q==", - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^3.0.0", - "domutils": "^2.0.0", - "entities": "^2.0.0" - } - }, "http-cache-semantics": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", @@ -14184,7 +14126,8 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, "lodash.assignin": { "version": "4.2.0", @@ -15915,11 +15858,6 @@ "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", "dev": true }, - "parse-srcset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", - "integrity": "sha1-8r0iH2zJcKk42IVWq8WJyqqiveE=" - }, "parse5": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", @@ -16269,6 +16207,7 @@ "version": "7.0.35", "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", + "dev": true, "requires": { "chalk": "^2.4.2", "source-map": "^0.6.1", @@ -17621,16 +17560,16 @@ "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" }, "react-focus-lock": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.5.1.tgz", - "integrity": "sha512-gOToRZKVEymGEjFaTRUKgJsdYQrNosoiK7yZnXnnd8bYew4vMzk3Rxb0Q4nyrGwsFuUmgQiSAulQirA0J+v4hA==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.5.2.tgz", + "integrity": "sha512-WzpdOnEqjf+/A3EH9opMZWauag7gV0BxFl+EY4ElA4qFqYsUsBLnmo2sELbN5OC30S16GAWMy16B9DLPpdJKAQ==", "requires": { "@babel/runtime": "^7.0.0", "focus-lock": "^0.9.1", "prop-types": "^15.6.2", - "react-clientside-effect": "^1.2.2", - "use-callback-ref": "^1.2.1", - "use-sidecar": "^1.0.1" + "react-clientside-effect": "^1.2.5", + "use-callback-ref": "^1.2.5", + "use-sidecar": "^1.0.5" } }, "react-focus-on": { @@ -17687,9 +17626,9 @@ "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, "react-overlays": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.1.0.tgz", - "integrity": "sha512-Qp8dqDIIYgQoHxOGVKHwvQUkDe70/Ja/6dn8iCQAXyPvvpks3+T8scLTZLK8MPBBu+X8ustas6y4U6M6zdmCjA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.1.1.tgz", + "integrity": "sha512-eCN2s2/+GVZzpnId4XVWtvDPYYBD2EtOGP74hE+8yDskPzFy9+pV1H3ZZihxuRdEbQzzacySaaDkR7xE0ydl4Q==", "requires": { "@babel/runtime": "^7.13.8", "@popperjs/core": "^2.8.6", @@ -17729,9 +17668,9 @@ } }, "react-remove-scroll": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.4.2.tgz", - "integrity": "sha512-mMSIZYQF3jS2uRJXeFDRaVGA+BGs/hIryV64YUKsHFtpgwZloOUcdu0oW8K6OU8uLHt/kM5d0lUZbdpIVwgXtQ==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.4.3.tgz", + "integrity": "sha512-lGWYXfV6jykJwbFpsuPdexKKzp96f3RbvGapDSIdcyGvHb7/eqyn46C7/6h+rUzYar1j5mdU+XECITHXCKBk9Q==", "requires": { "react-remove-scroll-bar": "^2.1.0", "react-style-singleton": "^2.1.0", @@ -17750,13 +17689,14 @@ } }, "react-responsive": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-6.1.2.tgz", - "integrity": "sha512-AXentVC/kN3KED9zhzJv2pu4vZ0i6cSHdTtbCScVV1MT6F5KXaG2qs5D7WLmhdaOvmiMX8UfmS4ZSO+WPwDt4g==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-8.2.0.tgz", + "integrity": "sha512-iagCqVrw4QSjhxKp3I/YK6+ODkWY6G+YPElvdYKiUUbywwh9Ds0M7r26Fj2/7dWFFbOpcGnJE6uE7aMck8j5Qg==", "requires": { "hyphenate-style-name": "^1.0.0", "matchmediaquery": "^0.3.0", - "prop-types": "^15.6.1" + "prop-types": "^15.6.1", + "shallow-equal": "^1.1.0" } }, "react-router": { @@ -18749,17 +18689,6 @@ "walker": "~1.0.5" } }, - "sanitize-html": { - "version": "1.27.5", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-1.27.5.tgz", - "integrity": "sha512-M4M5iXDAUEcZKLXkmk90zSYWEtk5NH3JmojQxKxV371fnMh+x9t1rqdmXaGoyEHw3z/X/8vnFhKjGL5xFGOJ3A==", - "requires": { - "htmlparser2": "^4.1.0", - "lodash": "^4.17.15", - "parse-srcset": "^1.0.2", - "postcss": "^7.0.27" - } - }, "sass": { "version": "1.26.11", "resolved": "https://registry.npmjs.org/sass/-/sass-1.26.11.tgz", @@ -19057,6 +18986,11 @@ "kind-of": "^6.0.2" } }, + "shallow-equal": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz", + "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==" + }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -19352,7 +19286,8 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true }, "source-map-loader": { "version": "0.2.4", @@ -20106,6 +20041,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, "requires": { "has-flag": "^3.0.0" } diff --git a/package.json b/package.json index d437bd0e..39bb6e27 100644 --- a/package.json +++ b/package.json @@ -38,9 +38,9 @@ "@edx/brand": "npm:@edx/brand-openedx@1.1.0", "@edx/frontend-component-footer": "10.1.5", "@edx/frontend-enterprise": "4.2.3", - "@edx/frontend-lib-special-exams": "1.9.0", + "@edx/frontend-lib-special-exams": "1.11.0", "@edx/frontend-platform": "1.11.0", - "@edx/paragon": "15.2.2", + "@edx/paragon": "16.5.0", "@fortawesome/fontawesome-svg-core": "1.2.34", "@fortawesome/free-brands-svg-icons": "5.13.1", "@fortawesome/free-regular-svg-icons": "5.13.1", diff --git a/src/alerts/access-expiration-alert/AccessExpirationAlert.jsx b/src/alerts/access-expiration-alert/AccessExpirationAlert.jsx index db627764..d40d28f7 100644 --- a/src/alerts/access-expiration-alert/AccessExpirationAlert.jsx +++ b/src/alerts/access-expiration-alert/AccessExpirationAlert.jsx @@ -4,9 +4,9 @@ import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { FormattedMessage, FormattedDate, injectIntl, intlShape, } from '@edx/frontend-platform/i18n'; -import { Hyperlink } from '@edx/paragon'; +import { Alert, Hyperlink } from '@edx/paragon'; +import { Info } from '@edx/paragon/icons'; -import { Alert, ALERT_TYPES } from '../../generic/user-messages'; import messages from './messages'; import AccessExpirationAlertMMP2P from './AccessExpirationAlertMMP2P'; import AccessExpirationAlertMasquerade from './AccessExpirationAlertMasquerade'; @@ -100,7 +100,7 @@ function AccessExpirationAlert({ intl, payload }) { } return ( - + + Unlock full course content by {formatDate(upgradeDeadline, 'upgradeTitle')} diff --git a/src/alerts/access-expiration-alert/AccessExpirationAlertMasquerade.jsx b/src/alerts/access-expiration-alert/AccessExpirationAlertMasquerade.jsx index 305cedb3..a71df84f 100644 --- a/src/alerts/access-expiration-alert/AccessExpirationAlertMasquerade.jsx +++ b/src/alerts/access-expiration-alert/AccessExpirationAlertMasquerade.jsx @@ -1,8 +1,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n'; - -import { Alert, ALERT_TYPES } from '../../generic/user-messages'; +import { Alert } from '@edx/paragon'; +import { Info } from '@edx/paragon/icons'; function AccessExpirationAlertMasquerade({ payload }) { const { @@ -26,7 +26,7 @@ function AccessExpirationAlertMasquerade({ payload }) { const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {}; return ( - + + ); return ( - - {text} - {' '} - {button} - {' '} - {loading && } + +
+ {text} + {button} + {loading && } +
); } diff --git a/src/alerts/logistration-alert/LogistrationAlert.jsx b/src/alerts/logistration-alert/LogistrationAlert.jsx index 3271329d..be72180e 100644 --- a/src/alerts/logistration-alert/LogistrationAlert.jsx +++ b/src/alerts/logistration-alert/LogistrationAlert.jsx @@ -2,9 +2,9 @@ import React from 'react'; import { getConfig } from '@edx/frontend-platform'; import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n'; import { getLoginRedirectUrl } from '@edx/frontend-platform/auth'; -import { Hyperlink } from '@edx/paragon'; +import { Alert, Hyperlink } from '@edx/paragon'; +import { WarningFilled } from '@edx/paragon/icons'; -import { Alert } from '../../generic/user-messages'; import genericMessages from '../../generic/messages'; function LogistrationAlert({ intl }) { @@ -29,7 +29,7 @@ function LogistrationAlert({ intl }) { ); return ( - + { - sendTrackEvent('edx.bi.ecommerce.upsell_links_clicked', { - org_key: org, - courserun_key: courseId, - linkCategory: 'welcome', - linkName: `${analyticsPageName}_welcome`, - linkType: 'link', - pageName: analyticsPageName, - }); - }; - - return ( - - - - ), - fullPricing: , - percentage, - }} - /> - -
- {code}), - }} - /> -   - - {intl.formatMessage(messages.upgradeNow)} - -
- ); -} - -OfferAlert.propTypes = { - intl: intlShape.isRequired, - payload: PropTypes.shape({ - courseId: PropTypes.string.isRequired, - offer: PropTypes.shape({ - code: PropTypes.string.isRequired, - discountedPrice: PropTypes.string.isRequired, - expirationDate: PropTypes.string.isRequired, - originalPrice: PropTypes.string.isRequired, - percentage: PropTypes.number.isRequired, - upgradeUrl: PropTypes.string.isRequired, - }).isRequired, - org: PropTypes.string.isRequired, - userTimezone: PropTypes.string.isRequired, - analyticsPageName: PropTypes.string.isRequired, - }).isRequired, -}; - -export default injectIntl(OfferAlert); diff --git a/src/alerts/offer-alert/hooks.js b/src/alerts/offer-alert/hooks.js deleted file mode 100644 index be86db3e..00000000 --- a/src/alerts/offer-alert/hooks.js +++ /dev/null @@ -1,25 +0,0 @@ -import React, { useMemo } from 'react'; -import { useAlert } from '../../generic/user-messages'; - -const OfferAlert = React.lazy(() => import('./OfferAlert')); - -export function useOfferAlert(courseId, offer, org, userTimezone, topic, analyticsPageName) { - const isVisible = !!offer; // if it exists, show it. - const payload = { - analyticsPageName, - courseId, - offer, - org, - userTimezone, - }; - - useAlert(isVisible, { - code: 'clientOfferAlert', - topic, - payload: useMemo(() => payload, Object.values(payload).sort()), - }); - - return { clientOfferAlert: OfferAlert }; -} - -export default useOfferAlert; diff --git a/src/alerts/offer-alert/index.js b/src/alerts/offer-alert/index.js deleted file mode 100644 index ed12eb0b..00000000 --- a/src/alerts/offer-alert/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './hooks'; diff --git a/src/alerts/offer-alert/messages.js b/src/alerts/offer-alert/messages.js deleted file mode 100644 index 149c0d07..00000000 --- a/src/alerts/offer-alert/messages.js +++ /dev/null @@ -1,10 +0,0 @@ -import { defineMessages } from '@edx/frontend-platform/i18n'; - -const messages = defineMessages({ - upgradeNow: { - id: 'learning.offer.upgradeNow', - defaultMessage: 'Upgrade now', - }, -}); - -export default messages; diff --git a/src/course-header/CourseTabsNavigation.jsx b/src/course-header/CourseTabsNavigation.jsx index efa4aa83..62366adf 100644 --- a/src/course-header/CourseTabsNavigation.jsx +++ b/src/course-header/CourseTabsNavigation.jsx @@ -11,7 +11,7 @@ function CourseTabsNavigation({ }) { return (
-
+
{intl.formatMessage(messages.skipNavLink)} -
+
{headerLogo}
{courseOrg} {courseNumber} diff --git a/src/course-home/data/__factories__/courseHomeMetadata.factory.js b/src/course-home/data/__factories__/courseHomeMetadata.factory.js index 00a2b3ca..43539ca4 100644 --- a/src/course-home/data/__factories__/courseHomeMetadata.factory.js +++ b/src/course-home/data/__factories__/courseHomeMetadata.factory.js @@ -10,4 +10,13 @@ Factory.define('courseHomeMetadata') is_self_paced: false, is_enrolled: false, can_load_courseware: false, + course_access: { + additional_context_user_message: null, + developer_message: null, + error_code: null, + has_access: true, + user_fragment: null, + user_message: null, + }, + start: '2013-02-05T05:00:00Z', }); diff --git a/src/course-home/data/__factories__/progressTabData.factory.js b/src/course-home/data/__factories__/progressTabData.factory.js index 6e2a1987..ab21a13e 100644 --- a/src/course-home/data/__factories__/progressTabData.factory.js +++ b/src/course-home/data/__factories__/progressTabData.factory.js @@ -24,10 +24,12 @@ Factory.define('progressTabData') assignment_type: 'Homework', block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345', display_name: 'First subsection', + learner_has_access: true, has_graded_assignment: true, num_points_earned: 0, - num_points_possible: 1, + num_points_possible: 3, percent_graded: 0.0, + problem_scores: [{ earned: 0, possible: 1 }, { earned: 0, possible: 1 }, { earned: 0, possible: 1 }], show_correctness: 'always', show_grades: true, url: 'http://learning.edx.org/course/course-v1:edX+Test+run/first_subsection', @@ -44,6 +46,7 @@ Factory.define('progressTabData') num_points_earned: 1, num_points_possible: 1, percent_graded: 1.0, + problem_scores: [{ earned: 1, possible: 1 }], show_correctness: 'always', show_grades: true, url: 'http://learning.edx.org/course/course-v1:edX+Test+run/second_subsection', diff --git a/src/course-home/data/__snapshots__/redux.test.js.snap b/src/course-home/data/__snapshots__/redux.test.js.snap index 283e0455..504c866e 100644 --- a/src/course-home/data/__snapshots__/redux.test.js.snap +++ b/src/course-home/data/__snapshots__/redux.test.js.snap @@ -5,7 +5,6 @@ Object { "courseHome": Object { "courseId": "course-v1:edX+DemoX+Demo_Course_1", "courseStatus": "loaded", - "gradesFeatureIsLocked": false, "targetUserId": undefined, "toastBodyLink": null, "toastBodyText": null, @@ -23,6 +22,14 @@ Object { "courseHomeMeta": Object { "course-v1:edX+DemoX+Demo_Course_1": Object { "canLoadCourseware": false, + "courseAccess": Object { + "additionalContextUserMessage": null, + "developerMessage": null, + "errorCode": null, + "hasAccess": true, + "userFragment": null, + "userMessage": null, + }, "id": "course-v1:edX+DemoX+Demo_Course_1", "isEnrolled": false, "isSelfPaced": false, @@ -30,6 +37,7 @@ Object { "number": "DemoX", "org": "edX", "originalUserIsStaff": false, + "start": "2013-02-05T05:00:00Z", "tabs": Array [ Object { "slug": "outline", @@ -302,7 +310,6 @@ Object { "courseHome": Object { "courseId": "course-v1:edX+DemoX+Demo_Course_1", "courseStatus": "loaded", - "gradesFeatureIsLocked": false, "targetUserId": undefined, "toastBodyLink": null, "toastBodyText": null, @@ -320,6 +327,14 @@ Object { "courseHomeMeta": Object { "course-v1:edX+DemoX+Demo_Course_1": Object { "canLoadCourseware": false, + "courseAccess": Object { + "additionalContextUserMessage": null, + "developerMessage": null, + "errorCode": null, + "hasAccess": true, + "userFragment": null, + "userMessage": null, + }, "id": "course-v1:edX+DemoX+Demo_Course_1", "isEnrolled": false, "isSelfPaced": false, @@ -327,6 +342,7 @@ Object { "number": "DemoX", "org": "edX", "originalUserIsStaff": false, + "start": "2013-02-05T05:00:00Z", "tabs": Array [ Object { "slug": "outline", @@ -380,8 +396,6 @@ Object { "courseBlocks": Object { "courses": Object { "block-v1:edX+DemoX+Demo_Course+type@course+block@bcdabcdabcdabcdabcdabcdabcdabcd3": Object { - "effortActivities": undefined, - "effortTime": undefined, "hasScheduledContent": false, "id": "course-v1:edX+DemoX+Demo_Course_1", "sectionIds": Array [ @@ -394,8 +408,6 @@ Object { "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2": Object { "complete": false, "courseId": "course-v1:edX+DemoX+Demo_Course_1", - "effortActivities": 2, - "effortTime": 15, "id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2", "resumeBlock": false, "sequenceIds": Array [ @@ -409,8 +421,8 @@ Object { "complete": false, "description": null, "due": null, - "effortActivities": undefined, - "effortTime": undefined, + "effortActivities": 2, + "effortTime": 15, "icon": null, "id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1", "legacyWebUrl": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1?experience=legacy", @@ -482,7 +494,6 @@ Object { "courseHome": Object { "courseId": "course-v1:edX+DemoX+Demo_Course_1", "courseStatus": "loaded", - "gradesFeatureIsLocked": false, "targetUserId": undefined, "toastBodyLink": null, "toastBodyText": null, @@ -500,6 +511,14 @@ Object { "courseHomeMeta": Object { "course-v1:edX+DemoX+Demo_Course_1": Object { "canLoadCourseware": false, + "courseAccess": Object { + "additionalContextUserMessage": null, + "developerMessage": null, + "errorCode": null, + "hasAccess": true, + "userFragment": null, + "userMessage": null, + }, "id": "course-v1:edX+DemoX+Demo_Course_1", "isEnrolled": false, "isSelfPaced": false, @@ -507,6 +526,7 @@ Object { "number": "DemoX", "org": "edX", "originalUserIsStaff": false, + "start": "2013-02-05T05:00:00Z", "tabs": Array [ Object { "slug": "outline", @@ -564,6 +584,8 @@ Object { "courseId": "course-v1:edX+DemoX+Demo_Course_1", "end": "3027-03-31T00:00:00Z", "enrollmentMode": "audit", + "gradesFeatureIsFullyLocked": false, + "gradesFeatureIsPartiallyLocked": false, "gradingPolicy": Object { "assignmentPolicies": Array [ Object { @@ -590,9 +612,24 @@ Object { "blockKey": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345", "displayName": "First subsection", "hasGradedAssignment": true, + "learnerHasAccess": true, "numPointsEarned": 0, - "numPointsPossible": 1, + "numPointsPossible": 3, "percentGraded": 0, + "problemScores": Array [ + Object { + "earned": 0, + "possible": 1, + }, + Object { + "earned": 0, + "possible": 1, + }, + Object { + "earned": 0, + "possible": 1, + }, + ], "showCorrectness": "always", "showGrades": true, "url": "http://learning.edx.org/course/course-v1:edX+Test+run/first_subsection", @@ -609,6 +646,12 @@ Object { "numPointsEarned": 1, "numPointsPossible": 1, "percentGraded": 1, + "problemScores": Array [ + Object { + "earned": 1, + "possible": 1, + }, + ], "showCorrectness": "always", "showGrades": true, "url": "http://learning.edx.org/course/course-v1:edX+Test+run/second_subsection", diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js index 5b9d8684..556e8779 100644 --- a/src/course-home/data/api.js +++ b/src/course-home/data/api.js @@ -111,8 +111,6 @@ export function normalizeOutlineBlocks(courseId, blocks) { switch (block.type) { case 'course': models.courses[block.id] = { - effortActivities: block.effort_activities, - effortTime: block.effort_time, id: courseId, title: block.display_name, sectionIds: block.children || [], @@ -123,8 +121,6 @@ export function normalizeOutlineBlocks(courseId, blocks) { case 'chapter': models.sections[block.id] = { complete: block.complete, - effortActivities: block.effort_activities, - effortTime: block.effort_time, id: block.id, title: block.display_name, resumeBlock: block.resume_block, @@ -253,6 +249,26 @@ export async function getProgressTabData(courseId, targetUserId) { // in order to preserve a course team's desired grade formatting. camelCasedData.gradingPolicy.gradeRange = data.grading_policy.grade_range; + camelCasedData.gradesFeatureIsFullyLocked = camelCasedData.completionSummary.lockedCount > 0; + + camelCasedData.gradesFeatureIsPartiallyLocked = false; + if (camelCasedData.gradesFeatureIsFullyLocked) { + camelCasedData.sectionScores.forEach((chapter) => { + chapter.subsections.forEach((subsection) => { + // If something is eligible to be gated by content type gating and would show up on the progress page + if (subsection.assignmentType !== null && subsection.hasGradedAssignment && subsection.showGrades + && (subsection.numPointsPossible > 0 || subsection.numPointsEarned > 0)) { + // but the learner still has access to it, then we are in a partially locked, rather than fully locked state + // since the learner has access to some (but not all) content that would normally be locked + if (subsection.learnerHasAccess) { + camelCasedData.gradesFeatureIsPartiallyLocked = true; + camelCasedData.gradesFeatureIsFullyLocked = false; + } + } + }); + }); + } + return camelCasedData; } catch (error) { const { httpErrorStatus } = error && error.customAttributes; @@ -268,7 +284,7 @@ export async function getProgressTabData(courseId, targetUserId) { } export async function getProctoringInfoData(courseId, username) { - let url = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/user_onboarding/status?course_id=${encodeURIComponent(courseId)}`; + let url = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/user_onboarding/status?is_learning_mfe=true&course_id=${encodeURIComponent(courseId)}`; if (username) { url += `&username=${encodeURIComponent(username)}`; } diff --git a/src/course-home/data/slice.js b/src/course-home/data/slice.js index 4534887e..b195b420 100644 --- a/src/course-home/data/slice.js +++ b/src/course-home/data/slice.js @@ -4,18 +4,26 @@ import { createSlice } from '@reduxjs/toolkit'; export const LOADING = 'loading'; export const LOADED = 'loaded'; export const FAILED = 'failed'; +export const DENIED = 'denied'; const slice = createSlice({ name: 'course-home', initialState: { courseStatus: 'loading', courseId: null, - gradesFeatureIsLocked: false, toastBodyText: null, toastBodyLink: null, toastHeader: '', }, reducers: { + fetchTabDenied: (state, { payload }) => { + state.courseId = payload.courseId; + state.courseStatus = DENIED; + }, + fetchTabFailure: (state, { payload }) => { + state.courseId = payload.courseId; + state.courseStatus = FAILED; + }, fetchTabRequest: (state, { payload }) => { state.courseId = payload.courseId; state.courseStatus = LOADING; @@ -25,10 +33,6 @@ const slice = createSlice({ state.targetUserId = payload.targetUserId; state.courseStatus = LOADED; }, - fetchTabFailure: (state, { payload }) => { - state.courseId = payload.courseId; - state.courseStatus = FAILED; - }, setCallToActionToast: (state, { payload }) => { const { header, @@ -39,18 +43,15 @@ const slice = createSlice({ state.toastBodyText = linkText; state.toastHeader = header; }, - setGradesFeatureStatus: (state, { payload }) => { - state.gradesFeatureIsLocked = payload.gradesFeatureIsLocked; - }, }, }); export const { + fetchTabDenied, + fetchTabFailure, fetchTabRequest, fetchTabSuccess, - fetchTabFailure, setCallToActionToast, - setGradesFeatureStatus, } = slice.actions; export const { diff --git a/src/course-home/data/thunks.js b/src/course-home/data/thunks.js index f95a7052..1002a7b0 100644 --- a/src/course-home/data/thunks.js +++ b/src/course-home/data/thunks.js @@ -17,6 +17,7 @@ import { } from '../../generic/model-store'; import { + // fetchTabDenied, fetchTabFailure, fetchTabRequest, fetchTabSuccess, @@ -61,6 +62,10 @@ export function fetchTab(courseId, tab, getTabData, targetUserId) { logError(tabDataResult.reason); } + // Disable the access-denied path for now - it caused a regression + /* if (fetchedCourseHomeCourseMetadata && !courseHomeCourseMetadataResult.value.courseAccess.hasAccess) { + dispatch(fetchTabDenied({ courseId })); + } else */ if (fetchedCourseHomeCourseMetadata && fetchedTabData) { dispatch(fetchTabSuccess({ courseId, targetUserId })); } else { diff --git a/src/course-home/dates-tab/DatesTab.test.jsx b/src/course-home/dates-tab/DatesTab.test.jsx index aaa40704..d0937772 100644 --- a/src/course-home/dates-tab/DatesTab.test.jsx +++ b/src/course-home/dates-tab/DatesTab.test.jsx @@ -23,19 +23,24 @@ jest.mock('@edx/frontend-platform/analytics'); describe('DatesTab', () => { let axiosMock; + let store; + let component; - const store = initializeStore(); - const component = ( - - - - - - - - - - ); + beforeEach(() => { + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + store = initializeStore(); + component = ( + + + + + + + + + + ); + }); const datesTabData = Factory.build('datesTabData'); let courseMetadata = Factory.build('courseHomeMetadata'); @@ -74,7 +79,6 @@ describe('DatesTab', () => { describe('when receiving a full set of dates data', () => { beforeEach(() => { - axiosMock = new MockAdapter(getAuthenticatedHttpClient()); axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata); axiosMock.onGet(datesUrl).reply(200, datesTabData); history.push(`/course/${courseId}/dates`); // so tab can pull course id from url @@ -142,7 +146,6 @@ describe('DatesTab', () => { describe('Suggested schedule messaging', () => { beforeEach(() => { - axiosMock = new MockAdapter(getAuthenticatedHttpClient()); setMetadata({ is_self_paced: true, is_enrolled: true }); history.push(`/course/${courseId}/dates`); }); @@ -295,4 +298,45 @@ describe('DatesTab', () => { }); }); }); + + describe.skip('when receiving an access denied error', () => { + // These tests could go into any particular tab, as they all go through the same flow. But dates tab works. + + async function renderDenied(errorCode) { + setMetadata({ + course_access: { + has_access: false, + error_code: errorCode, + additional_context_user_message: 'uhoh oh no', // only used by audit_expired + }, + }); + render(component); + await waitForElementToBeRemoved(screen.getByRole('status')); + } + + beforeEach(() => { + axiosMock.onGet(datesUrl).reply(200, datesTabData); + history.push(`/course/${courseId}/dates`); // so tab can pull course id from url + }); + + it('redirects to course survey for a survey_required error code', async () => { + await renderDenied('survey_required'); + expect(global.location.href).toEqual(`http://localhost/redirect/survey/${courseMetadata.id}`); + }); + + it('redirects to dashboard for an unfulfilled_milestones error code', async () => { + await renderDenied('unfulfilled_milestones'); + expect(global.location.href).toEqual('http://localhost/redirect/dashboard'); + }); + + it('redirects to the dashboard with an attached access_response_error for an audit_expired error code', async () => { + await renderDenied('audit_expired'); + expect(global.location.href).toEqual('http://localhost/redirect/dashboard?access_response_error=uhoh%20oh%20no'); + }); + + it('redirects to the dashboard with a notlive start date for a course_not_started error code', async () => { + await renderDenied('course_not_started'); + expect(global.location.href).toEqual('http://localhost/redirect/dashboard?notlive=2/5/2013'); // date from factory + }); + }); }); diff --git a/src/course-home/outline-tab/OutlineTab.test.jsx b/src/course-home/outline-tab/OutlineTab.test.jsx index 5ea7a373..040f3ce0 100644 --- a/src/course-home/outline-tab/OutlineTab.test.jsx +++ b/src/course-home/outline-tab/OutlineTab.test.jsx @@ -29,7 +29,7 @@ describe('Outline Tab', () => { const enrollmentUrl = `${getConfig().LMS_BASE_URL}/api/enrollment/v1/enrollment`; const goalUrl = `${getConfig().LMS_BASE_URL}/api/course_home/v1/save_course_goal`; const outlineUrl = `${getConfig().LMS_BASE_URL}/api/course_home/v1/outline/${courseId}`; - const proctoringInfoUrl = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/user_onboarding/status?course_id=${encodeURIComponent(courseId)}`; + const proctoringInfoUrl = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/user_onboarding/status?is_learning_mfe=true&course_id=${encodeURIComponent(courseId)}`; const store = initializeStore(); const defaultMetadata = Factory.build('courseHomeMetadata', { id: courseId }); @@ -485,8 +485,8 @@ describe('Outline Tab', () => { }); await fetchAndRender(); - const alert = await screen.findByText('Welcome to Demonstration Course'); - expect(alert.parentElement).toHaveAttribute('role', 'alert'); + const alert = await screen.findByTestId('private-course-alert'); + expect(alert).toHaveAttribute('role', 'alert'); expect(screen.queryByRole('button', { name: 'Enroll now' })).not.toBeInTheDocument(); expect(screen.getByText('You must be enrolled in the course to see course content.')).toBeInTheDocument(); @@ -495,8 +495,8 @@ describe('Outline Tab', () => { it('displays alert for unenrolled user', async () => { await fetchAndRender(); - const alert = await screen.findByText('Welcome to Demonstration Course'); - expect(alert.parentElement).toHaveAttribute('role', 'alert'); + const alert = await screen.findByTestId('private-course-alert'); + expect(alert).toHaveAttribute('role', 'alert'); expect(screen.getByRole('button', { name: 'Enroll now' })).toBeInTheDocument(); }); @@ -693,6 +693,138 @@ describe('Outline Tab', () => { await fetchAndRender(); expect(screen.queryByText('Verify your identity to earn a certificate!')).toBeInTheDocument(); }); + it('tracks request cert button', async () => { + sendTrackEvent.mockClear(); + const now = new Date(); + const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1); + const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1); + setMetadata({ is_enrolled: true }); + setTabData({ + cert_data: { + cert_status: CERT_STATUS_TYPE.REQUESTING, + cert_web_view_url: null, + download_url: null, + }, + }, { + date_blocks: [ + { + date_type: 'course-end-date', + date: yesterday.toISOString(), + title: 'End', + }, + { + date_type: 'certificate-available-date', + date: tomorrow.toISOString(), + title: 'Cert Available', + }, + { + date_type: 'verification-deadline-date', + date: tomorrow.toISOString(), + link_text: 'Verify', + title: 'Verification Upgrade Deadline', + }, + ], + }); + await fetchAndRender(); + sendTrackEvent.mockClear(); + const requestingButton = screen.getByRole('button', { name: 'Request certificate' }); + fireEvent.click(requestingButton); + expect(sendTrackEvent).toHaveBeenCalledTimes(1); + expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_outline.certificate_alert_request_cert_button.clicked', + { + courserun_key: 'course-v1:edX+Test+run', + is_staff: false, + org_key: 'edX', + }); + }); + it('tracks download cert button', async () => { + sendTrackEvent.mockClear(); + const now = new Date(); + const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1); + const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1); + setMetadata({ is_enrolled: true }); + setTabData({ + cert_data: { + cert_status: CERT_STATUS_TYPE.DOWNLOADABLE, + cert_web_view_url: null, + download_url: null, + }, + }, { + date_blocks: [ + { + date_type: 'course-end-date', + date: yesterday.toISOString(), + title: 'End', + }, + { + date_type: 'certificate-available-date', + date: tomorrow.toISOString(), + title: 'Cert Available', + }, + { + date_type: 'verification-deadline-date', + date: tomorrow.toISOString(), + link_text: 'Verify', + title: 'Verification Upgrade Deadline', + }, + ], + }); + await fetchAndRender(); + sendTrackEvent.mockClear(); + const requestingButton = screen.getByRole('button', { name: 'View my certificate' }); + fireEvent.click(requestingButton); + expect(sendTrackEvent).toHaveBeenCalledTimes(1); + expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_outline.certificate_alert_downloadable_button.clicked', + { + courserun_key: 'course-v1:edX+Test+run', + is_staff: false, + org_key: 'edX', + }); + }); + it('tracks unverified cert button', async () => { + sendTrackEvent.mockClear(); + const now = new Date(); + const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1); + const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1); + setMetadata({ is_enrolled: true }); + setTabData({ + cert_data: { + cert_status: CERT_STATUS_TYPE.UNVERIFIED, + cert_web_view_url: null, + download_url: null, + }, + }, { + date_blocks: [ + { + date_type: 'course-end-date', + date: yesterday.toISOString(), + title: 'End', + }, + { + date_type: 'certificate-available-date', + date: tomorrow.toISOString(), + title: 'Cert Available', + }, + { + date_type: 'verification-deadline-date', + date: tomorrow.toISOString(), + link_text: 'Verify', + title: 'Verification Upgrade Deadline', + }, + ], + }); + await fetchAndRender(); + sendTrackEvent.mockClear(); + const requestingButton = screen.getByRole('link', { name: 'Verify my ID' }); + fireEvent.click(requestingButton); + expect(sendTrackEvent).toHaveBeenCalledTimes(1); + expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_outline.certificate_alert_unverified_button.clicked', + { + courserun_key: 'course-v1:edX+Test+run', + is_staff: false, + org_key: 'edX', + }); + }); }); describe('Scheduled Content Alert', () => { diff --git a/src/course-home/outline-tab/Section.jsx b/src/course-home/outline-tab/Section.jsx index ce69f760..b5078126 100644 --- a/src/course-home/outline-tab/Section.jsx +++ b/src/course-home/outline-tab/Section.jsx @@ -6,7 +6,6 @@ import { faCheckCircle as fasCheckCircle, faMinus, faPlus } from '@fortawesome/f import { faCheckCircle as farCheckCircle } from '@fortawesome/free-regular-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import EffortEstimate from '../../shared/effort-estimate'; import SequenceLink from './SequenceLink'; import { useModel } from '../../generic/model-store'; @@ -67,7 +66,6 @@ function Section({ , {intl.formatMessage(complete ? messages.completedSection : messages.incompleteSection)} -
); diff --git a/src/course-home/outline-tab/alerts/certificate-status-alert/CertificateStatusAlert.jsx b/src/course-home/outline-tab/alerts/certificate-status-alert/CertificateStatusAlert.jsx index 7e9c069b..c92497b1 100644 --- a/src/course-home/outline-tab/alerts/certificate-status-alert/CertificateStatusAlert.jsx +++ b/src/course-home/outline-tab/alerts/certificate-status-alert/CertificateStatusAlert.jsx @@ -12,6 +12,8 @@ import { useDispatch } from 'react-redux'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCheckCircle, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; import { getConfig } from '@edx/frontend-platform'; +import { sendTrackEvent } from '@edx/frontend-platform/analytics'; +import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import certMessages from './messages'; import certStatusMessages from '../../../progress-tab/certificate-status/messages'; import { requestCert } from '../../../data/thunks'; @@ -33,11 +35,21 @@ function CertificateStatusAlert({ intl, payload }) { certURL, isWebCert, userTimezone, + org, } = payload; // eslint-disable-next-line react/prop-types const AlertWrapper = (props) => props.children(props); + const sendAlertClickTracking = (id) => { + const { administrator } = getAuthenticatedUser(); + sendTrackEvent(id, { + org_key: org, + courserun_key: courseId, + is_staff: administrator, + }); + }; + const renderCertAwardedStatus = () => { const alertProps = { variant: 'success', @@ -53,7 +65,7 @@ function CertificateStatusAlert({ intl, payload }) {

{ + sendAlertClickTracking('edx.ui.lms.course_outline.certificate_alert_downloadable_button.clicked'); + }; } else if (certStatus === CERT_STATUS_TYPE.REQUESTING) { alertProps.header = intl.formatMessage(certMessages.certStatusDownloadableHeader); alertProps.buttonMessage = intl.formatMessage(certStatusMessages.requestableButton); alertProps.buttonVisible = true; alertProps.buttonLink = ''; - alertProps.buttonAction = () => { dispatch(requestCert(courseId)); }; + alertProps.buttonAction = () => { + sendAlertClickTracking('edx.ui.lms.course_outline.certificate_alert_request_cert_button.clicked'); + dispatch(requestCert(courseId)); + }; } return alertProps; }; @@ -92,6 +110,9 @@ function CertificateStatusAlert({ intl, payload }) { body: intl.formatMessage(certStatusMessages.unverifiedHomeBody), buttonVisible: true, buttonLink: getConfig().SUPPORT_URL_ID_VERIFICATION, + buttonAction: () => { + sendAlertClickTracking('edx.ui.lms.course_outline.certificate_alert_unverified_button.clicked'); + }, }; return alertProps; @@ -162,6 +183,7 @@ CertificateStatusAlert.propTypes = { certURL: PropTypes.string, isWebCert: PropTypes.bool, userTimezone: PropTypes.string, + org: PropTypes.string, }).isRequired, }; diff --git a/src/course-home/outline-tab/alerts/certificate-status-alert/hooks.js b/src/course-home/outline-tab/alerts/certificate-status-alert/hooks.js index 35ad4ad3..8a8e62d7 100644 --- a/src/course-home/outline-tab/alerts/certificate-status-alert/hooks.js +++ b/src/course-home/outline-tab/alerts/certificate-status-alert/hooks.js @@ -23,6 +23,7 @@ function verifyCertStatusType(status) { function useCertificateStatusAlert(courseId) { const { isEnrolled, + org, } = useModel('courseHomeMeta', courseId); const { @@ -61,6 +62,7 @@ function useCertificateStatusAlert(courseId) { courseEndDate: endBlock && endBlock.date, userTimezone, isWebCert, + org, }; useAlert(isVisible, { diff --git a/src/course-home/outline-tab/alerts/course-end-alert/CourseEndAlert.jsx b/src/course-home/outline-tab/alerts/course-end-alert/CourseEndAlert.jsx index ee2fd80a..3e5d06c5 100644 --- a/src/course-home/outline-tab/alerts/course-end-alert/CourseEndAlert.jsx +++ b/src/course-home/outline-tab/alerts/course-end-alert/CourseEndAlert.jsx @@ -6,8 +6,8 @@ import { FormattedRelative, FormattedTime, } from '@edx/frontend-platform/i18n'; - -import { Alert, ALERT_TYPES } from '../../../../generic/user-messages'; +import { Alert } from '@edx/paragon'; +import { Info } from '@edx/paragon/icons'; const DAY_MS = 24 * 60 * 60 * 1000; // in ms @@ -78,7 +78,7 @@ function CourseEndAlert({ payload }) { } return ( - + {msg}
{description}
diff --git a/src/course-home/outline-tab/alerts/course-start-alert/CourseStartAlert.jsx b/src/course-home/outline-tab/alerts/course-start-alert/CourseStartAlert.jsx index 51db2091..5304d5fb 100644 --- a/src/course-home/outline-tab/alerts/course-start-alert/CourseStartAlert.jsx +++ b/src/course-home/outline-tab/alerts/course-start-alert/CourseStartAlert.jsx @@ -6,8 +6,8 @@ import { FormattedRelative, FormattedTime, } from '@edx/frontend-platform/i18n'; - -import { Alert, ALERT_TYPES } from '../../../../generic/user-messages'; +import { Alert } from '@edx/paragon'; +import { Info } from '@edx/paragon/icons'; const DAY_MS = 24 * 60 * 60 * 1000; // in ms @@ -30,7 +30,7 @@ function CourseStartAlert({ payload }) { const delta = new Date(startDate) - new Date(); if (delta < DAY_MS) { return ( - + + {intl.formatMessage(enrollmentMessages.enrollNowInline)} @@ -63,7 +64,7 @@ function PrivateCourseAlert({ intl, payload }) { ); return ( - + {anonymousUser && ( <>

@@ -84,15 +85,11 @@ function PrivateCourseAlert({ intl, payload }) { <>

{intl.formatMessage(outlineMessages.welcomeTo)} {title}

{canEnroll && ( - <> - +
+ {enrollNowButton} + {intl.formatMessage(messages.toAccess)} {loading && } - +
)} {!canEnroll && ( <> diff --git a/src/course-home/outline-tab/alerts/private-course-alert/messages.js b/src/course-home/outline-tab/alerts/private-course-alert/messages.js index 903f92a1..67388a6a 100644 --- a/src/course-home/outline-tab/alerts/private-course-alert/messages.js +++ b/src/course-home/outline-tab/alerts/private-course-alert/messages.js @@ -1,10 +1,11 @@ import { defineMessages } from '@edx/frontend-platform/i18n'; const messages = defineMessages({ - enroll: { + toAccess: { id: 'alert.enroll', - defaultMessage: 'You must be enrolled in the course to see course content.', - description: 'Text instructing the learner to enroll in the course in order to see course content.', + defaultMessage: ' to access the full course.', + description: 'Text instructing the learner to enroll in the course in order to see course content. The full string' + + 'would say "Enroll now to access the full course", where "Enroll now" is a button.', }, }); diff --git a/src/course-home/outline-tab/widgets/ProctoringInfoPanel.jsx b/src/course-home/outline-tab/widgets/ProctoringInfoPanel.jsx index 458f2a38..acde48d6 100644 --- a/src/course-home/outline-tab/widgets/ProctoringInfoPanel.jsx +++ b/src/course-home/outline-tab/widgets/ProctoringInfoPanel.jsx @@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react'; import camelCase from 'lodash.camelcase'; import PropTypes from 'prop-types'; -import { getConfig } from '@edx/frontend-platform'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Button } from '@edx/paragon'; @@ -131,7 +130,7 @@ function ProctoringInfoPanel({ courseId, username, intl }) { {isNotYetSubmitted(status) && ( <> {!isNotYetReleased(releaseDate) && ( - -
-
+ { + setDisplay(false); + dispatch(dismissWelcomeMessage(courseId)); + }} + actions={messageCanBeShortened ? [ + , + ] : []} + > + + {showShortMessage ? ( + + ) : ( + )} - > - - {showShortMessage ? ( - - ) : ( - - )} - - - ) + + ); } diff --git a/src/course-home/progress-tab/ProgressTab.jsx b/src/course-home/progress-tab/ProgressTab.jsx index f4f1a4a9..d9a22c4d 100644 --- a/src/course-home/progress-tab/ProgressTab.jsx +++ b/src/course-home/progress-tab/ProgressTab.jsx @@ -1,6 +1,6 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { layoutGenerator } from 'react-break'; -import { useDispatch, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; import CertificateStatus from './certificate-status/CertificateStatus'; import CourseCompletion from './course-completion/CourseCompletion'; @@ -10,7 +10,6 @@ import GradeSummary from './grades/grade-summary/GradeSummary'; import ProgressHeader from './ProgressHeader'; import RelatedLinks from './related-links/RelatedLinks'; -import { setGradesFeatureStatus } from '../data/slice'; import { useModel } from '../../generic/model-store'; function ProgressTab() { @@ -19,18 +18,10 @@ function ProgressTab() { } = useSelector(state => state.courseHome); const { - completionSummary: { - lockedCount, - }, + gradesFeatureIsFullyLocked, } = useModel('progress', courseId); - const gradesFeatureIsLocked = lockedCount > 0; - const applyLockedOverlay = gradesFeatureIsLocked ? 'locked-overlay' : ''; - - const dispatch = useDispatch(); - useEffect(() => { - dispatch(setGradesFeatureStatus({ gradesFeatureIsLocked })); - }, []); + const applyLockedOverlay = gradesFeatureIsFullyLocked ? 'locked-overlay' : ''; const layout = layoutGenerator({ mobile: 0, @@ -50,7 +41,7 @@ function ProgressTab() { -
+
diff --git a/src/course-home/progress-tab/ProgressTab.test.jsx b/src/course-home/progress-tab/ProgressTab.test.jsx index 1b7349a1..2558b003 100644 --- a/src/course-home/progress-tab/ProgressTab.test.jsx +++ b/src/course-home/progress-tab/ProgressTab.test.jsx @@ -111,6 +111,7 @@ describe('Progress Tab', () => { assignment_type: 'Homework', block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345', display_name: 'First subsection', + learner_has_access: true, has_graded_assignment: true, num_points_earned: 1, num_points_possible: 2, @@ -176,6 +177,7 @@ describe('Progress Tab', () => { assignment_type: 'Homework', block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345', display_name: 'First subsection', + learner_has_access: true, has_graded_assignment: true, num_points_earned: 8, num_points_possible: 10, @@ -252,6 +254,26 @@ describe('Progress Tab', () => { sku: 'ABCD1234', upgrade_url: 'edx.org/upgrade', }, + section_scores: [ + { + display_name: 'First section', + subsections: [ + { + assignment_type: 'Homework', + block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345', + display_name: 'First subsection', + learner_has_access: false, + has_graded_assignment: true, + num_points_earned: 8, + num_points_possible: 10, + percent_graded: 1.0, + show_correctness: 'always', + show_grades: true, + url: 'http://learning.edx.org/course/course-v1:edX+Test+run/first_subsection', + }, + ], + }, + ], }); await fetchAndRender(); expect(screen.getByText('locked feature')).toBeInTheDocument(); @@ -259,7 +281,7 @@ describe('Progress Tab', () => { expect(screen.getAllByRole('link', 'Unlock now')).toHaveLength(3); }); - it('sends event on click of upgrade button in locked content header (CourseGradeHeader)', async () => { + it('sends events on click of upgrade button in locked content header (CourseGradeHeader)', async () => { sendTrackEvent.mockClear(); setTabData({ completion_summary: { @@ -275,6 +297,26 @@ describe('Progress Tab', () => { sku: 'ABCD1234', upgrade_url: 'edx.org/upgrade', }, + section_scores: [ + { + display_name: 'First section', + subsections: [ + { + assignment_type: 'Homework', + block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345', + display_name: 'First subsection', + learner_has_access: false, + has_graded_assignment: true, + num_points_earned: 8, + num_points_possible: 10, + percent_graded: 1.0, + show_correctness: 'always', + show_grades: true, + url: 'http://learning.edx.org/course/course-v1:edX+Test+run/first_subsection', + }, + ], + }, + ], }); await fetchAndRender(); expect(screen.getByText('locked feature')).toBeInTheDocument(); @@ -283,12 +325,20 @@ describe('Progress Tab', () => { const upgradeButton = screen.getAllByRole('link', 'Unlock now')[0]; fireEvent.click(upgradeButton); - expect(sendTrackEvent).toHaveBeenCalledTimes(1); + expect(sendTrackEvent).toHaveBeenCalledTimes(2); expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_progress.grades_upgrade.clicked', { org_key: 'edX', courserun_key: courseId, is_staff: false, }); + expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.ecommerce.upsell_links_clicked', { + org_key: 'edX', + courserun_key: courseId, + linkCategory: '(none)', + linkName: 'progress_locked', + linkType: 'button', + pageName: 'progress', + }); }); it('renders locked feature preview with no upgrade button when user has locked content but cannot upgrade', async () => { @@ -298,6 +348,26 @@ describe('Progress Tab', () => { incomplete_count: 1, locked_count: 1, }, + section_scores: [ + { + display_name: 'First section', + subsections: [ + { + assignment_type: 'Homework', + block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345', + display_name: 'First subsection', + learner_has_access: false, + has_graded_assignment: true, + num_points_earned: 1, + num_points_possible: 2, + percent_graded: 1.0, + show_correctness: 'always', + show_grades: true, + url: 'http://learning.edx.org/course/course-v1:edX+Test+run/first_subsection', + }, + ], + }, + ], }); await fetchAndRender(); expect(screen.getByText('locked feature')).toBeInTheDocument(); @@ -309,6 +379,62 @@ describe('Progress Tab', () => { expect(screen.queryByText('locked feature')).not.toBeInTheDocument(); }); + it('renders limited feature preview with upgrade button when user has access to some content that would typically be locked', async () => { + setTabData({ + completion_summary: { + complete_count: 1, + incomplete_count: 1, + locked_count: 1, + }, + verified_mode: { + access_expiration_date: '2050-01-01T12:00:00', + currency: 'USD', + currency_symbol: '$', + price: 149, + sku: 'ABCD1234', + upgrade_url: 'edx.org/upgrade', + }, + section_scores: [ + { + display_name: 'First section', + subsections: [ + { + assignment_type: 'Homework', + block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@123456', + display_name: 'First subsection', + learner_has_access: false, + has_graded_assignment: true, + num_points_earned: 8, + num_points_possible: 10, + percent_graded: 1.0, + show_correctness: 'always', + show_grades: true, + url: 'http://learning.edx.org/course/course-v1:edX+Test+run/first_subsection', + }, + { + assignment_type: 'Exam', + display_name: 'Second subsection', + learner_has_access: true, + has_graded_assignment: true, + num_points_earned: 1, + num_points_possible: 1, + percent_graded: 1.0, + show_correctness: 'always', + show_grades: true, + url: 'http://learning.edx.org/course/course-v1:edX+Test+run/second_subsection', + }, + ], + }, + ], + }); + await fetchAndRender(); + expect(screen.getByText('limited feature')).toBeInTheDocument(); + expect(screen.getByText('Unlock to work towards a certificate.')).toBeInTheDocument(); + expect(screen.queryAllByText('You have limited access to graded assignments as part of the audit track in this course.')).toHaveLength(2); + + expect(screen.queryAllByTestId('blocked-icon')).toHaveLength(4); + }); + it('renders correct current grade tooltip when showGrades is false', async () => { // The learner has a 50% on the first assignment and a 100% on the second, making their grade a 75% // The second assignment has showGrades set to false, so the grade reflected to the learner should be 50%. @@ -321,6 +447,7 @@ describe('Progress Tab', () => { assignment_type: 'Homework', block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345', display_name: 'First subsection', + learner_has_access: true, has_graded_assignment: true, num_points_earned: 1, num_points_possible: 2, @@ -337,6 +464,7 @@ describe('Progress Tab', () => { { assignment_type: 'Homework', display_name: 'Second subsection', + learner_has_access: true, has_graded_assignment: true, num_points_earned: 1, num_points_possible: 1, @@ -531,6 +659,7 @@ describe('Progress Tab', () => { { assignment_type: 'Homework', display_name: 'Second subsection', + learner_has_access: true, has_graded_assignment: true, num_points_earned: 1, num_points_possible: 1, @@ -554,8 +683,8 @@ describe('Progress Tab', () => { await fetchAndRender(); expect(screen.getByText('Detailed grades')).toBeInTheDocument(); - expect(screen.getByRole('link', { name: 'First subsection' })); - expect(screen.getByRole('link', { name: 'Second subsection' })); + expect(screen.getByText('First subsection')); + expect(screen.getByText('Second subsection')); }); it('sends event on click of subsection link', async () => { @@ -591,6 +720,20 @@ describe('Progress Tab', () => { }); }); + it('renders individual problem score drawer', async () => { + sendTrackEvent.mockClear(); + await fetchAndRender(); + expect(screen.getByText('Detailed grades')).toBeInTheDocument(); + + const problemScoreDrawerToggle = screen.getByRole('button', { name: 'Toggle individual problem scores for First subsection' }); + expect(problemScoreDrawerToggle).toBeInTheDocument(); + + // Open the problem score drawer + fireEvent.click(problemScoreDrawerToggle); + expect(screen.getByText('Problem Scores:')).toBeInTheDocument(); + expect(screen.getAllByText('0/1')).toHaveLength(3); + }); + it('render message when section scores are not populated', async () => { setTabData({ section_scores: [], @@ -958,13 +1101,21 @@ describe('Progress Tab', () => { const upgradeLink = screen.getByRole('link', { name: 'Upgrade now' }); fireEvent.click(upgradeLink); - expect(sendTrackEvent).toHaveBeenCalledTimes(2); + expect(sendTrackEvent).toHaveBeenCalledTimes(3); expect(sendTrackEvent).toHaveBeenNthCalledWith(2, 'edx.ui.lms.course_progress.certificate_status.clicked', { org_key: 'edX', courserun_key: courseId, is_staff: false, certificate_status_variant: 'audit_passing', }); + expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.ecommerce.upsell_links_clicked', { + org_key: 'edX', + courserun_key: courseId, + linkCategory: '(none)', + linkName: 'progress_certificate', + linkType: 'button', + pageName: 'progress', + }); }); it('Displays nothing if audit only', async () => { diff --git a/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx b/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx index 32ce1991..ea79584d 100644 --- a/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx +++ b/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx @@ -36,6 +36,9 @@ function CertificateStatus({ intl }) { verificationData, verifiedMode, } = useModel('progress', courseId); + const { + certificateAvailableDate, + } = certificateData || {}; const mode = getCourseExitMode( certificateData, @@ -43,6 +46,12 @@ function CertificateStatus({ intl }) { isEnrolled, userHasPassingGrade, ); + + const eventProperties = { + org_key: org, + courserun_key: courseId, + }; + const dispatch = useDispatch(); const { administrator } = getAuthenticatedUser(); @@ -63,6 +72,7 @@ function CertificateStatus({ intl }) { let buttonLocation; let buttonText; let endDate; + let certAvailabilityDate; let gradeEventName = 'not_passing'; if (userHasPassingGrade) { @@ -137,12 +147,13 @@ function CertificateStatus({ intl }) { case 'earned_but_not_available': certCase = 'notAvailable'; endDate = ; + certAvailabilityDate = ; body = ( ); break; @@ -193,6 +204,15 @@ function CertificateStatus({ intl }) { is_staff: administrator, certificate_status_variant: certEventName, }); + if (certCase === 'upgrade') { + sendTrackEvent('edx.bi.ecommerce.upsell_links_clicked', { + ...eventProperties, + linkCategory: '(none)', + linkName: 'progress_certificate', + linkType: 'button', + pageName: 'progress', + }); + } }; return ( diff --git a/src/course-home/progress-tab/certificate-status/messages.js b/src/course-home/progress-tab/certificate-status/messages.js index 13dde252..890bbbbb 100644 --- a/src/course-home/progress-tab/certificate-status/messages.js +++ b/src/course-home/progress-tab/certificate-status/messages.js @@ -61,10 +61,6 @@ const messages = defineMessages({ id: 'progress.certificateStatus.notAvailableHeader', defaultMessage: 'Certificate status', }, - notAvailableBody: { - id: 'progress.certificateStatus.notAvailableBody', - defaultMessage: 'Your certificate will be available soon! After this course officially ends on {end_date}, you will receive an email notification with your certificate.', - }, upgradeHeader: { id: 'progress.certificateStatus.upgradeHeader', defaultMessage: 'Earn a certificate', diff --git a/src/course-home/progress-tab/grades/course-grade/CourseGrade.jsx b/src/course-home/progress-tab/grades/course-grade/CourseGrade.jsx index 65b5a817..c350296a 100644 --- a/src/course-home/progress-tab/grades/course-grade/CourseGrade.jsx +++ b/src/course-home/progress-tab/grades/course-grade/CourseGrade.jsx @@ -13,10 +13,11 @@ import messages from '../messages'; function CourseGrade({ intl }) { const { courseId, - gradesFeatureIsLocked, } = useSelector(state => state.courseHome); const { + gradesFeatureIsFullyLocked, + gradesFeatureIsPartiallyLocked, gradingPolicy: { gradeRange, }, @@ -24,12 +25,12 @@ function CourseGrade({ intl }) { const passingGrade = Number((Math.min(...Object.values(gradeRange)) * 100).toFixed(0)); - const applyLockedOverlay = gradesFeatureIsLocked ? 'locked-overlay' : ''; + const applyLockedOverlay = gradesFeatureIsFullyLocked ? 'locked-overlay' : ''; return (
- {gradesFeatureIsLocked && } -
+ {(gradesFeatureIsFullyLocked || gradesFeatureIsPartiallyLocked) && } +

{intl.formatMessage(messages.grades)}

diff --git a/src/course-home/progress-tab/grades/course-grade/CourseGradeHeader.jsx b/src/course-home/progress-tab/grades/course-grade/CourseGradeHeader.jsx index 45128488..4a485b11 100644 --- a/src/course-home/progress-tab/grades/course-grade/CourseGradeHeader.jsx +++ b/src/course-home/progress-tab/grades/course-grade/CourseGradeHeader.jsx @@ -19,8 +19,14 @@ function CourseGradeHeader({ intl }) { } = useModel('courseHomeMeta', courseId); const { verifiedMode, + gradesFeatureIsFullyLocked, } = useModel('progress', courseId); + const eventProperties = { + org_key: org, + courserun_key: courseId, + }; + const { administrator } = getAuthenticatedUser(); const logUpgradeButtonClick = () => { sendTrackEvent('edx.ui.lms.course_progress.grades_upgrade.clicked', { @@ -28,7 +34,22 @@ function CourseGradeHeader({ intl }) { courserun_key: courseId, is_staff: administrator, }); + sendTrackEvent('edx.bi.ecommerce.upsell_links_clicked', { + ...eventProperties, + linkCategory: '(none)', + linkName: 'progress_locked', + linkType: 'button', + pageName: 'progress', + }); }; + let previewText; + if (verifiedMode) { + previewText = gradesFeatureIsFullyLocked + ? intl.formatMessage(messages.courseGradePreviewUnlockCertificateBody) + : intl.formatMessage(messages.courseGradePartialPreviewUnlockCertificateBody); + } else { + previewText = intl.formatMessage(messages.courseGradePreviewUpgradeDeadlinePassedBody); + } return (
@@ -40,13 +61,14 @@ function CourseGradeHeader({ intl }) { - {intl.formatMessage(messages.courseGradePreviewHeader)} + {gradesFeatureIsFullyLocked + ? intl.formatMessage(messages.courseGradePreviewHeaderLocked) + : intl.formatMessage(messages.courseGradePreviewHeaderLimited)}
- {verifiedMode ? intl.formatMessage(messages.courseGradePreviewUnlockCertificateBody) - : intl.formatMessage(messages.courseGradePreviewUpgradeDeadlinePassedBody)} + {previewText}
diff --git a/src/course-home/progress-tab/grades/course-grade/GradeBar.jsx b/src/course-home/progress-tab/grades/course-grade/GradeBar.jsx index 424c13d3..01e5c107 100644 --- a/src/course-home/progress-tab/grades/course-grade/GradeBar.jsx +++ b/src/course-home/progress-tab/grades/course-grade/GradeBar.jsx @@ -12,7 +12,6 @@ import messages from '../messages'; function GradeBar({ intl, passingGrade }) { const { courseId, - gradesFeatureIsLocked, } = useSelector(state => state.courseHome); const { @@ -20,11 +19,12 @@ function GradeBar({ intl, passingGrade }) { isPassing, visiblePercent, }, + gradesFeatureIsFullyLocked, } = useModel('progress', courseId); const currentGrade = Number((visiblePercent * 100).toFixed(0)); - const lockedTooltipClassName = gradesFeatureIsLocked ? 'locked-overlay' : ''; + const lockedTooltipClassName = gradesFeatureIsFullyLocked ? 'locked-overlay' : ''; return (
diff --git a/src/course-home/progress-tab/grades/course-grade/GradeRangeTooltip.jsx b/src/course-home/progress-tab/grades/course-grade/GradeRangeTooltip.jsx index 856fe07b..e983d962 100644 --- a/src/course-home/progress-tab/grades/course-grade/GradeRangeTooltip.jsx +++ b/src/course-home/progress-tab/grades/course-grade/GradeRangeTooltip.jsx @@ -14,10 +14,10 @@ import messages from '../messages'; function GradeRangeTooltip({ intl, iconButtonClassName, passingGrade }) { const { courseId, - gradesFeatureIsLocked, } = useSelector(state => state.courseHome); const { + gradesFeatureIsFullyLocked, gradingPolicy: { gradeRange, }, @@ -68,7 +68,7 @@ function GradeRangeTooltip({ intl, iconButtonClassName, passingGrade }) { src={InfoOutline} iconAs={Icon} size="inline" - disabled={gradesFeatureIsLocked} + disabled={gradesFeatureIsFullyLocked} /> ); diff --git a/src/course-home/progress-tab/grades/detailed-grades/DetailedGrades.jsx b/src/course-home/progress-tab/grades/detailed-grades/DetailedGrades.jsx index 9b579ac1..51c24ce7 100644 --- a/src/course-home/progress-tab/grades/detailed-grades/DetailedGrades.jsx +++ b/src/course-home/progress-tab/grades/detailed-grades/DetailedGrades.jsx @@ -5,7 +5,8 @@ import { getConfig } from '@edx/frontend-platform'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { Hyperlink } from '@edx/paragon'; +import { Blocked } from '@edx/paragon/icons'; +import { Icon, Hyperlink } from '@edx/paragon'; import { useModel } from '../../../../generic/model-store'; import DetailedGradesTable from './DetailedGradesTable'; @@ -16,12 +17,13 @@ function DetailedGrades({ intl }) { const { administrator } = getAuthenticatedUser(); const { courseId, - gradesFeatureIsLocked, } = useSelector(state => state.courseHome); const { org, } = useModel('courseHomeMeta', courseId); const { + gradesFeatureIsFullyLocked, + gradesFeatureIsPartiallyLocked, sectionScores, } = useModel('progress', courseId); @@ -40,7 +42,7 @@ function DetailedGrades({ intl }) { className="muted-link inline-link" destination={`${getConfig().LMS_BASE_URL}/courses/${courseId}/course`} onClick={logOutlineLinkClick} - tabIndex={gradesFeatureIsLocked ? '-1' : '0'} + tabIndex={gradesFeatureIsFullyLocked ? '-1' : '0'} > {intl.formatMessage(messages.courseOutline)} @@ -49,8 +51,14 @@ function DetailedGrades({ intl }) { return (

{intl.formatMessage(messages.detailedGrades)}

+ {gradesFeatureIsPartiallyLocked && ( +
+ + {intl.formatMessage(messages.gradeSummaryLimitedAccessExplanation)} +
+ )} {hasSectionScores && ( - + )} {!hasSectionScores && (

{intl.formatMessage(messages.detailedGradesEmpty)}

diff --git a/src/course-home/progress-tab/grades/detailed-grades/DetailedGradesTable.jsx b/src/course-home/progress-tab/grades/detailed-grades/DetailedGradesTable.jsx index fe8b673e..0f5e561f 100644 --- a/src/course-home/progress-tab/grades/detailed-grades/DetailedGradesTable.jsx +++ b/src/course-home/progress-tab/grades/detailed-grades/DetailedGradesTable.jsx @@ -1,32 +1,22 @@ import React from 'react'; import { useSelector } from 'react-redux'; -import PropTypes from 'prop-types'; -import { sendTrackEvent } from '@edx/frontend-platform/analytics'; -import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { DataTable } from '@edx/paragon'; -import messages from '../messages'; import { useModel } from '../../../../generic/model-store'; +import messages from '../messages'; +import SubsectionTitleCell from './SubsectionTitleCell'; -function DetailedGradesTable({ intl, sectionScores }) { +function DetailedGradesTable({ intl }) { const { courseId, - gradesFeatureIsLocked, } = useSelector(state => state.courseHome); + const { - org, - } = useModel('courseHomeMeta', courseId); - const { administrator } = getAuthenticatedUser(); - const logSubsectionClicked = (blockKey) => { - sendTrackEvent('edx.ui.lms.course_progress.detailed_grades_assignment.clicked', { - org_key: org, - courserun_key: courseId, - is_staff: administrator, - assignment_block_key: blockKey, - }); - }; + sectionScores, + } = useModel('progress', courseId); + return ( sectionScores.map((chapter) => { const subsectionScores = chapter.subsections.filter( @@ -40,24 +30,10 @@ function DetailedGradesTable({ intl, sectionScores }) { return null; } - const detailedGradesData = subsectionScores.map((subsection) => { - const title = ( - { - logSubsectionClicked(subsection.blockKey); - }} - tabIndex={gradesFeatureIsLocked ? '-1' : '0'} - > - {subsection.displayName} - - ); - return { - subsectionTitle: title, - score: `${subsection.numPointsEarned}/${subsection.numPointsPossible}`, - }; - }); + const detailedGradesData = subsectionScores.map((subsection) => ({ + subsectionTitle: , + score: {subsection.numPointsEarned}/{subsection.numPointsPossible}, + })); return (
@@ -89,21 +65,6 @@ function DetailedGradesTable({ intl, sectionScores }) { DetailedGradesTable.propTypes = { intl: intlShape.isRequired, - sectionScores: PropTypes.arrayOf(PropTypes.shape({ - displayName: PropTypes.string.isRequired, - subsections: PropTypes.arrayOf(PropTypes.shape({ - displayName: PropTypes.string.isRequired, - numPointsEarned: PropTypes.number.isRequired, - numPointsPossible: PropTypes.number.isRequired, - url: PropTypes.string.isRequired, - })), - })).isRequired, -}; - -DetailedGradesTable.defaultProps = { - sectionScores: { - subsections: [], - }, }; export default injectIntl(DetailedGradesTable); diff --git a/src/course-home/progress-tab/grades/detailed-grades/ProblemScoreDrawer.jsx b/src/course-home/progress-tab/grades/detailed-grades/ProblemScoreDrawer.jsx new file mode 100644 index 00000000..2a90551e --- /dev/null +++ b/src/course-home/progress-tab/grades/detailed-grades/ProblemScoreDrawer.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; + +import messages from '../messages'; + +function ProblemScoreDrawer({ intl, problemScores, subsection }) { + return ( + + {intl.formatMessage(messages.problemScoreLabel)} +
+
    + {problemScores.map(problemScore => ( +
  • {problemScore.earned}/{problemScore.possible}
  • + ))} +
+
+
+ ); +} + +ProblemScoreDrawer.propTypes = { + intl: intlShape.isRequired, + problemScores: PropTypes.arrayOf(PropTypes.shape({ + earned: PropTypes.number.isRequired, + possible: PropTypes.number.isRequired, + })).isRequired, + subsection: PropTypes.shape({ learnerHasAccess: PropTypes.bool }).isRequired, +}; + +export default injectIntl(ProblemScoreDrawer); diff --git a/src/course-home/progress-tab/grades/detailed-grades/SubsectionTitleCell.jsx b/src/course-home/progress-tab/grades/detailed-grades/SubsectionTitleCell.jsx new file mode 100644 index 00000000..2ab27834 --- /dev/null +++ b/src/course-home/progress-tab/grades/detailed-grades/SubsectionTitleCell.jsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; + +import { sendTrackEvent } from '@edx/frontend-platform/analytics'; +import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { Collapsible, Icon, Row } from '@edx/paragon'; +import { ArrowDropDown, ArrowDropUp, Blocked } from '@edx/paragon/icons'; + +import messages from '../messages'; +import { useModel } from '../../../../generic/model-store'; +import ProblemScoreDrawer from './ProblemScoreDrawer'; + +function SubsectionTitleCell({ intl, subsection }) { + const { + courseId, + } = useSelector(state => state.courseHome); + const { + org, + } = useModel('courseHomeMeta', courseId); + const { + gradesFeatureIsFullyLocked, + } = useModel('progress', courseId); + + const { + blockKey, + displayName, + problemScores, + url, + } = subsection; + + const { administrator } = getAuthenticatedUser(); + const logSubsectionClicked = () => { + sendTrackEvent('edx.ui.lms.course_progress.detailed_grades_assignment.clicked', { + org_key: org, + courserun_key: courseId, + is_staff: administrator, + assignment_block_key: blockKey, + }); + }; + + return ( + + + + + + + + {gradesFeatureIsFullyLocked || subsection.learnerHasAccess ? '' : } + {url ? ( + + {displayName} + + ) : ( + {displayName} + )} + + + + + + + ); +} + +SubsectionTitleCell.propTypes = { + intl: intlShape.isRequired, + subsection: PropTypes.shape.isRequired, +}; + +export default injectIntl(SubsectionTitleCell); diff --git a/src/course-home/progress-tab/grades/grade-summary/AssignmentTypeCell.jsx b/src/course-home/progress-tab/grades/grade-summary/AssignmentTypeCell.jsx index 2ffaa3a5..015e39c8 100644 --- a/src/course-home/progress-tab/grades/grade-summary/AssignmentTypeCell.jsx +++ b/src/course-home/progress-tab/grades/grade-summary/AssignmentTypeCell.jsx @@ -1,14 +1,28 @@ import React from 'react'; import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { Blocked } from '@edx/paragon/icons'; +import { Icon } from '@edx/paragon'; +import { useModel } from '../../../../generic/model-store'; +import messages from '../messages'; -function AssignmentTypeCell({ assignmentType, footnoteMarker, footnoteId }) { +function AssignmentTypeCell({ + intl, assignmentType, footnoteMarker, footnoteId, locked, +}) { const { - gradesFeatureIsLocked, + courseId, } = useSelector(state => state.courseHome); + + const { + gradesFeatureIsFullyLocked, + } = useModel('progress', courseId); + + const lockedIcon = locked ? : ''; + return (
- {assignmentType} + {lockedIcon}{assignmentType} {footnoteId && footnoteMarker && ( {footnoteMarker} @@ -27,14 +42,17 @@ function AssignmentTypeCell({ assignmentType, footnoteMarker, footnoteId }) { } AssignmentTypeCell.propTypes = { + intl: intlShape.isRequired, assignmentType: PropTypes.string.isRequired, footnoteId: PropTypes.string, footnoteMarker: PropTypes.number, + locked: PropTypes.bool, }; AssignmentTypeCell.defaultProps = { footnoteId: '', footnoteMarker: null, + locked: false, }; -export default AssignmentTypeCell; +export default injectIntl(AssignmentTypeCell); diff --git a/src/course-home/progress-tab/grades/grade-summary/DroppableAssignmentFootnote.jsx b/src/course-home/progress-tab/grades/grade-summary/DroppableAssignmentFootnote.jsx index 4d114fc8..56d1a739 100644 --- a/src/course-home/progress-tab/grades/grade-summary/DroppableAssignmentFootnote.jsx +++ b/src/course-home/progress-tab/grades/grade-summary/DroppableAssignmentFootnote.jsx @@ -5,11 +5,15 @@ import PropTypes from 'prop-types'; import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import messages from '../messages'; +import { useModel } from '../../../../generic/model-store'; function DroppableAssignmentFootnote({ footnotes, intl }) { const { - gradesFeatureIsLocked, + courseId, } = useSelector(state => state.courseHome); + const { + gradesFeatureIsFullyLocked, + } = useModel('progress', courseId); return ( <> {intl.formatMessage(messages.footnotesTitle)} @@ -25,7 +29,7 @@ function DroppableAssignmentFootnote({ footnotes, intl }) { assignmentType: footnote.assignmentType, }} /> - + {intl.formatMessage(messages.backToContent)} diff --git a/src/course-home/progress-tab/grades/grade-summary/GradeSummary.jsx b/src/course-home/progress-tab/grades/grade-summary/GradeSummary.jsx index 4b9d96c2..b937652c 100644 --- a/src/course-home/progress-tab/grades/grade-summary/GradeSummary.jsx +++ b/src/course-home/progress-tab/grades/grade-summary/GradeSummary.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { useSelector } from 'react-redux'; import { useModel } from '../../../../generic/model-store'; @@ -16,14 +16,16 @@ function GradeSummary() { }, } = useModel('progress', courseId); + const [allOfSomeAssignmentTypeIsLocked, setAllOfSomeAssignmentTypeIsLocked] = useState(false); + if (assignmentPolicies.length === 0) { return null; } return (
- - + +
); } diff --git a/src/course-home/progress-tab/grades/grade-summary/GradeSummaryHeader.jsx b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryHeader.jsx index 8ee62924..230d2059 100644 --- a/src/course-home/progress-tab/grades/grade-summary/GradeSummaryHeader.jsx +++ b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryHeader.jsx @@ -1,19 +1,25 @@ import React, { useState } from 'react'; import { useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Icon, IconButton, OverlayTrigger, Popover, } from '@edx/paragon'; -import { InfoOutline } from '@edx/paragon/icons'; +import { Blocked, InfoOutline } from '@edx/paragon/icons'; import messages from '../messages'; +import { useModel } from '../../../../generic/model-store'; -function GradeSummaryHeader({ intl }) { +function GradeSummaryHeader({ intl, allOfSomeAssignmentTypeIsLocked }) { const { - gradesFeatureIsLocked, + courseId, } = useSelector(state => state.courseHome); + const { + gradesFeatureIsFullyLocked, + } = useModel('progress', courseId); const [showTooltip, setShowTooltip] = useState(false); + return (

{intl.formatMessage(messages.gradeSummary)}

@@ -37,15 +43,22 @@ function GradeSummaryHeader({ intl }) { iconAs={Icon} className="mb-3" size="sm" - disabled={gradesFeatureIsLocked} + disabled={gradesFeatureIsFullyLocked} /> + {!gradesFeatureIsFullyLocked && allOfSomeAssignmentTypeIsLocked && ( +
+ + {intl.formatMessage(messages.gradeSummaryLimitedAccessExplanation)} +
+ )}
); } GradeSummaryHeader.propTypes = { intl: intlShape.isRequired, + allOfSomeAssignmentTypeIsLocked: PropTypes.bool.isRequired, }; export default injectIntl(GradeSummaryHeader); diff --git a/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTable.jsx b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTable.jsx index 09a0edb9..591c162c 100644 --- a/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTable.jsx +++ b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTable.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; @@ -11,7 +12,7 @@ import GradeSummaryTableFooter from './GradeSummaryTableFooter'; import messages from '../messages'; -function GradeSummaryTable({ intl }) { +function GradeSummaryTable({ intl, setAllOfSomeAssignmentTypeIsLocked }) { const { courseId, } = useSelector(state => state.courseHome); @@ -20,6 +21,8 @@ function GradeSummaryTable({ intl }) { gradingPolicy: { assignmentPolicies, }, + gradesFeatureIsFullyLocked, + sectionScores, } = useModel('progress', courseId); const footnotes = []; @@ -29,6 +32,23 @@ function GradeSummaryTable({ intl }) { return footnoteId.replace(/[^A-Za-z0-9.-_]+/g, '-'); }; + const hasNoAccessToAssignmentsOfType = (assignmentType) => { + const subsectionAssignmentsOfType = sectionScores.map((chapter) => chapter.subsections.filter((subsection) => ( + subsection.assignmentType === assignmentType && subsection.hasGradedAssignment + && (subsection.numPointsPossible > 0 || subsection.numPointsEarned > 0) + ))).flat(); + if (subsectionAssignmentsOfType.length) { + const noAccessToAssignmentsOfType = !subsectionAssignmentsOfType.some((subsection) => ( + subsection.learnerHasAccess === true + )); + if (noAccessToAssignmentsOfType) { + setAllOfSomeAssignmentTypeIsLocked(true); + return true; + } + } + return false; + }; + const gradeSummaryData = assignmentPolicies.map((assignment) => { let footnoteId = ''; let footnoteMarker; @@ -44,11 +64,15 @@ function GradeSummaryTable({ intl }) { footnoteMarker = footnotes.length; } + const locked = !gradesFeatureIsFullyLocked && hasNoAccessToAssignmentsOfType(assignment.type) ? 'greyed-out' : ''; + return { - type: { footnoteId, footnoteMarker, type: assignment.type }, - weight: `${(assignment.weight * 100).toFixed(0)}%`, - grade: `${(assignment.averageGrade * 100).toFixed(0)}%`, - weightedGrade: `${(assignment.weightedGrade * 100).toFixed(0)}%`, + type: { + footnoteId, footnoteMarker, type: assignment.type, locked, + }, + weight: { weight: `${(assignment.weight * 100).toFixed(0)}%`, locked }, + grade: { grade: `${(assignment.averageGrade * 100).toFixed(0)}%`, locked }, + weightedGrade: { weightedGrade: `${(assignment.weightedGrade * 100).toFixed(0)}%`, locked }, }; }); @@ -67,6 +91,7 @@ function GradeSummaryTable({ intl }) { assignmentType={value.type} // eslint-disable-line react/prop-types footnoteId={value.footnoteId} // eslint-disable-line react/prop-types footnoteMarker={value.footnoteMarker} // eslint-disable-line react/prop-types + locked={value.locked} // eslint-disable-line react/prop-types /> ), headerClassName: 'h5 mb-0', @@ -75,18 +100,30 @@ function GradeSummaryTable({ intl }) { Header: `${intl.formatMessage(messages.weight)}`, accessor: 'weight', headerClassName: 'justify-content-end h5 mb-0', + // eslint-disable-next-line react/prop-types + Cell: ({ value }) => ( + {value.weight} // eslint-disable-line react/prop-types + ), cellClassName: 'float-right small', }, { Header: `${intl.formatMessage(messages.grade)}`, accessor: 'grade', headerClassName: 'justify-content-end h5 mb-0', + // eslint-disable-next-line react/prop-types + Cell: ({ value }) => ( + {value.grade} // eslint-disable-line react/prop-types + ), cellClassName: 'float-right small', }, { Header: `${intl.formatMessage(messages.weightedGrade)}`, accessor: 'weightedGrade', headerClassName: 'justify-content-end h5 mb-0 text-right', + // eslint-disable-next-line react/prop-types + Cell: ({ value }) => ( + {value.weightedGrade} // eslint-disable-line react/prop-types + ), cellClassName: 'float-right font-weight-bold small', }, ]} @@ -104,6 +141,7 @@ function GradeSummaryTable({ intl }) { GradeSummaryTable.propTypes = { intl: intlShape.isRequired, + setAllOfSomeAssignmentTypeIsLocked: PropTypes.func.isRequired, }; export default injectIntl(GradeSummaryTable); diff --git a/src/course-home/progress-tab/grades/messages.js b/src/course-home/progress-tab/grades/messages.js index 3fb24437..1d6efda5 100644 --- a/src/course-home/progress-tab/grades/messages.js +++ b/src/course-home/progress-tab/grades/messages.js @@ -29,10 +29,14 @@ const messages = defineMessages({ id: 'progress.courseGrade.footer.passing', defaultMessage: 'You’re currently passing this course with a grade of {letterGrade} ({minGrade}-{maxGrade}%)', }, - courseGradePreviewHeader: { - id: 'progress.courseGrade.preview.header', + courseGradePreviewHeaderLocked: { + id: 'progress.courseGrade.preview.headerLocked', defaultMessage: 'locked feature', }, + courseGradePreviewHeaderLimited: { + id: 'progress.courseGrade.preview.headerLimited', + defaultMessage: 'limited feature', + }, courseGradePreviewHeaderAriaHidden: { id: 'progress.courseGrade.preview.header.ariaHidden', defaultMessage: 'Preview of a ', @@ -41,6 +45,10 @@ const messages = defineMessages({ id: 'progress.courseGrade.preview.body.unlockCertificate', defaultMessage: 'Unlock to view grades and work towards a certificate.', }, + courseGradePartialPreviewUnlockCertificateBody: { + id: 'progress.courseGrade.partialpreview.body.unlockCertificate', + defaultMessage: 'Unlock to work towards a certificate.', + }, courseGradePreviewUpgradeDeadlinePassedBody: { id: 'progress.courseGrade.preview.body.upgradeDeadlinePassed', defaultMessage: 'The deadline to upgrade in this course has passed.', @@ -89,6 +97,10 @@ const messages = defineMessages({ id: 'progress.gradeSummary', defaultMessage: 'Grade summary', }, + gradeSummaryLimitedAccessExplanation: { + id: 'progress.gradeSummary.limitedAccessExplanation', + defaultMessage: 'You have limited access to graded assignments as part of the audit track in this course.', + }, gradeSummaryTooltipAlt: { id: 'progress.gradeSummary.tooltip.alt', defaultMessage: 'Grade summary tooltip', @@ -103,6 +115,14 @@ const messages = defineMessages({ id: 'progress.courseGrade.label.passingGrade', defaultMessage: 'Passing grade', }, + problemScoreLabel: { + id: 'progress.detailedGrades.problemScore.label', + defaultMessage: 'Problem Scores:', + }, + problemScoreToggleAltText: { + id: 'progress.detailedGrades.problemScore.toggleButton', + defaultMessage: 'Toggle individual problem scores for {subsectionTitle}', + }, score: { id: 'progress.score', defaultMessage: 'Score', @@ -119,6 +139,14 @@ const messages = defineMessages({ id: 'progress.weightedGradeSummary', defaultMessage: 'Your current weighted grade summary', }, + noAcessToAssignmentType: { + id: 'progress.noAcessToAssignmentType', + defaultMessage: 'You do not have access to assignments of type {assignmentType}', + }, + noAcessToSubsection: { + id: 'progress.noAcessToSubsection', + defaultMessage: 'You do not have access to subsection {displayName}', + }, }); export default messages; diff --git a/src/courseware/CoursewareContainer.jsx b/src/courseware/CoursewareContainer.jsx index b64c3ed0..28ef1333 100644 --- a/src/courseware/CoursewareContainer.jsx +++ b/src/courseware/CoursewareContainer.jsx @@ -2,8 +2,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { history } from '@edx/frontend-platform'; -import { getLocale } from '@edx/frontend-platform/i18n'; -import { Redirect } from 'react-router'; import { createSelector } from '@reduxjs/toolkit'; import { defaultMemoize as memoize } from 'reselect'; @@ -244,42 +242,6 @@ class CoursewareContainer extends Component { } } - renderDenied() { - const { - course, - courseId, - match: { - params: { - unitId: routeUnitId, - }, - }, - } = this.props; - let url = `/redirect/course-home/${courseId}`; - switch (course.canLoadCourseware.errorCode) { - case 'audit_expired': - url = `/redirect/dashboard?access_response_error=${course.canLoadCourseware.additionalContextUserMessage}`; - break; - case 'course_not_started': - // eslint-disable-next-line no-case-declarations - const startDate = (new Intl.DateTimeFormat(getLocale())).format(new Date(course.start)); - url = `/redirect/dashboard?notlive=${startDate}`; - break; - case 'survey_required': // TODO: Redirect to the course survey - case 'unfulfilled_milestones': - url = '/redirect/dashboard'; - break; - case 'microfrontend_disabled': - url = `/redirect/courseware/${courseId}/unit/${routeUnitId}`; - break; - case 'authentication_required': - case 'enrollment_required': - default: - } - return ( - - ); - } - render() { const { courseStatus, @@ -292,10 +254,6 @@ class CoursewareContainer extends Component { }, } = this.props; - if (courseStatus === 'denied') { - return this.renderDenied(); - } - return ( { }); }); - describe('when receiving a can_load_courseware error_code', () => { + describe('when receiving a course_access error_code', () => { function setUpWithDeniedStatus(errorCode) { const courseMetadata = Factory.build('courseMetadata', { - can_load_courseware: { + course_access: { has_access: false, error_code: errorCode, additional_context_user_message: 'uhoh oh no', // only used by audit_expired }, }); const courseId = courseMetadata.id; - const { courseBlocks } = buildSimpleCourseBlocks(courseId, courseMetadata.name); + + const { courseBlocks, sequenceBlocks, unitBlocks } = buildSimpleCourseBlocks(courseId, courseMetadata.name); setUpMockRequests({ courseBlocks, courseMetadata }); - history.push(`/course/${courseId}`); - return courseMetadata; + history.push(`/course/${courseId}/${sequenceBlocks[0].id}/${unitBlocks[0].id}`); + return { courseMetadata, unitBlocks }; } it('should go to course home for an enrollment_required error code', async () => { - const courseMetadata = setUpWithDeniedStatus('enrollment_required'); + const { courseMetadata } = setUpWithDeniedStatus('enrollment_required'); await loadContainer(); expect(global.location.href).toEqual(`http://localhost/redirect/course-home/${courseMetadata.id}`); }); + it('should go to course survey for a survey_required error code', async () => { + const { courseMetadata } = setUpWithDeniedStatus('survey_required'); + await loadContainer(); + + expect(global.location.href).toEqual(`http://localhost/redirect/survey/${courseMetadata.id}`); + }); + + it('should go to legacy courseware for a microfrontend_disabled error code', async () => { + const { courseMetadata, unitBlocks } = setUpWithDeniedStatus('microfrontend_disabled'); + await loadContainer(); + + expect(global.location.href).toEqual(`http://localhost/redirect/courseware/${courseMetadata.id}/unit/${unitBlocks[0].id}`); + }); + it('should go to course home for an authentication_required error code', async () => { - const courseMetadata = setUpWithDeniedStatus('authentication_required'); + const { courseMetadata } = setUpWithDeniedStatus('authentication_required'); await loadContainer(); expect(global.location.href).toEqual(`http://localhost/redirect/course-home/${courseMetadata.id}`); diff --git a/src/courseware/CoursewareRedirectLandingPage.jsx b/src/courseware/CoursewareRedirectLandingPage.jsx index 246a5404..f63ce4af 100644 --- a/src/courseware/CoursewareRedirectLandingPage.jsx +++ b/src/courseware/CoursewareRedirectLandingPage.jsx @@ -31,6 +31,12 @@ export default () => { global.location.assign(`${getConfig().LMS_BASE_URL}/courses/${match.params.courseId}/course/`); }} /> + { + global.location.assign(`${getConfig().LMS_BASE_URL}/courses/${match.params.courseId}/survey`); + }} + /> { diff --git a/src/courseware/course/Course.jsx b/src/courseware/course/Course.jsx index d64a7f6d..217d7336 100644 --- a/src/courseware/course/Course.jsx +++ b/src/courseware/course/Course.jsx @@ -2,12 +2,9 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { Helmet } from 'react-helmet'; import { useDispatch } from 'react-redux'; -import Cookies from 'js-cookie'; import { getConfig } from '@edx/frontend-platform'; import { AlertList } from '../../generic/user-messages'; -import useAccessExpirationAlert from '../../alerts/access-expiration-alert'; -import useOfferAlert from '../../alerts/offer-alert'; import Sequence from './sequence'; @@ -42,33 +39,26 @@ function Course({ ].filter(element => element != null).map(element => element.title); const { - accessExpiration, canShowUpgradeSock, celebrations, offer, org, - userTimezone, verifiedMode, } = course; // Below the tabs, above the breadcrumbs alerts (appearing in the order listed here) - const offerAlert = useOfferAlert(courseId, offer, org, userTimezone, 'course', 'in_course'); - const accessExpirationAlert = useAccessExpirationAlert(accessExpiration, courseId, org, userTimezone, 'course', 'in_course'); - const dispatch = useDispatch(); const celebrateFirstSection = celebrations && celebrations.firstSection; const celebrationOpen = shouldCelebrateOnSectionLoad( courseId, sequenceId, unitId, celebrateFirstSection, dispatch, celebrations, ); - // REV-2297 TODO: temporary cookie code that should be removed. - // In order to see the Value Prop sidebar in prod, a cookie should be set in - // the browser console and refresh: document.cookie = 'value_prop_cookie=true'; - const isValuePropCookieSet = Cookies.get('value_prop_cookie') === 'true'; - const shouldDisplayNotificationTrigger = useWindowSize().width >= responsiveBreakpoints.small.minWidth; - const [notificationTrayVisible, setNotificationTray] = verifiedMode ? useState(true) : useState(false); + const shouldDisplayNotificationTrayOpen = useWindowSize().width > responsiveBreakpoints.medium.minWidth; + + const [notificationTrayVisible, setNotificationTray] = verifiedMode + && shouldDisplayNotificationTrayOpen ? useState(true) : useState(false); const isNotificationTrayVisible = () => notificationTrayVisible && setNotificationTray; const toggleNotificationTray = () => { if (notificationTrayVisible) { setNotificationTray(false); } else { setNotificationTray(true); } @@ -82,17 +72,6 @@ function Course({ {`${pageTitleBreadCrumbs.join(' | ')} | ${getConfig().SITE_NAME}`} - { /** This conditional is for the [MM-P2P] Experiment */} - { !MMP2P.state.isEnabled && ( - - )}
- { isValuePropCookieSet && shouldDisplayNotificationTrigger ? ( + { shouldDisplayNotificationTrigger ? ( diff --git a/src/courseware/course/Course.test.jsx b/src/courseware/course/Course.test.jsx index 5b795e5f..dd4a7785 100644 --- a/src/courseware/course/Course.test.jsx +++ b/src/courseware/course/Course.test.jsx @@ -1,15 +1,16 @@ import React from 'react'; import { Factory } from 'rosie'; -import Cookies from 'js-cookie'; -import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { loadUnit, render, screen, waitFor, getByRole, initializeTestStore, fireEvent, } from '../../setupTest'; import Course from './Course'; import { handleNextSectionCelebration } from './celebration'; import * as celebrationUtils from './celebration/utils'; +import useWindowSize from '../../generic/tabs/useWindowSize'; jest.mock('@edx/frontend-platform/analytics'); +jest.mock('../../generic/tabs/useWindowSize'); +useWindowSize.mockReturnValue({ width: 1200 }); const recordFirstSectionCelebration = jest.fn(); celebrationUtils.recordFirstSectionCelebration = recordFirstSectionCelebration; @@ -20,7 +21,6 @@ describe('Course', () => { nextSequenceHandler: () => {}, previousSequenceHandler: () => {}, unitNavigationHandler: () => {}, - toggleNotificationTray: () => {}, }; beforeAll(async () => { @@ -87,115 +87,16 @@ describe('Course', () => { expect(screen.getByRole('button', { name: 'Learn About Verified Certificates' })).toBeInTheDocument(); }); - it('displays notification trigger', async () => { - const toggleNotificationTray = jest.fn(); - const isNotificationTrayVisible = jest.fn(); + it('displays notification trigger and toggles active class on click', async () => { + useWindowSize.mockReturnValue({ width: 1200 }); + render(); - // REV-2297 TODO: remove cookie related code once temporary value prop cookie code is removed. - const cookieName = 'value_prop_cookie'; - Cookies.set = jest.fn(); - Cookies.get = jest.fn().mockImplementation(() => cookieName); - const getSpy = jest.spyOn(Cookies, 'get').mockReturnValueOnce('true'); + const notificationTrigger = screen.getByRole('button', { name: /Show notification tray/i }); - const courseMetadata = Factory.build('courseMetadata'); - const testStore = await initializeTestStore({ courseMetadata, excludeFetchSequence: true }, false); - const testData = { - ...mockData, - toggleNotificationTray, - isNotificationTrayVisible, - }; - render(, { store: testStore }); - - const notificationOpenButton = screen.getByRole('button', { name: /Show notification tray/i }); - - expect(getSpy).toBeCalledWith(cookieName); - expect(notificationOpenButton).toBeInTheDocument(); - }); - - it('displays offer and expiration alert', async () => { - const courseMetadata = Factory.build('courseMetadata', { - access_expiration: { - expiration_date: '2080-01-01T12:00:00Z', - masquerading_expired_course: false, - upgrade_deadline: null, - upgrade_url: null, - }, - offer: { - code: 'EDXWELCOME', - expiration_date: '2070-01-01T12:00:00Z', - original_price: '$100', - discounted_price: '$85', - percentage: 15, - upgrade_url: 'https://example.com/upgrade', - }, - }); - const testStore = await initializeTestStore({ courseMetadata, excludeFetchSequence: true }, false); - render(, { store: testStore }); - - await screen.findByText('EDXWELCOME'); - await screen.findByText('Audit Access Expires'); - }); - - it('sends analytics event onClick of access expiration upgrade link', async () => { - sendTrackEvent.mockClear(); - - const courseMetadata = Factory.build('courseMetadata', { - access_expiration: { - expiration_date: '2080-01-01T12:00:00Z', - masquerading_expired_course: false, - upgrade_deadline: '2070-01-01T12:00:00Z', - upgrade_url: 'https://example.com/upgrade', - }, - user_timezone: 'UTC', - }); - const testStore = await initializeTestStore({ courseMetadata, excludeFetchSequence: true }, false); - render(, { store: testStore }); - await screen.findByText('Audit Access Expires'); - - const upgradeLink = screen.getByRole('link', { name: 'Upgrade now' }); - fireEvent.click(upgradeLink); - - expect(sendTrackEvent).toHaveBeenCalledTimes(1); - expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.ecommerce.upsell_links_clicked', { - org_key: 'edX', - courserun_key: courseMetadata.id, - linkCategory: 'FBE_banner', - linkName: 'in_course_audit_access_expires', - linkType: 'link', - pageName: 'in_course', - }); - }); - - it('sends analytics event onClick of offer alert link', async () => { - sendTrackEvent.mockClear(); - - const courseMetadata = Factory.build('courseMetadata', { - offer: { - code: 'EDXWELCOME', - expiration_date: '2070-01-01T12:00:00Z', - original_price: '$100', - discounted_price: '$85', - percentage: 15, - upgrade_url: 'https://example.com/upgrade', - }, - user_timezone: 'UTC', - }); - const testStore = await initializeTestStore({ courseMetadata, excludeFetchSequence: true }, false); - render(, { store: testStore }); - await screen.findByText('EDXWELCOME'); - - const upgradeLink = screen.getByRole('link', { name: 'Upgrade now' }); - fireEvent.click(upgradeLink); - - expect(sendTrackEvent).toHaveBeenCalledTimes(1); - expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.ecommerce.upsell_links_clicked', { - org_key: 'edX', - courserun_key: courseMetadata.id, - linkCategory: 'welcome', - linkName: 'in_course_welcome', - linkType: 'link', - pageName: 'in_course', - }); + expect(notificationTrigger).toBeInTheDocument(); + expect(notificationTrigger).toHaveClass('trigger-active'); + fireEvent.click(notificationTrigger); + expect(notificationTrigger).not.toHaveClass('trigger-active'); }); it('passes handlers to the sequence', async () => { diff --git a/src/courseware/course/NotificationIcon.jsx b/src/courseware/course/NotificationIcon.jsx index da8f0112..ae8c08c5 100644 --- a/src/courseware/course/NotificationIcon.jsx +++ b/src/courseware/course/NotificationIcon.jsx @@ -5,7 +5,6 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Icon } from '@edx/paragon'; import { WatchOutline } from '@edx/paragon/icons'; -import './NotificationIcon.scss'; import messages from './messages'; function NotificationIcon({ intl, status, notificationColor }) { diff --git a/src/courseware/course/NotificationTray.jsx b/src/courseware/course/NotificationTray.jsx index 85b83d19..779853b5 100644 --- a/src/courseware/course/NotificationTray.jsx +++ b/src/courseware/course/NotificationTray.jsx @@ -3,7 +3,7 @@ import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { Icon } from '@edx/paragon'; +import { Icon, IconButton } from '@edx/paragon'; import { ArrowBackIos, Close } from '@edx/paragon/icons'; import messages from './messages'; @@ -40,11 +40,15 @@ function NotificationTray({ {intl.formatMessage(messages.responsiveCloseNotificationTray)}
) : null} -
- {intl.formatMessage(messages.notificationTitle)} +
+ {intl.formatMessage(messages.notificationTitle)} {shouldDisplayFullScreen ? null - : { toggleNotificationTray(); }} onKeyDown={() => { toggleNotificationTray(); }} role="button" tabIndex="0" alt={intl.formatMessage(messages.closeNotificationTrigger)} />} + : ( +
+ { toggleNotificationTray(); }} variant="primary" alt={intl.formatMessage(messages.closeNotificationTrigger)} /> +
+ )}
{verifiedMode diff --git a/src/courseware/course/NotificationTray.scss b/src/courseware/course/NotificationTray.scss index b3471a81..b4ac9234 100644 --- a/src/courseware/course/NotificationTray.scss +++ b/src/courseware/course/NotificationTray.scss @@ -23,24 +23,22 @@ height: 15rem; } -.notification-tray-header { - padding: 0.625rem 0; - - span { - display: inline-block; - } +.notification-tray-title { + display: inline-block; + padding: 0.625rem 0 0.625rem 1.25rem; } .close-btn { float: right; + margin-right: 0.5rem; + margin-top: 0.35rem; } .notification-tray-divider { - width: 100.5%; height: 0.5rem; background: $gray-100; - border: 1px solid $light-400; - border-left: 0; + border-top: 1px solid $light-400; + border-bottom: 1px solid $light-400; } .notification-tray-content { diff --git a/src/courseware/course/NotificationTray.test.jsx b/src/courseware/course/NotificationTray.test.jsx index acbcf0af..6d688bd4 100644 --- a/src/courseware/course/NotificationTray.test.jsx +++ b/src/courseware/course/NotificationTray.test.jsx @@ -47,10 +47,23 @@ describe('NotificationTray', () => { }; }); - it('renders notification tray', async () => { - useWindowSize.mockReturnValue({ width: 1200, height: 422 }); - await fetchAndRender(); + it('renders notification tray and close tray button', async () => { + useWindowSize.mockReturnValue({ width: 1200 }); + const toggleNotificationTray = jest.fn(); + const testData = { + ...mockData, + toggleNotificationTray, + }; + await fetchAndRender(); + expect(screen.getByText('Notifications')).toBeInTheDocument(); + const notificationCloseIconButton = screen.getByRole('button', { name: /Close notification tray/i }); + expect(notificationCloseIconButton).toBeInTheDocument(); + expect(notificationCloseIconButton).toHaveClass('btn-icon-primary'); + fireEvent.click(notificationCloseIconButton); + expect(toggleNotificationTray).toHaveBeenCalledTimes(1); + + // should not render responsive "Back to course" to close the tray expect(screen.queryByText('Back to course')).not.toBeInTheDocument(); }); @@ -69,8 +82,8 @@ describe('NotificationTray', () => { expect(screen.queryByText('You have no new notifications at this time.')).toBeInTheDocument(); }); - it('renders notification tray with full screen "Back to course" at response width', async () => { - useWindowSize.mockReturnValue({ width: 991, height: 422 }); + it('renders notification tray with full screen "Back to course" at responsive view', async () => { + useWindowSize.mockReturnValue({ width: 991 }); const toggleNotificationTray = jest.fn(); const testData = { ...mockData, diff --git a/src/courseware/course/NotificationTrigger.jsx b/src/courseware/course/NotificationTrigger.jsx index 183a35a1..3486ae4e 100644 --- a/src/courseware/course/NotificationTrigger.jsx +++ b/src/courseware/course/NotificationTrigger.jsx @@ -9,7 +9,7 @@ import messages from './messages'; function NotificationTrigger({ intl, toggleNotificationTray, isNotificationTrayVisible }) { return (
-
+ -
+
- +
{certHeader && ( - -
-
{certHeader}
- {message} -
- {buttonPrefix} - {buttonLocation && ( - - )} - {buttonSuffix} + +
+
+
{certHeader}
+ {message} +
+ {buttonPrefix} + {buttonLocation && ( + + )} + {buttonSuffix} +
+ {certStatus !== 'unverified' && ( +
+ {`${intl.formatMessage(messages.certificateImage)}`} +
+ )}
- {certStatus !== 'unverified' && ( -
- {`${intl.formatMessage(messages.certificateImage)}`} -
- )}
)} {relatedPrograms && relatedPrograms.map(program => ( diff --git a/src/courseware/course/course-exit/CourseInProgress.jsx b/src/courseware/course/course-exit/CourseInProgress.jsx index 96132f9e..a575d3a9 100644 --- a/src/courseware/course/course-exit/CourseInProgress.jsx +++ b/src/courseware/course/course-exit/CourseInProgress.jsx @@ -34,13 +34,13 @@ function CourseInProgress({ intl }) {
{ intl.formatMessage(messages.courseInProgressHeader) }
- +
-
{ intl.formatMessage(messages.courseInProgressDescription) }
+
{ intl.formatMessage(messages.courseInProgressDescription) }
{datesTabLink && ( + + )} + {type === 'micromasters' && (

+ {intl.formatMessage(messages.microMastersMessage)} + {' '} - {intl.formatMessage(messages.microBachelorsLearnMore)} + {intl.formatMessage(messages.microMastersLearnMore)}

- - - )} - {type === 'micromasters' && ( -

- {intl.formatMessage(messages.microMastersMessage)} - {' '} - - {intl.formatMessage(messages.microMastersLearnMore)} - -

- )} -
-
- {`${intl.formatMessage(messages.certificateImage)}`} + )} +
+
+ {`${intl.formatMessage(messages.certificateImage)}`} +
); diff --git a/src/courseware/course/messages.js b/src/courseware/course/messages.js index 1295797c..acf6cc27 100644 --- a/src/courseware/course/messages.js +++ b/src/courseware/course/messages.js @@ -13,7 +13,7 @@ const messages = defineMessages({ }, closeNotificationTrigger: { id: 'notification.close.button', - defaultMessage: 'Close sidebar notification', + defaultMessage: 'Close notification tray', description: 'Button for the learner to close the sidebar', }, responsiveCloseNotificationTray: { diff --git a/src/courseware/course/sequence/Sequence.jsx b/src/courseware/course/sequence/Sequence.jsx index 5be4abb5..c274a65b 100644 --- a/src/courseware/course/sequence/Sequence.jsx +++ b/src/courseware/course/sequence/Sequence.jsx @@ -40,7 +40,6 @@ function Sequence({ toggleNotificationTray, notificationTrayVisible, isNotificationTrayVisible, - isValuePropCookieSet, mmp2p, }) { const course = useModel('coursewareMeta', courseId); @@ -189,10 +188,9 @@ function Sequence({ handlePrevious(); }} goToCourseExitPage={() => goToCourseExitPage()} - isValuePropCookieSet={isValuePropCookieSet} /> - {isValuePropCookieSet && shouldDisplayNotificationTrigger ? ( + {shouldDisplayNotificationTrigger ? ( @@ -226,7 +225,7 @@ function Sequence({ )}
- {isValuePropCookieSet && notificationTrayVisible ? ( + {notificationTrayVisible ? ( { let mockData; @@ -28,7 +31,6 @@ describe('Sequence', () => { unitNavigationHandler: () => {}, nextSequenceHandler: () => {}, previousSequenceHandler: () => {}, - notificationTrayVisible: false, }; }); @@ -130,17 +132,6 @@ describe('Sequence', () => { expect(screen.getAllByRole('button', { name: /previous|next/i }).length).toEqual(4); }); - it('renders notification tray in sequence', async () => { - const testData = { - ...mockData, - notificationTrayVisible: true, - isValuePropCookieSet: true, - }; - - render(); - expect(await screen.findByText('Notifications')).toBeInTheDocument(); - }); - describe('sequence and unit navigation buttons', () => { let testStore; const sequenceBlocks = [Factory.build( @@ -388,4 +379,22 @@ describe('Sequence', () => { }); }); }); + + describe('notification feature', () => { + it('renders notification tray in sequence', async () => { + const testData = { + ...mockData, + notificationTrayVisible: true, + }; + render(); + expect(await screen.findByText('Notifications')).toBeInTheDocument(); + }); + + it('does not render notification tray in sequence by default if in responsive view', async () => { + useWindowSize.mockReturnValue({ width: 991 }); + const { container } = render(); + // unable to test the absence of 'Notifications' by finding it by text, using the class of the tray instead: + expect(container).not.toHaveClass('notification-tray-container'); + }); + }); }); diff --git a/src/courseware/course/sequence/SequenceContent.jsx b/src/courseware/course/sequence/SequenceContent.jsx index d6eff201..110dd0c3 100644 --- a/src/courseware/course/sequence/SequenceContent.jsx +++ b/src/courseware/course/sequence/SequenceContent.jsx @@ -16,6 +16,7 @@ function SequenceContent({ sequenceId, unitId, unitLoadedHandler, + notificationTrayVisible, /** [MM-P2P] Experiment */ mmp2p, }) { @@ -61,6 +62,7 @@ function SequenceContent({ key={unitId} id={unitId} onLoaded={unitLoadedHandler} + notificationTrayVisible={notificationTrayVisible} /** [MM-P2P] Experiment */ mmp2p={mmp2p} /> @@ -73,6 +75,7 @@ SequenceContent.propTypes = { sequenceId: PropTypes.string.isRequired, unitId: PropTypes.string, unitLoadedHandler: PropTypes.func.isRequired, + notificationTrayVisible: PropTypes.bool.isRequired, intl: intlShape.isRequired, /** [MM-P2P] Experiment */ mmp2p: PropTypes.shape({ diff --git a/src/courseware/course/sequence/Unit.jsx b/src/courseware/course/sequence/Unit.jsx index 98c88c37..d761f307 100644 --- a/src/courseware/course/sequence/Unit.jsx +++ b/src/courseware/course/sequence/Unit.jsx @@ -85,6 +85,7 @@ function Unit({ onLoaded, id, intl, + notificationTrayVisible, /** [MM-P2P] Experiment */ mmp2p, }) { @@ -171,7 +172,7 @@ function Unit({ /> )} > - + )} { /** [MM-P2P] Experiment */ } @@ -252,6 +253,7 @@ Unit.propTypes = { id: PropTypes.string.isRequired, intl: intlShape.isRequired, onLoaded: PropTypes.func, + notificationTrayVisible: PropTypes.bool.isRequired, /** [MM-P2P] Experiment */ mmp2p: PropTypes.shape({ state: PropTypes.shape({ diff --git a/src/courseware/course/sequence/lock-paywall/LockPaywall.jsx b/src/courseware/course/sequence/lock-paywall/LockPaywall.jsx index c8b62935..97ebab25 100644 --- a/src/courseware/course/sequence/lock-paywall/LockPaywall.jsx +++ b/src/courseware/course/sequence/lock-paywall/LockPaywall.jsx @@ -1,22 +1,22 @@ import React from 'react'; import PropTypes from 'prop-types'; +import classNames from 'classnames'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCheck } from '@fortawesome/free-solid-svg-icons'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { - Alert, Icon, -} from '@edx/paragon'; +import { Alert } from '@edx/paragon'; import { Locked } from '@edx/paragon/icons'; import messages from './messages'; import certificateLocked from '../../../../generic/assets/edX_locked_certificate.png'; import { useModel } from '../../../../generic/model-store'; +import useWindowSize, { responsiveBreakpoints } from '../../../../generic/tabs/useWindowSize'; import { UpgradeButton } from '../../../../generic/upgrade-button'; -import './LockPaywall.scss'; function LockPaywall({ intl, courseId, + notificationTrayVisible, }) { const course = useModel('coursewareMeta', courseId); const { @@ -25,6 +25,18 @@ function LockPaywall({ verifiedMode, } = course; + // the following variables are set and used for resposive layout to work with + // whether the NotificationTray is open or not and if there's an offer with longer text + const shouldDisplayBulletPointsBelowCertificate = useWindowSize().width + <= responsiveBreakpoints.large.minWidth; + const shouldDisplayGatedContentOneColumn = useWindowSize().width <= responsiveBreakpoints.extraLarge.minWidth + && notificationTrayVisible; + const shouldDisplayGatedContentTwoColumns = useWindowSize().width < responsiveBreakpoints.large.minWidth + && notificationTrayVisible; + const shouldDisplayGatedContentTwoColumnsHalf = useWindowSize().width <= responsiveBreakpoints.large.minWidth + && !notificationTrayVisible; + const shouldWrapTextOnButton = useWindowSize().width > responsiveBreakpoints.extraSmall.minWidth; + if (!verifiedMode) { return null; } @@ -44,14 +56,6 @@ function LockPaywall({ }); }; - const lockIcon = ( -