From 2fa4a837b1bc2bb46f65c98ac0f8272b9eeb8f02 Mon Sep 17 00:00:00 2001 From: Carla Duarte Date: Tue, 14 Dec 2021 12:53:10 -0500 Subject: [PATCH] feat: new user course home tour (AA-1027) (#750) --- .../0010-tour-structure-decisions.md | 32 ++ package-lock.json | 6 +- package.json | 2 +- src/course-header/CourseTabsNavigation.jsx | 2 +- src/course-header/Header.jsx | 9 +- .../courseHomeMetadata.factory.js | 1 + .../data/__snapshots__/redux.test.js.snap | 24 ++ src/course-home/outline-tab/OutlineTab.jsx | 2 +- .../outline-tab/OutlineTab.test.jsx | 9 +- .../outline-tab/widgets/CourseDates.jsx | 32 +- .../outline-tab/widgets/CourseTools.jsx | 4 + .../widgets/WeeklyLearningGoalCard.jsx | 1 + src/courseware/course/NotificationTray.scss | 1 + .../SequenceNavigation.jsx | 2 +- .../UpgradeNotification.jsx | 32 +- src/index.scss | 1 + src/product-tours/AbandonTour.jsx | 22 ++ src/product-tours/CoursewareTour.jsx | 22 ++ .../ExistingUserCourseHomeTour.jsx | 22 ++ .../GenericTourFormattedMessages.jsx | 32 ++ src/product-tours/ProductTours.jsx | 184 +++++++++++ src/product-tours/ProductTours.test.jsx | 308 ++++++++++++++++++ src/product-tours/data/api.js | 26 ++ src/product-tours/data/index.js | 8 + src/product-tours/data/slice.js | 58 ++++ src/product-tours/data/thunks.js | 50 +++ src/product-tours/messages.js | 30 ++ .../LaunchCourseHomeTourButton.jsx | 71 ++++ .../NewUserCourseHomeTour.jsx | 120 +++++++ .../NewUserCourseHomeTourModal.jsx | 73 +++++ .../course_home_tour_modal_hero.png | Bin 0 -> 18916 bytes src/store.js | 2 + src/tab-page/LoadedTabPage.jsx | 14 +- src/tab-page/LoadedTabPage.test.jsx | 1 + src/tab-page/TabPage.jsx | 2 + src/tour/Checkpoint.jsx | 88 ++++- src/tour/Checkpoint.scss | 63 +++- src/tour/CheckpointActionRow.jsx | 9 +- src/tour/CheckpointBreadcrumbs.jsx | 4 +- src/tour/README.md | 2 + src/tour/Tour.jsx | 134 +++----- src/tour/tests/Checkpoint.test.jsx | 167 +++++----- src/tour/tests/Tour.test.jsx | 254 +++++++++------ 43 files changed, 1599 insertions(+), 327 deletions(-) create mode 100644 docs/decisions/0010-tour-structure-decisions.md create mode 100644 src/product-tours/AbandonTour.jsx create mode 100644 src/product-tours/CoursewareTour.jsx create mode 100644 src/product-tours/ExistingUserCourseHomeTour.jsx create mode 100644 src/product-tours/GenericTourFormattedMessages.jsx create mode 100644 src/product-tours/ProductTours.jsx create mode 100644 src/product-tours/ProductTours.test.jsx create mode 100644 src/product-tours/data/api.js create mode 100644 src/product-tours/data/index.js create mode 100644 src/product-tours/data/slice.js create mode 100644 src/product-tours/data/thunks.js create mode 100644 src/product-tours/messages.js create mode 100644 src/product-tours/newUserCourseHomeTour/LaunchCourseHomeTourButton.jsx create mode 100644 src/product-tours/newUserCourseHomeTour/NewUserCourseHomeTour.jsx create mode 100644 src/product-tours/newUserCourseHomeTour/NewUserCourseHomeTourModal.jsx create mode 100644 src/product-tours/newUserCourseHomeTour/course_home_tour_modal_hero.png diff --git a/docs/decisions/0010-tour-structure-decisions.md b/docs/decisions/0010-tour-structure-decisions.md new file mode 100644 index 00000000..76e3329d --- /dev/null +++ b/docs/decisions/0010-tour-structure-decisions.md @@ -0,0 +1,32 @@ +# Tour Structure Decisions + +## Compartmentalizing the tour objects +We created the directory `src/tours` in order to organize tours across the MFE. Each tour has its own JSX file where we +define a tour object to be passed to the `` component in ``. + +Although each tour is stored in a JSX file, the tour object itself is meant to be an `object` type. Thus, the structure +of each tour object is as follows: +```$xslt +// Note: this is a simplified version of a tour object + +const exampleTour = (enabled) => ({ + checkpoints: [], + enabled, + tourId: 'exampleTour', +}); +``` + +The reason we use a JSX file rather than a JS file is to allow for use of React components within the objects such as +``. + +## Implementing i18n in tour objects +The `` component ingests a single prop called `tours` which expects a list of objects. +Given the structure in which we organized tour objects, there were two considerations in working with i18n: +- You can't injectIntl into something that isn't a React component without considerable adjustments, +so using the familiar `{intl.formatMessage(messages.foo)}` syntax would not be possible. +- You can't return normal objects from a React component, only React elements. I.e. switching these from arrow functions + to React function based components would not be ideal because the `tours` prop expects objects. + +### Decision +We chose to use `` directly within the tour objects. We also created shared `` +components inside of `GenericTourFormattedMessages.jsx` for use across the tours. diff --git a/package-lock.json b/package-lock.json index 08a01023..6f2bd3cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4149,9 +4149,9 @@ } }, "@edx/paragon": { - "version": "16.18.0", - "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-16.18.0.tgz", - "integrity": "sha512-VxNYFymoGQx0SG0wuotYPbco98Qk2Ao6gZUFyRCb49QIK0kjmnhdV04ltWfG2X7RNjziuVKsak43NbI53Zo5TQ==", + "version": "16.19.0", + "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-16.19.0.tgz", + "integrity": "sha512-1URyxUC42JT+DF7hWY75SrY4q/zU9dDB8noiIsSVuB2LCoYze209PATmp5L7DBRj6d4m3b859SPqvPD9v2P65Q==", "requires": { "@fortawesome/fontawesome-svg-core": "^1.2.30", "@fortawesome/free-solid-svg-icons": "^5.14.0", diff --git a/package.json b/package.json index 16eb5c3a..e744d798 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@edx/frontend-enterprise-utils": "1.1.1", "@edx/frontend-lib-special-exams": "1.14.1", "@edx/frontend-platform": "1.14.3", - "@edx/paragon": "16.18.0", + "@edx/paragon": "16.19.0", "@fortawesome/fontawesome-svg-core": "1.2.36", "@fortawesome/free-brands-svg-icons": "5.15.4", "@fortawesome/free-regular-svg-icons": "5.15.4", diff --git a/src/course-header/CourseTabsNavigation.jsx b/src/course-header/CourseTabsNavigation.jsx index 62366adf..ec84ce07 100644 --- a/src/course-header/CourseTabsNavigation.jsx +++ b/src/course-header/CourseTabsNavigation.jsx @@ -10,7 +10,7 @@ function CourseTabsNavigation({ activeTabSlug, className, tabs, intl, }) { return ( -
+
+ {showLaunchTourButton && ()} {intl.formatMessage(messages.skipNavLink)}
{headerLogo} @@ -86,6 +89,8 @@ Header.propTypes = { courseNumber: PropTypes.string, courseTitle: PropTypes.string, intl: intlShape.isRequired, + metadataModel: PropTypes.string, + showLaunchTourButton: PropTypes.bool, showUserDropdown: PropTypes.bool, }; @@ -93,6 +98,8 @@ Header.defaultProps = { courseOrg: null, courseNumber: null, courseTitle: null, + metadataModel: 'courseHomeMeta', + showLaunchTourButton: false, showUserDropdown: true, }; diff --git a/src/course-home/data/__factories__/courseHomeMetadata.factory.js b/src/course-home/data/__factories__/courseHomeMetadata.factory.js index f6fe95b0..02bc83c2 100644 --- a/src/course-home/data/__factories__/courseHomeMetadata.factory.js +++ b/src/course-home/data/__factories__/courseHomeMetadata.factory.js @@ -20,4 +20,5 @@ Factory.define('courseHomeMetadata') }, start: '2013-02-05T05:00:00Z', user_timezone: 'UTC', + username: 'testuser', }); diff --git a/src/course-home/data/__snapshots__/redux.test.js.snap b/src/course-home/data/__snapshots__/redux.test.js.snap index f31764d8..ab8ac292 100644 --- a/src/course-home/data/__snapshots__/redux.test.js.snap +++ b/src/course-home/data/__snapshots__/redux.test.js.snap @@ -71,6 +71,7 @@ Object { ], "title": "Demonstration Course", "userTimezone": "UTC", + "username": "testuser", "verifiedMode": Object { "currencySymbol": "$", "price": 10, @@ -301,6 +302,13 @@ Object { "recommendations": Object { "recommendationsStatus": "loading", }, + "tours": Object { + "showCoursewareTour": false, + "showExistingUserCourseHomeTour": false, + "showNewUserCourseHomeModal": false, + "showNewUserCourseHomeTour": false, + "toursEnabled": false, + }, } `; @@ -375,6 +383,7 @@ Object { ], "title": "Demonstration Course", "userTimezone": "UTC", + "username": "testuser", "verifiedMode": Object { "currencySymbol": "$", "price": 10, @@ -485,6 +494,13 @@ Object { "recommendations": Object { "recommendationsStatus": "loading", }, + "tours": Object { + "showCoursewareTour": false, + "showExistingUserCourseHomeTour": false, + "showNewUserCourseHomeModal": false, + "showNewUserCourseHomeTour": false, + "toursEnabled": false, + }, } `; @@ -559,6 +575,7 @@ Object { ], "title": "Demonstration Course", "userTimezone": "UTC", + "username": "testuser", "verifiedMode": Object { "currencySymbol": "$", "price": 10, @@ -674,5 +691,12 @@ Object { "recommendations": Object { "recommendationsStatus": "loading", }, + "tours": Object { + "showCoursewareTour": false, + "showExistingUserCourseHomeTour": false, + "showNewUserCourseHomeModal": false, + "showNewUserCourseHomeTour": false, + "toursEnabled": false, + }, } `; diff --git a/src/course-home/outline-tab/OutlineTab.jsx b/src/course-home/outline-tab/OutlineTab.jsx index 86f434e8..cfcbf8dd 100644 --- a/src/course-home/outline-tab/OutlineTab.jsx +++ b/src/course-home/outline-tab/OutlineTab.jsx @@ -185,7 +185,7 @@ function OutlineTab({ intl }) {
-
    +
      {courses[rootCourseId].sectionIds.map((sectionId) => (
      { const goalUrl = `${getConfig().LMS_BASE_URL}/api/course_home/save_course_goal`; const masqueradeUrl = `${getConfig().LMS_BASE_URL}/courses/${courseId}/masquerade`; const outlineUrl = `${getConfig().LMS_BASE_URL}/api/course_home/outline/${courseId}`; - const proctoringInfoUrl = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/user_onboarding/status?is_learning_mfe=true&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)}&username=testuser`; const store = initializeStore(); const defaultMetadata = Factory.build('courseHomeMetadata', { id: courseId }); @@ -1322,7 +1325,7 @@ describe('Outline Tab', () => { }); }); - describe('Accont Activation Alert', () => { + describe('Account Activation Alert', () => { beforeEach(() => { const intersectionObserverMock = () => ({ observe: () => null, @@ -1350,7 +1353,7 @@ describe('Outline Tab', () => { expect(screen.queryByRole('button', { name: 'resend the email' })).not.toBeInTheDocument(); }); - it('sends account activation email on clicking the resened email in account activation alert', async () => { + it('sends account activation email on clicking the re-send email in account activation alert', async () => { Cookies.set = jest.fn(); Cookies.get = jest.fn().mockImplementation(() => 'true'); Cookies.remove = jest.fn().mockImplementation(() => { Cookies.get = jest.fn(); }); diff --git a/src/course-home/outline-tab/widgets/CourseDates.jsx b/src/course-home/outline-tab/widgets/CourseDates.jsx index e4e0fdcb..c78dbef5 100644 --- a/src/course-home/outline-tab/widgets/CourseDates.jsx +++ b/src/course-home/outline-tab/widgets/CourseDates.jsx @@ -29,21 +29,23 @@ function CourseDates({ return (
      -

      {intl.formatMessage(messages.dates)}

      -
        - {courseDateBlocks.map((courseDateBlock) => ( - - ))} -
      - - {intl.formatMessage(messages.allDates)} - +
      +

      {intl.formatMessage(messages.dates)}

      +
        + {courseDateBlocks.map((courseDateBlock) => ( + + ))} +
      + + {intl.formatMessage(messages.allDates)} + +
      ); } diff --git a/src/course-home/outline-tab/widgets/CourseTools.jsx b/src/course-home/outline-tab/widgets/CourseTools.jsx index 876dece0..9ac09277 100644 --- a/src/course-home/outline-tab/widgets/CourseTools.jsx +++ b/src/course-home/outline-tab/widgets/CourseTools.jsx @@ -12,6 +12,7 @@ import { faNewspaper } from '@fortawesome/free-regular-svg-icons'; import messages from '../messages'; import { useModel } from '../../../generic/model-store'; +import LaunchCourseHomeTourButton from '../../../product-tours/newUserCourseHomeTour/LaunchCourseHomeTourButton'; function CourseTools({ courseId, intl }) { const { org } = useModel('courseHomeMeta', courseId); @@ -69,6 +70,9 @@ function CourseTools({ courseId, intl }) { ))} +
      ); diff --git a/src/course-home/outline-tab/widgets/WeeklyLearningGoalCard.jsx b/src/course-home/outline-tab/widgets/WeeklyLearningGoalCard.jsx index 0b5e19ba..2d1adfdf 100644 --- a/src/course-home/outline-tab/widgets/WeeklyLearningGoalCard.jsx +++ b/src/course-home/outline-tab/widgets/WeeklyLearningGoalCard.jsx @@ -67,6 +67,7 @@ function WeeklyLearningGoalCard({ return ( diff --git a/src/courseware/course/NotificationTray.scss b/src/courseware/course/NotificationTray.scss index b4ac9234..abddee9a 100644 --- a/src/courseware/course/NotificationTray.scss +++ b/src/courseware/course/NotificationTray.scss @@ -16,6 +16,7 @@ margin: 0; border: none; border-radius: 0; + z-index: 1100; } } diff --git a/src/courseware/course/sequence/sequence-navigation/SequenceNavigation.jsx b/src/courseware/course/sequence/sequence-navigation/SequenceNavigation.jsx index c1beb1bf..337e921e 100644 --- a/src/courseware/course/sequence/sequence-navigation/SequenceNavigation.jsx +++ b/src/courseware/course/sequence/sequence-navigation/SequenceNavigation.jsx @@ -84,7 +84,7 @@ function SequenceNavigation({ const prevArrow = isRtl(getLocale()) ? ChevronRight : ChevronLeft; return sequenceStatus === LOADED && ( -