From 0adb558d24ccb65d2ccbf69b35b7e861506927e5 Mon Sep 17 00:00:00 2001 From: Zachary Trabookis Date: Tue, 8 Mar 2022 15:03:27 -0500 Subject: [PATCH 01/17] feat(badges): Initial step setting up the BadgeProgressTab to adhere to the new frontend structure. Todo: This just renders the start of BadgeProgressTab at this URL, however, we still need to render out the progress details. Also the Jest tests for `Test fetchBadgesProgressTab` still need to be worked out. The `Should fetch, normalize, and save metadata` is not working due to the `frontend-app-learning/src/course-home/data/__factories__/badgeProgressTabData.factory.js` Factory needing to be setup. http://localhost:2000/course/{courseId}/badges/progress --- .../badges-tab/BadgeProgressTab.jsx | 155 ++++++++ .../badges-tab/BadgeProgressTab.test.jsx | 370 ++++++++++++++++++ .../__factories__/badgeProgress.factory.js | 120 ++++++ .../badgeProgressTabData.factory.js | 61 +++ src/course-home/data/__factories__/index.js | 2 + .../data/__snapshots__/redux.test.js.snap | 15 + src/course-home/data/api.js | 28 +- src/course-home/data/index.js | 1 + src/course-home/data/redux.test.js | 33 +- src/course-home/data/thunks.js | 8 + src/index.jsx | 8 +- .../courseMetadataBase.factory.js | 10 + 12 files changed, 808 insertions(+), 3 deletions(-) create mode 100644 src/course-home/badges-tab/BadgeProgressTab.jsx create mode 100644 src/course-home/badges-tab/BadgeProgressTab.test.jsx create mode 100644 src/course-home/data/__factories__/badgeProgress.factory.js create mode 100644 src/course-home/data/__factories__/badgeProgressTabData.factory.js diff --git a/src/course-home/badges-tab/BadgeProgressTab.jsx b/src/course-home/badges-tab/BadgeProgressTab.jsx new file mode 100644 index 0000000000..78bd71d902 --- /dev/null +++ b/src/course-home/badges-tab/BadgeProgressTab.jsx @@ -0,0 +1,155 @@ +import React, { useEffect, useState } from 'react'; +// import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { StatusAlert } from '@edx/paragon'; +import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; +// import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons'; + +import { useModel } from '../../generic/model-store'; +import { debug } from 'util'; + +// import { BadgeTabsNavigation } from './badge-header'; +// import { BadgeProgressBanner, BadgeProgressCard, BadgeProgressCourseList } from './badge-progress'; + +// import { headingMapper } from './utils'; + + +function BadgeProgressTab({ intl }) { + // const activeTabSlug = 'progress'; + + const { + courseId, + } = useSelector(state => state.courseHome); + + // username + const { + administrator, + username, + roles + } = getAuthenticatedUser(); + + const hasInstructorStaffRights = () => administrator; + + const [progress, setProgress] = useState([]); + // const badgeProgressState = useModel('badges-progress', courseId); + + const { + id, + ...badgeProgressState + } = useModel('badges-progress', courseId); + + const hasBadgeProgress = () => progress && progress.length > 0; + + const checkBadgeProgressExists = ( progress ) => { + let _badgeProgressState = []; + Object.values(progress).forEach(student => { + if (typeof student === 'object' && Array.isArray(student.progress)) { + if (student.progress.length > 0) { + _badgeProgressState.push(student); + } + } + }); + + return _badgeProgressState; + } + + useEffect(() => { + let _badgeProgressState = checkBadgeProgressExists(badgeProgressState); + if ( _badgeProgressState.length ) { + setProgress(_badgeProgressState); + } else { + console.log("BadgeProgressTab: Could not find any course badge progress."); + } + + // if ( badgeProgressState ) { + // let checkBadgeProgressExists = badgeProgressState.some(x => x.progress.length > 0); + // debugger; + // if ( checkBadgeProgressExists ) { + // debugger; + // setProgress(badgeProgressState); + // } + // } + + // setProgress(badgeProgressState); + + // let classBadgeProgressExists = 0; + // let badgeProgressStateUpdated = []; + + // if (hasInstructorStaffRights()) { + // // Loop through all student's and build new state by removing added course_id from fetchTabData. + // debugger; + // Object.values(badgeProgressState).forEach(student => { + // if (typeof student === 'object' && Array.isArray(student.progress)) { + // badgeProgressStateUpdated.push(student); + // classBadgeProgressExists += student.progress.length; + // debugger; + // } + // }); + // debugger; + // if (classBadgeProgressExists) { + // debugger; + // setProgress(badgeProgressStateUpdated); + // } + // } else { + // // Loop through all student's and build new state by removing added course_id from fetchTabData. + // Object.values(badgeProgressState).forEach(value => { + // if (typeof value === 'object' && Array.isArray(value.progress)) { + // badgeProgressStateUpdated.push(value); + // } + // }); + // setProgress(badgeProgressStateUpdated); + // } + }, []); //, courseId, administrator]); + + const renderBadgeProgress = () => { + const defaultAssignmentFilter = 'All'; + + const userRoleNames = roles ? roles.map(role => role.split(':')[0]) : []; + + return ( +
+

+ the user is {username} + {administrator + &&
the user is admin
} + {roles &&
{userRoleNames}
} +

+
+ ); + }; + + const renderNoBadgeProgress = () => ( + + + There is no course badge progress to show. + + )} + alertType="info" + dismissible={false} + open + /> + ); + + return ( + <> + {hasBadgeProgress() && ( + renderBadgeProgress() + )} + {!hasBadgeProgress() && ( + renderNoBadgeProgress() + )} + + ); +} + +BadgeProgressTab.propTypes = { + intl: intlShape.isRequired, +}; + +export default injectIntl(BadgeProgressTab); +// export default BadgeProgressTab; diff --git a/src/course-home/badges-tab/BadgeProgressTab.test.jsx b/src/course-home/badges-tab/BadgeProgressTab.test.jsx new file mode 100644 index 0000000000..c09b64805d --- /dev/null +++ b/src/course-home/badges-tab/BadgeProgressTab.test.jsx @@ -0,0 +1,370 @@ +import React from 'react'; +import { Route } from 'react-router'; +import MockAdapter from 'axios-mock-adapter'; +import { Factory } from 'rosie'; +import { getConfig, history } from '@edx/frontend-platform'; +import { sendTrackEvent } from '@edx/frontend-platform/analytics'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { AppProvider } from '@edx/frontend-platform/react'; +import { waitForElementToBeRemoved } from '@testing-library/dom'; +import { render, screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import BadgeProgressTab from './BadgeProgressTab'; +import { fetchBadgeProgressTab } from '../data'; +import { fireEvent, initializeMockApp, waitFor } from '../../setupTest'; +import initializeStore from '../../store'; +import { TabContainer } from '../../tab-page'; +import { appendBrowserTimezoneToUrl } from '../../utils'; +import { UserMessagesProvider } from '../../generic/user-messages'; + +initializeMockApp(); +jest.mock('@edx/frontend-platform/analytics'); + +describe('BadgeProgressTab', () => { + let axiosMock; + let store; + let component; + + beforeEach(() => { + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + store = initializeStore(); + component = ( + + + + + + + + + + ); + }); + + const badgeProgressTabData = Factory.build('badgeProgressTabData'); + let courseMetadata = Factory.build('courseHomeMetadata', { user_timezone: 'America/New_York' }); + const { id: courseId } = courseMetadata; + + const badgeProgressUrl = `${getConfig().LMS_BASE_URL}/api/badges/v1/progress/${courseId}`; + let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/course_home/course_metadata/${courseId}`; + courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl); + + function setMetadata(attributes, options) { + courseMetadata = Factory.build('courseHomeMetadata', { id: courseId, ...attributes }, options); + axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata); + } + + // // The dates tab is largely repetitive non-interactive static data. Thus it's a little tough to follow + // // testing-library's advice around testing the way your user uses the site (i.e. can't find form elements by label or + // // anything). Instead, we find elements by printed date (which is what the user sees) and data-testid. Which is + // // better than assuming anything about how the surrounding elements are organized by div and span or whatever. And + // // better than adding non-style class names. + // // Hence the following getDay query helper. + // async function getDay(date) { + // const dateNode = await screen.findByText(date); + // let parent = dateNode.parentElement; + // while (parent) { + // if (parent.dataset && parent.dataset.testid === 'dates-day') { + // return { + // day: parent, + // header: within(parent).getByTestId('dates-header'), + // items: within(parent).queryAllByTestId('dates-item'), + // }; + // } + // parent = parent.parentElement; + // } + // throw new Error('Did not find day container'); + // } + + describe('when receiving a full set of dates data', () => { + beforeEach(() => { + axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata); + axiosMock.onGet(badgeProgressUrl).reply(200, badgeProgressTabData); + history.push(`/course/${courseId}/badges/progress`); // so tab can pull course id from url + + render(component); + }); + + it('handles unreleased & complete', async () => { + // const { header } = await getDay('Sun, May 3, 2020'); + // const badges = within(header).getAllByTestId('dates-badge'); + // expect(badges).toHaveLength(2); + // expect(badges[0]).toHaveTextContent('Completed'); + // expect(badges[1]).toHaveTextContent('Not yet released'); + }); + }); + + // describe('when receiving a full set of dates data', () => { + // beforeEach(() => { + // 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 + + // render(component); + // }); + + // it('handles unreleased & complete', async () => { + // const { header } = await getDay('Sun, May 3, 2020'); + // const badges = within(header).getAllByTestId('dates-badge'); + // expect(badges).toHaveLength(2); + // expect(badges[0]).toHaveTextContent('Completed'); + // expect(badges[1]).toHaveTextContent('Not yet released'); + // }); + + // it('handles unreleased & past due', async () => { + // const { header } = await getDay('Mon, May 4, 2020'); + // const badges = within(header).getAllByTestId('dates-badge'); + // expect(badges).toHaveLength(2); + // expect(badges[0]).toHaveTextContent('Past due'); + // expect(badges[1]).toHaveTextContent('Not yet released'); + // }); + + // it('handles verified only', async () => { + // const { day } = await getDay('Sun, Aug 18, 2030'); + // const badge = within(day).getByTestId('dates-badge'); + // expect(badge).toHaveTextContent('Verified only'); + // }); + + // it('verified only has no link', async () => { + // const { day } = await getDay('Sun, Aug 18, 2030'); + // expect(within(day).queryByRole('link')).toBeNull(); + // }); + + // it('same status items have header badge', async () => { + // const { day, header } = await getDay('Tue, May 26, 2020'); + // const badge = within(header).getByTestId('dates-badge'); + // expect(badge).toHaveTextContent('Past due'); // one header badge + // expect(within(day).getAllByTestId('dates-badge')).toHaveLength(1); // no other badges + // }); + + // it('different status items have individual badges', async () => { + // const { header, items } = await getDay('Thu, May 28, 2020'); + // const headerBadges = within(header).queryAllByTestId('dates-badge'); + // expect(headerBadges).toHaveLength(0); // no header badges + // expect(items).toHaveLength(2); + // expect(within(items[0]).getByTestId('dates-badge')).toHaveTextContent('Completed'); + // expect(within(items[1]).getByTestId('dates-badge')).toHaveTextContent('Past due'); + // }); + + // it('shows extra info', async () => { + // const { items } = await getDay('Sat, Aug 17, 2030'); + // expect(items).toHaveLength(3); + + // const tipIcon = within(items[2]).getByTestId('dates-extra-info'); + // const tipText = "ORA Dates are set by the instructor, and can't be changed"; + + // expect(screen.queryByText(tipText)).toBeNull(); // tooltip does not start in DOM + // userEvent.hover(tipIcon); + // const tooltip = screen.getByText(tipText); // now it's there + // userEvent.unhover(tipIcon); + // waitForElementToBeRemoved(tooltip); // and it's gone again + // }); + // }); + + // describe('Suggested schedule messaging', () => { + // beforeEach(() => { + // setMetadata({ is_self_paced: true, is_enrolled: true }); + // history.push(`/course/${courseId}/dates`); + // }); + + // it('renders SuggestedScheduleHeader', async () => { + // datesTabData.datesBannerInfo = { + // contentTypeGatingEnabled: false, + // missedDeadlines: false, + // missedGatedContent: false, + // }; + + // axiosMock.onGet(datesUrl).reply(200, datesTabData); + // render(component); + + // await waitFor(() => expect(screen.getByText('We’ve built a suggested schedule to help you stay on track. But don’t worry—it’s flexible so you can learn at your own pace.')).toBeInTheDocument()); + // }); + + // it('renders UpgradeToCompleteAlert', async () => { + // datesTabData.datesBannerInfo = { + // contentTypeGatingEnabled: true, + // missedDeadlines: false, + // missedGatedContent: false, + // verifiedUpgradeLink: 'http://localhost:18130/basket/add/?sku=8CF08E5', + // }; + + // axiosMock.onGet(datesUrl).reply(200, datesTabData); + // render(component); + + // await waitFor(() => expect(screen.getByText('You are auditing this course, which means that you are unable to participate in graded assignments. To complete graded assignments as part of this course, you can upgrade today.')).toBeInTheDocument()); + // expect(screen.getByRole('button', { name: 'Upgrade now' })).toBeInTheDocument(); + // }); + + // it('renders UpgradeToShiftDatesAlert', async () => { + // datesTabData.datesBannerInfo = { + // contentTypeGatingEnabled: true, + // missedDeadlines: true, + // missedGatedContent: true, + // verifiedUpgradeLink: 'http://localhost:18130/basket/add/?sku=8CF08E5', + // }; + + // axiosMock.onGet(datesUrl).reply(200, datesTabData); + // render(component); + + // await waitFor(() => expect(screen.getByText('It looks like you missed some important deadlines based on our suggested schedule.')).toBeInTheDocument()); + // expect(screen.getByText('To keep yourself on track, you can update this schedule and shift the past due assignments into the future. Don’t worry—you won’t lose any of the progress you’ve made when you shift your due dates.')).toBeInTheDocument(); + // expect(screen.getByRole('button', { name: 'Upgrade to shift due dates' })).toBeInTheDocument(); + // }); + + // it('renders ShiftDatesAlert', async () => { + // datesTabData.datesBannerInfo = { + // contentTypeGatingEnabled: true, + // missedDeadlines: true, + // missedGatedContent: false, + // verifiedUpgradeLink: 'http://localhost:18130/basket/add/?sku=8CF08E5', + // }; + + // axiosMock.onGet(datesUrl).reply(200, datesTabData); + // render(component); + + // await waitFor(() => expect(screen.getByText('It looks like you missed some important deadlines based on our suggested schedule.')).toBeInTheDocument()); + // expect(screen.getByText('To keep yourself on track, you can update this schedule and shift the past due assignments into the future. Don’t worry—you won’t lose any of the progress you’ve made when you shift your due dates.')).toBeInTheDocument(); + // expect(screen.getByRole('button', { name: 'Shift due dates' })).toBeInTheDocument(); + // }); + + // it('handles shift due dates click', async () => { + // datesTabData.datesBannerInfo = { + // contentTypeGatingEnabled: true, + // missedDeadlines: true, + // missedGatedContent: false, + // verifiedUpgradeLink: 'http://localhost:18130/basket/add/?sku=8CF08E5', + // }; + + // axiosMock.onGet(datesUrl).reply(200, datesTabData); + // render(component); + + // // confirm "Shift due dates" button has rendered + // await waitFor(() => expect(screen.getByRole('button', { name: 'Shift due dates' })).toBeInTheDocument()); + + // // update response to reflect shifted dates + // datesTabData.datesBannerInfo = { + // contentTypeGatingEnabled: true, + // missedDeadlines: false, + // missedGatedContent: false, + // verifiedUpgradeLink: 'http://localhost:18130/basket/add/?sku=8CF08E5', + // }; + + // const resetDeadlinesData = { + // header: "You've successfully shifted your dates!", + // }; + // axiosMock.onPost(`${getConfig().LMS_BASE_URL}/api/course_experience/v1/reset_course_deadlines`).reply(200, resetDeadlinesData); + + // // click "Shift due dates" + // fireEvent.click(screen.getByRole('button', { name: 'Shift due dates' })); + + // // wait for page to reload & Toast to render + // await waitFor(() => expect(screen.getByText("You've successfully shifted your dates!")).toBeInTheDocument()); + // // confirm "Shift due dates" button has not rendered + // expect(screen.queryByRole('button', { name: 'Shift due dates' })).not.toBeInTheDocument(); + // }); + + // it('sends analytics event onClick of upgrade button in UpgradeToCompleteAlert', async () => { + // sendTrackEvent.mockClear(); + // datesTabData.datesBannerInfo = { + // contentTypeGatingEnabled: true, + // missedDeadlines: false, + // missedGatedContent: false, + // verifiedUpgradeLink: 'http://localhost:18130/basket/add/?sku=8CF08E5', + // }; + + // axiosMock.onGet(datesUrl).reply(200, datesTabData); + // render(component); + + // const upgradeButton = await waitFor(() => screen.getByRole('button', { name: 'Upgrade now' })); + // fireEvent.click(upgradeButton); + + // expect(sendTrackEvent).toHaveBeenCalledTimes(1); + // expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.ecommerce.upsell_links_clicked', { + // org_key: 'edX', + // courserun_key: courseId, + // linkCategory: 'personalized_learner_schedules', + // linkName: 'dates_upgrade', + // linkType: 'button', + // pageName: 'dates_tab', + // }); + // }); + + // it('sends analytics event onClick of upgrade button in UpgradeToShiftDatesAlert', async () => { + // sendTrackEvent.mockClear(); + // datesTabData.datesBannerInfo = { + // contentTypeGatingEnabled: true, + // missedDeadlines: true, + // missedGatedContent: true, + // verifiedUpgradeLink: 'http://localhost:18130/basket/add/?sku=8CF08E5', + // }; + + // axiosMock.onGet(datesUrl).reply(200, datesTabData); + // render(component); + + // const upgradeButton = await waitFor(() => screen.getByRole('button', { name: 'Upgrade to shift due dates' })); + // fireEvent.click(upgradeButton); + + // expect(sendTrackEvent).toHaveBeenCalledTimes(1); + // expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.ecommerce.upsell_links_clicked', { + // org_key: 'edX', + // courserun_key: courseId, + // linkCategory: 'personalized_learner_schedules', + // linkName: 'dates_upgrade', + // linkType: 'button', + // pageName: 'dates_tab', + // }); + // }); + // }); + + // describe('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 + // }); + + // it('redirects to the home page when unauthenticated', async () => { + // await renderDenied('authentication_required'); + // expect(global.location.href).toEqual(`http://localhost/redirect/course-home/${courseMetadata.id}`); + // }); + + // it('redirects to the home page when unenrolled', async () => { + // await renderDenied('enrollment_required'); + // expect(global.location.href).toEqual(`http://localhost/redirect/course-home/${courseMetadata.id}`); + // }); + // }); +}); diff --git a/src/course-home/data/__factories__/badgeProgress.factory.js b/src/course-home/data/__factories__/badgeProgress.factory.js new file mode 100644 index 0000000000..e3cb5b9d49 --- /dev/null +++ b/src/course-home/data/__factories__/badgeProgress.factory.js @@ -0,0 +1,120 @@ +import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies + +Factory.define('badge-progress') + .attrs({ + course_id: 'course-v1:edX+DemoX+Demo_Course', + block_id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@dc1e160e5dc348a48a98fa0f4a6e8675', + block_display_name: 'Example Week 1: Getting Started', + event_type: 'chapter_complete', + }); + + // Default to one badge_class. If badge_class was given, fill in + // whatever attributes might be missing. + // .attr('badge_class', ['badge_class'], (badge_class) => { + // if (!badge_class) { + // badge_class = [{}]; + // } + // return badge_class.map((data) => Factory.attributes('badge_class', data)); + // }) + + // Default to one assertion. If assertion was given, fill in + // whatever attributes might be missing. + // .attr('assertion', ['assertion'], (assertion) => { + // if (!assertion) { + // assertion = [{}]; + // } + // return assertion.map((data) => Factory.attributes('assertion', data)); + // }) + +// { +// course_id: "course-v1:edX+DemoX+Demo_Course", +// block_id: "block-v1:edX+DemoX+Demo_Course+type@chapter+block@dc1e160e5dc348a48a98fa0f4a6e8675", +// block_display_name: "Example Week 1: Getting Started", +// event_type: "chapter_complete", +// badge_class: { +// slug: "special_award", +// issuing_component: "openedx__course", +// display_name: "Very Special Award", +// course_id: "course-v1:edX+DemoX+Demo_Course", +// description: "Awarded for people who did something incredibly special", +// criteria: "Do something incredibly special.", +// image: "http://example.com/media/badge_classes/badges/special_xdpqpBv_9FYOZwN.png" +// }, +// assertion: { +// issuedOn: "2019-04-20T02:43:06.566955Z", +// expires: "2019-04-30T00:00:00.000000Z", +// revoked: false, +// image_url: "http://badges.example.com/media/issued/cd75b69fc1c979fcc1697c8403da2bdf.png", +// assertion_url: "http://badges.example.com/public/assertions/07020647-e772-44dd-98b7-d13d34335ca6", +// recipient: { +// plaintextIdentity: "john.doe@example.com" +// }, +// issuer: { +// entityType: "Issuer", +// entityId: "npqlh0acRFG5pKKbnb4SRg", +// openBadgeId: "https://api.badgr.io/public/issuers/npqlh0acRFG5pKKbnb4SRg", +// name: "EducateWorkforce", +// image: "https://media.us.badgr.io/uploads/issuers/issuer_logo_77bb4498-838b-45b7-8722-22878fedb5e8.svg", +// email: "cucwd.developer@gmail.com", +// description: "An online learning solution offered with partnering 2-year colleges to help integrate web and digital solutions into their existing courses. The platform was designed by multiple instructional design, usability, and computing experts to include research-based learning features.", +// url: "https://ew-localhost.com" +// } +// } +// }, + +Factory.define('badge_class') + .attrs({ + slug: "special_award", + issuing_component: "openedx__course", + display_name: "Very Special Award", + course_id: "course-v1:edX+DemoX+Demo_Course", + description: "Awarded for people who did something incredibly special", + criteria: "Do something incredibly special.", + image: "http://example.com/media/badge_classes/badges/special_xdpqpBv_9FYOZwN.png" + }); + +Factory.define('assertion') + .attrs({ + issuedOn: "2019-04-20T02:43:06.566955Z", + expires: "2019-04-30T00:00:00.000000Z", + revoked: false, + image_url: "http://badges.example.com/media/issued/cd75b69fc1c979fcc1697c8403da2bdf.png", + assertion_url: "http://badges.example.com/public/assertions/07020647-e772-44dd-98b7-d13d34335ca6", + }) + + // Default to one recipient. If recipient was given, fill in + // whatever attributes might be missing. + .attr('recipient', ['recipient'], (recipient) => { + if (!recipient) { + recipient = [{}]; + } + return recipient.map((data) => Factory.attributes('recipient', data)); + }) + + // Default to one issuer. If issuer was given, fill in + // whatever attributes might be missing. + .attr('issuer', ['issuer'], (issuer) => { + if (!issuer) { + issuer = [{}]; + } + return issuer.map((data) => Factory.attributes('issuer', data)); + }) + + +Factory.define('recipient') + .attrs({ + plaintextIdentity: "john.doe@example.com" + }) + +Factory.define('issuer') + .attrs({ + entityType: "Issuer", + entityId: "npqlh0acRFG5pKKbnb4SRg", + openBadgeId: "https://api.badgr.io/public/issuers/npqlh0acRFG5pKKbnb4SRg", + name: "EducateWorkforce", + image: "https://media.us.badgr.io/uploads/issuers/issuer_logo_77bb4498-838b-45b7-8722-22878fedb5e8.svg", + email: "cucwd.developer@gmail.com", + description: "An online learning solution offered with partnering 2-year colleges to help integrate web and digital solutions into their existing courses. The platform was designed by multiple instructional design, usability, and computing experts to include research-based learning features.", + url: "https://ew-localhost.com" + }) + diff --git a/src/course-home/data/__factories__/badgeProgressTabData.factory.js b/src/course-home/data/__factories__/badgeProgressTabData.factory.js new file mode 100644 index 0000000000..c57a5ad0b3 --- /dev/null +++ b/src/course-home/data/__factories__/badgeProgressTabData.factory.js @@ -0,0 +1,61 @@ +import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies + +import './badgeProgress.factory'; + +// Sample data helpful when developing & testing, to see a variety of configurations. +// This set of data is not realistic (mix of having access and not), but it +// is intended to demonstrate many UI results. +Factory.define('badgeProgressTabData') + .sequence('id', (i) => `course-v1:edX+DemoX+Demo_Course_${i}`) + .sequence('user_id') + .attrs({ + user_name: 'TestUser', + name: 'Test Username', + email: 'test@edx.org', + }) + .attrs( + 'progress', ['id'], (id) => { + const progress = [ + Factory.build( + 'badge-progress', + { + course_id: 'course-v1:edX+DemoX+Demo_Course', + block_id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@dc1e160e5dc348a48a98fa0f4a6e8675', + block_display_name: 'Example Week 1: Getting Started', + event_type: 'chapter_complete', + badge_class: { + slug: 'special_award', + issuing_component: 'openedx__course', + display_name: 'Very Special Award', + course_id: 'course-v1:edX+DemoX+Demo_Course', + description: 'Awarded for people who did something incredibly special', + criteria: 'Do something incredibly special.', + image: 'http://example.com/media/badge_classes/badges/special_xdpqpBv_9FYOZwN.png' + }, + assertion: { + issuedOn: '2019-04-20T02:43:06.566955Z', + expires: '2019-04-30T00:00:00.000000Z', + revoked: false, + image_url: 'http://badges.example.com/media/issued/cd75b69fc1c979fcc1697c8403da2bdf.png', + assertion_url: 'http://badges.example.com/public/assertions/07020647-e772-44dd-98b7-d13d34335ca6', + recipient: { + plaintextIdentity: 'john.doe@example.com' + }, + issuer: { + entityType: 'Issuer', + entityId: 'npqlh0acRFG5pKKbnb4SRg', + openBadgeId: 'https://api.badgr.io/public/issuers/npqlh0acRFG5pKKbnb4SRg', + name: 'EducateWorkforce', + image: 'https://media.us.badgr.io/uploads/issuers/issuer_logo_77bb4498-838b-45b7-8722-22878fedb5e8.svg', + email: 'cucwd.developer@gmail.com', + description: 'An online learning solution offered with partnering 2-year colleges to help integrate web and digital solutions into their existing courses. The platform was designed by multiple instructional design, usability, and computing experts to include research-based learning features.', + url: 'https://ew-localhost.com' + } + } + } + ), + ]; + + return progress; + }, + ); diff --git a/src/course-home/data/__factories__/index.js b/src/course-home/data/__factories__/index.js index e421d00c7c..ae35f0aa16 100644 --- a/src/course-home/data/__factories__/index.js +++ b/src/course-home/data/__factories__/index.js @@ -1,4 +1,6 @@ import './courseHomeMetadata.factory'; +import './badgeProgress.factory'; +import './badgeProgressTabData.factory'; import './datesTabData.factory'; import './outlineTabData.factory'; import './progressTabData.factory'; diff --git a/src/course-home/data/__snapshots__/redux.test.js.snap b/src/course-home/data/__snapshots__/redux.test.js.snap index 19a5bd8bd4..0e5649f876 100644 --- a/src/course-home/data/__snapshots__/redux.test.js.snap +++ b/src/course-home/data/__snapshots__/redux.test.js.snap @@ -68,6 +68,11 @@ Object { "title": "Dates", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/dates", }, + Object { + "slug": "badges-progress", + "title": "Badges", + "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/badges-progress", + }, ], "title": "Demonstration Course", "userTimezone": "UTC", @@ -372,6 +377,11 @@ Object { "title": "Dates", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/dates", }, + Object { + "slug": "badges-progress", + "title": "Badges", + "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/badges-progress", + }, ], "title": "Demonstration Course", "userTimezone": "UTC", @@ -552,6 +562,11 @@ Object { "title": "Dates", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/dates", }, + Object { + "slug": "badges-progress", + "title": "Badges", + "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/badges-progress", + }, ], "title": "Demonstration Course", "userTimezone": "UTC", diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js index 6680b42132..672b7ca4f8 100644 --- a/src/course-home/data/api.js +++ b/src/course-home/data/api.js @@ -1,5 +1,5 @@ import { camelCaseObject, getConfig } from '@edx/frontend-platform'; -import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { logInfo } from '@edx/frontend-platform/logging'; import { appendBrowserTimezoneToUrl } from '../../utils'; @@ -186,6 +186,32 @@ export async function getCourseHomeCourseMetadata(courseId) { return normalizeCourseHomeCourseMetadata(data); } +export async function getBadgeProgressTabData(courseId) { + const { administrator, username } = getAuthenticatedUser(); + const getProgressApiEndPoint = () => ( + administrator + ? `${getConfig().LMS_BASE_URL}/api/badges/v1/progress/${courseId}` + : `${getConfig().LMS_BASE_URL}/api/badges/v1/progress/${courseId}/user/${username}` + ); + + try { + const { data } = await getAuthenticatedHttpClient().get(getProgressApiEndPoint()); + return camelCaseObject(data); + } catch (error) { + const { httpErrorStatus } = error && error.customAttributes; + if (httpErrorStatus === 404) { + global.location.replace(`${getConfig().LMS_BASE_URL}/courses/${courseId}/badges/progress`); + return {}; + } + if (httpErrorStatus === 401) { + // The backend sends this for unenrolled and unauthenticated learners, but we handle those cases by examining + // courseAccess in the metadata call, so just ignore this status for now. + return {}; + } + throw error; + } +} + // For debugging purposes, you might like to see a fully loaded dates tab. // Just uncomment the next few lines and the immediate 'return' in the function below // import { Factory } from 'rosie'; diff --git a/src/course-home/data/index.js b/src/course-home/data/index.js index c5f79fa6f6..a19babbc92 100644 --- a/src/course-home/data/index.js +++ b/src/course-home/data/index.js @@ -1,4 +1,5 @@ export { + fetchBadgeProgressTab, fetchDatesTab, fetchOutlineTab, fetchProgressTab, diff --git a/src/course-home/data/redux.test.js b/src/course-home/data/redux.test.js index 5ea0b858ae..14bc8b15d5 100644 --- a/src/course-home/data/redux.test.js +++ b/src/course-home/data/redux.test.js @@ -10,6 +10,7 @@ import { appendBrowserTimezoneToUrl, executeThunk } from '../../utils'; import { initializeMockApp } from '../../setupTest'; import initializeStore from '../../store'; +import { debug } from 'util'; const { loggingService } = initializeMockApp(); @@ -30,9 +31,39 @@ describe('Data layer integration tests', () => { store = initializeStore(); }); + describe('Test fetchBadgeProgressTab', () => { + const badgeProgressBaseUrl = `${getConfig().LMS_BASE_URL}/api/badges/v1/progress`; + it('Should fail to fetch if error occurs', async () => { + axiosMock.onGet(courseMetadataUrl).networkError(); + axiosMock.onGet(`${badgeProgressBaseUrl}/${courseId}`).networkError(); + + await executeThunk(thunks.fetchBadgeProgressTab(courseId), store.dispatch); + + expect(loggingService.logError).toHaveBeenCalled(); + expect(store.getState().courseHome.courseStatus).toEqual('failed'); + }); + + // it('Should fetch, normalize, and save metadata', async () => { + // const badgeProgressTabData = Factory.build('badgeProgressTabData'); + + // console.warn(badgeProgressTabData); + + // const badgeProgressUrl = `${badgeProgressBaseUrl}/${courseId}`; + + // axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeMetadata); + // axiosMock.onGet(badgeProgressUrl).reply(200, badgeProgressTabData); + + // await executeThunk(thunks.fetchBadgeProgressTab(courseId), store.dispatch); + + // const state = store.getState(); + // expect(state.courseHome.courseStatus).toEqual('loaded'); + // expect(state).toMatchSnapshot(); + // }); + }); + describe('Test fetchDatesTab', () => { const datesBaseUrl = `${getConfig().LMS_BASE_URL}/api/course_home/dates`; - +`` it('Should fail to fetch if error occurs', async () => { axiosMock.onGet(courseMetadataUrl).networkError(); axiosMock.onGet(`${datesBaseUrl}/${courseId}`).networkError(); diff --git a/src/course-home/data/thunks.js b/src/course-home/data/thunks.js index 99fdc6fdf8..2fd9bbd37b 100644 --- a/src/course-home/data/thunks.js +++ b/src/course-home/data/thunks.js @@ -3,6 +3,7 @@ import { camelCaseObject } from '@edx/frontend-platform'; import { executePostFromPostEvent, getCourseHomeCourseMetadata, + getBadgeProgressTabData, getDatesTabData, getOutlineTabData, getProgressTabData, @@ -23,6 +24,7 @@ import { fetchTabSuccess, setCallToActionToast, } from './slice'; +import { debug } from 'util'; const eventTypes = { POST_EVENT: 'post_event', @@ -71,9 +73,15 @@ export function fetchTab(courseId, tab, getTabData, targetUserId) { dispatch(fetchTabFailure({ courseId })); } }); + + }; } +export function fetchBadgeProgressTab(courseId) { + return fetchTab(courseId, 'badges-progress', getBadgeProgressTabData); +} + export function fetchDatesTab(courseId) { return fetchTab(courseId, 'dates', getDatesTabData); } diff --git a/src/index.jsx b/src/index.jsx index fd6d8c3567..7869da0c54 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -22,10 +22,11 @@ import CoursewareContainer from './courseware'; import CoursewareRedirectLandingPage from './courseware/CoursewareRedirectLandingPage'; import DatesTab from './course-home/dates-tab'; import GoalUnsubscribe from './course-home/goal-unsubscribe'; +import BadgeProgressTab from './course-home/badges-tab/BadgeProgressTab'; import ProgressTab from './course-home/progress-tab/ProgressTab'; import { TabContainer } from './tab-page'; -import { fetchDatesTab, fetchOutlineTab, fetchProgressTab } from './course-home/data'; +import { fetchDatesTab, fetchOutlineTab, fetchProgressTab, fetchBadgeProgressTab } from './course-home/data'; import { fetchCourse } from './courseware/data'; import initializeStore from './store'; import NoticesProvider from './generic/notices'; @@ -43,6 +44,11 @@ subscribe(APP_READY, () => { + + + + + diff --git a/src/shared/data/__factories__/courseMetadataBase.factory.js b/src/shared/data/__factories__/courseMetadataBase.factory.js index 9c822cfb88..1b0b653542 100644 --- a/src/shared/data/__factories__/courseMetadataBase.factory.js +++ b/src/shared/data/__factories__/courseMetadataBase.factory.js @@ -81,6 +81,16 @@ export default new Factory() }, { courseId: id, host, path: 'dates' }, ), + Factory.build( + 'tab', + { + title: 'Badges', + priority: 6, + slug: 'badges-progress', + type: 'badges-progress', + }, + { courseId: id, host, path: 'badges-progress' }, + ), ]; return tabs; From 6cc880aa90e6b3b1c7d50c2e7f0be0b6a4bf69ba Mon Sep 17 00:00:00 2001 From: Zachary Trabookis Date: Wed, 9 Mar 2022 12:40:50 -0500 Subject: [PATCH 02/17] feat(badges): Create Badge navigation for Progress and Leaderboard functionality. --- .../badges-tab/BadgeLeaderboardTab.jsx | 49 ++ .../badges-tab/BadgeProgressTab.jsx | 33 +- .../badge-header/BadgeTabsNavigation.jsx | 95 +++ .../badge-header/assets/logo-badgr-light.svg | 22 + .../badges-tab/badge-header/index.js | 2 + .../badges-tab/badge-header/messages.js | 11 + src/course-home/badges-tab/index.js | 2 + .../courseHomeMetadata.factory.js | 32 +- .../__factories__/datesTabData.factory.js | 208 +----- src/course-home/data/__factories__/index.js | 4 - .../__factories__/outlineTabData.factory.js | 63 +- .../data/__snapshots__/redux.test.js.snap | 594 ++---------------- src/course-home/data/api.js | 18 + src/course-home/data/index.js | 1 + src/course-home/data/redux.test.js | 141 +---- src/course-home/data/thunks.js | 7 +- src/index.jsx | 11 +- src/index.scss | 59 +- 18 files changed, 410 insertions(+), 942 deletions(-) create mode 100644 src/course-home/badges-tab/BadgeLeaderboardTab.jsx create mode 100644 src/course-home/badges-tab/badge-header/BadgeTabsNavigation.jsx create mode 100644 src/course-home/badges-tab/badge-header/assets/logo-badgr-light.svg create mode 100644 src/course-home/badges-tab/badge-header/index.js create mode 100644 src/course-home/badges-tab/badge-header/messages.js create mode 100644 src/course-home/badges-tab/index.js diff --git a/src/course-home/badges-tab/BadgeLeaderboardTab.jsx b/src/course-home/badges-tab/BadgeLeaderboardTab.jsx new file mode 100644 index 0000000000..a490236b65 --- /dev/null +++ b/src/course-home/badges-tab/BadgeLeaderboardTab.jsx @@ -0,0 +1,49 @@ +import React from 'react'; +// import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; +import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; +// import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { useModel } from '../../generic/model-store'; + +import { BadgeTabsNavigation } from './badge-header'; + +// { +// intl +// } +function BadgeLeaderboardTab() { + const { + courseId, + } = useSelector(state => state.courseHome); + + const { administrator, username } = getAuthenticatedUser(); + + const { + enrollmentMode, + } = useModel('courses', courseId); + + const activeTabSlug = 'leaderboard'; + + return ( + <> +
+ +
+
+
+ the user is {username} and they are enrolled as an {enrollmentMode} learner + {administrator + &&

the user is admin

} +
+
+
+
+ + ); +} + +// BadgeLeaderboardTab.propTypes = { +// intl: intlShape.isRequired, +// }; + +// export default injectIntl(BadgeLeaderboardTab); +export default BadgeLeaderboardTab; diff --git a/src/course-home/badges-tab/BadgeProgressTab.jsx b/src/course-home/badges-tab/BadgeProgressTab.jsx index 78bd71d902..de2c946516 100644 --- a/src/course-home/badges-tab/BadgeProgressTab.jsx +++ b/src/course-home/badges-tab/BadgeProgressTab.jsx @@ -11,15 +11,15 @@ import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons'; import { useModel } from '../../generic/model-store'; import { debug } from 'util'; -// import { BadgeTabsNavigation } from './badge-header'; +import { BadgeTabsNavigation } from './badge-header'; // import { BadgeProgressBanner, BadgeProgressCard, BadgeProgressCourseList } from './badge-progress'; // import { headingMapper } from './utils'; function BadgeProgressTab({ intl }) { - // const activeTabSlug = 'progress'; - + const activeTabSlug = 'progress'; + const { courseId, } = useSelector(state => state.courseHome); @@ -39,7 +39,7 @@ function BadgeProgressTab({ intl }) { const { id, ...badgeProgressState - } = useModel('badges-progress', courseId); + } = useModel('badge-progress', courseId); const hasBadgeProgress = () => progress && progress.length > 0; @@ -58,6 +58,7 @@ function BadgeProgressTab({ intl }) { useEffect(() => { let _badgeProgressState = checkBadgeProgressExists(badgeProgressState); + if ( _badgeProgressState.length ) { setProgress(_badgeProgressState); } else { @@ -110,14 +111,21 @@ function BadgeProgressTab({ intl }) { const userRoleNames = roles ? roles.map(role => role.split(':')[0]) : []; return ( -
-

- the user is {username} - {administrator - &&
the user is admin
} - {roles &&
{userRoleNames}
} -

-
+ <> +
+ +
+
+
+ the user is {username} + {administrator + &&
the user is admin
} + {roles &&
{userRoleNames}
} +
+
+
+
+ ); }; @@ -153,3 +161,4 @@ BadgeProgressTab.propTypes = { export default injectIntl(BadgeProgressTab); // export default BadgeProgressTab; + diff --git a/src/course-home/badges-tab/badge-header/BadgeTabsNavigation.jsx b/src/course-home/badges-tab/badge-header/BadgeTabsNavigation.jsx new file mode 100644 index 0000000000..ad8fcd6d45 --- /dev/null +++ b/src/course-home/badges-tab/badge-header/BadgeTabsNavigation.jsx @@ -0,0 +1,95 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import classNames from 'classnames'; +// import { getConfig } from '@edx/frontend-platform'; + +import messages from './messages'; +import Tabs from '../../../generic/tabs/Tabs'; + +import logo from './assets/logo-badgr-light.svg'; + +function LinkedLogo({ + href, + src, + alt, + ...attributes +}) { + return ( + + {alt} + + ); +} + +LinkedLogo.propTypes = { + href: PropTypes.string.isRequired, + src: PropTypes.string.isRequired, + alt: PropTypes.string.isRequired, +}; + +function BadgeTabsNavigation({ + activeTabSlug, className, intl, +}) { + const tabs = [ + { + title: 'Progress', + priority: 1, + slug: 'progress', + type: 'progress', + url: './progress', + disabled: false, + }, + { + title: 'Leaderboard', + priority: 2, + slug: 'leaderboard', + type: 'leaderboard', + url: './leaderboard', + disabled: true, + }, + ]; + // flex-shrink-0 + return ( +
+
+ + + {tabs.map(({ + url, title, slug, disabled, + }) => ( + + {title} + + ))} + +
+
+ ); +} + +BadgeTabsNavigation.propTypes = { + activeTabSlug: PropTypes.string, + className: PropTypes.string, + intl: intlShape.isRequired, +}; + +BadgeTabsNavigation.defaultProps = { + activeTabSlug: undefined, + className: null, +}; + +export default injectIntl(BadgeTabsNavigation); diff --git a/src/course-home/badges-tab/badge-header/assets/logo-badgr-light.svg b/src/course-home/badges-tab/badge-header/assets/logo-badgr-light.svg new file mode 100644 index 0000000000..ec6300017a --- /dev/null +++ b/src/course-home/badges-tab/badge-header/assets/logo-badgr-light.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/course-home/badges-tab/badge-header/index.js b/src/course-home/badges-tab/badge-header/index.js new file mode 100644 index 0000000000..d3f32f24a1 --- /dev/null +++ b/src/course-home/badges-tab/badge-header/index.js @@ -0,0 +1,2 @@ +/* eslint-disable import/prefer-default-export */ +export { default as BadgeTabsNavigation } from './BadgeTabsNavigation'; diff --git a/src/course-home/badges-tab/badge-header/messages.js b/src/course-home/badges-tab/badge-header/messages.js new file mode 100644 index 0000000000..e2ae0bf3ba --- /dev/null +++ b/src/course-home/badges-tab/badge-header/messages.js @@ -0,0 +1,11 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + 'learn.navigation.badge.tabs.label': { + id: 'learn.navigation.badge.tabs.label', + defaultMessage: 'Badge Material', + description: 'The accessible label for badge tabs navigation', + }, +}); + +export default messages; diff --git a/src/course-home/badges-tab/index.js b/src/course-home/badges-tab/index.js new file mode 100644 index 0000000000..9e82bd06bc --- /dev/null +++ b/src/course-home/badges-tab/index.js @@ -0,0 +1,2 @@ +export { default as BadgeProgressTab } from './BadgeProgressTab'; +export { default as BadgeLeaderboardTab } from './BadgeLeaderboardTab'; diff --git a/src/course-home/data/__factories__/courseHomeMetadata.factory.js b/src/course-home/data/__factories__/courseHomeMetadata.factory.js index f6fe95b01b..89911633cc 100644 --- a/src/course-home/data/__factories__/courseHomeMetadata.factory.js +++ b/src/course-home/data/__factories__/courseHomeMetadata.factory.js @@ -1,23 +1,23 @@ import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies -import courseMetadataBase from '../../../shared/data/__factories__/courseMetadataBase.factory'; - Factory.define('courseHomeMetadata') - .extend(courseMetadataBase) + .sequence( + 'course_id', + (courseId) => `course-v1:edX+DemoX+Demo_Course_${courseId}`, + ) + .option('courseTabs', []) .option('host', 'http://localhost:18000') .attrs({ + is_staff: false, + number: 'DemoX', + org: 'edX', title: 'Demonstration Course', 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', - user_timezone: 'UTC', - }); + }) + .attr('tabs', ['courseTabs', 'host'], (courseTabs, host) => courseTabs.map( + tab => ({ + tab_id: tab.slug, + title: tab.title, + url: `${host}${tab.url}`, + }), + )); diff --git a/src/course-home/data/__factories__/datesTabData.factory.js b/src/course-home/data/__factories__/datesTabData.factory.js index 9ece9a2d07..ee3b7c352e 100644 --- a/src/course-home/data/__factories__/datesTabData.factory.js +++ b/src/course-home/data/__factories__/datesTabData.factory.js @@ -1,222 +1,26 @@ import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies -// Sample data helpful when developing & testing, to see a variety of configurations. -// This set of data is not realistic (mix of having access and not), but it -// is intended to demonstrate many UI results. Factory.define('datesTabData') .attrs({ dates_banner_info: { content_type_gating_enabled: false, missed_gated_content: false, missed_deadlines: false, - verified_upgrade_link: 'http://localhost:18130/basket/add/?sku=8CF08E5', }, course_date_blocks: [ { - date: '2020-05-01T17:59:41Z', + date: '2013-02-05T05:00:00Z', date_type: 'course-start-date', description: '', learner_has_access: true, link: '', title: 'Course Starts', - extra_info: null, - }, - { - assignment_type: 'Homework', - complete: true, - date: '2020-05-04T02:59:40.942669Z', - date_type: 'assignment-due-date', - description: '', - learner_has_access: true, - title: 'Multi Badges Completed', - extra_info: null, - }, - { - assignment_type: 'Homework', - date: '2020-05-05T02:59:40.942669Z', - date_type: 'assignment-due-date', - description: '', - learner_has_access: true, - title: 'Multi Badges Past Due', - extra_info: null, - }, - { - assignment_type: 'Homework', - date: '2020-05-27T02:59:40.942669Z', - date_type: 'assignment-due-date', - description: '', - learner_has_access: true, - link: 'https://example.com/', - title: 'Both Past Due 1', - extra_info: null, - }, - { - assignment_type: 'Homework', - date: '2020-05-27T02:59:40.942669Z', - date_type: 'assignment-due-date', - description: '', - learner_has_access: true, - link: 'https://example.com/', - title: 'Both Past Due 2', - extra_info: null, - }, - { - assignment_type: 'Homework', - complete: true, - date: '2020-05-28T08:59:40.942669Z', - date_type: 'assignment-due-date', - description: '', - learner_has_access: true, - link: 'https://example.com/', - title: 'One Completed/Due 1', - extra_info: null, - }, - { - assignment_type: 'Homework', - date: '2020-05-28T08:59:40.942669Z', - date_type: 'assignment-due-date', - description: '', - learner_has_access: true, - link: 'https://example.com/', - title: 'One Completed/Due 2', - extra_info: null, - }, - { - assignment_type: 'Homework', - complete: true, - date: '2020-05-29T08:59:40.942669Z', - date_type: 'assignment-due-date', - description: '', - learner_has_access: true, - link: 'https://example.com/', - title: 'Both Completed 1', - extra_info: null, - }, - { - assignment_type: 'Homework', - complete: true, - date: '2020-05-29T08:59:40.942669Z', - date_type: 'assignment-due-date', - description: '', - learner_has_access: true, - link: 'https://example.com/', - title: 'Both Completed 2', - extra_info: null, - }, - { - date: '2020-06-16T17:59:40.942669Z', - date_type: 'verified-upgrade-deadline', - description: "Don't miss the opportunity to highlight your new knowledge and skills by earning a verified certificate.", - learner_has_access: true, - link: 'https://example.com/', - title: 'Upgrade to Verified Certificate', - extra_info: null, - }, - { - assignment_type: 'Homework', - date: '2030-08-17T05:59:40.942669Z', - date_type: 'assignment-due-date', - description: '', - learner_has_access: false, - link: 'https://example.com/', - title: 'One Verified 1', - extra_info: null, - }, - { - assignment_type: 'Homework', - date: '2030-08-17T05:59:40.942669Z', - date_type: 'assignment-due-date', - description: '', - learner_has_access: true, - link: 'https://example.com/', - title: 'One Verified 2', - extra_info: null, - }, - { - assignment_type: 'Homework', - date: '2030-08-17T05:59:40.942669Z', - date_type: 'assignment-due-date', - description: '', - learner_has_access: true, - link: 'https://example.com/', - title: 'ORA Verified 2', - extra_info: "ORA Dates are set by the instructor, and can't be changed", - }, - { - assignment_type: 'Homework', - date: '2030-08-18T05:59:40.942669Z', - date_type: 'assignment-due-date', - description: '', - learner_has_access: false, - link: 'https://example.com/', - title: 'Both Verified 1', - extra_info: null, - }, - { - assignment_type: 'Homework', - date: '2030-08-18T05:59:40.942669Z', - date_type: 'assignment-due-date', - description: '', - learner_has_access: false, - link: 'https://example.com/', - title: 'Both Verified 2', - extra_info: null, - }, - { - assignment_type: 'Homework', - date: '2030-08-19T05:59:40.942669Z', - date_type: 'assignment-due-date', - description: '', - learner_has_access: true, - title: 'One Unreleased 1', - }, - { - assignment_type: 'Homework', - date: '2030-08-19T05:59:40.942669Z', - date_type: 'assignment-due-date', - description: '', - learner_has_access: true, - link: 'https://example.com/', - title: 'One Unreleased 2', - extra_info: null, - }, - { - assignment_type: 'Homework', - date: '2030-08-20T05:59:40.942669Z', - date_type: 'assignment-due-date', - description: '', - learner_has_access: true, - title: 'Both Unreleased 1', - extra_info: null, - }, - { - assignment_type: 'Homework', - date: '2030-08-20T05:59:40.942669Z', - date_type: 'assignment-due-date', - description: '', - learner_has_access: true, - title: 'Both Unreleased 2', - extra_info: null, - }, - { - date: '2030-08-23T00:00:00Z', - date_type: 'course-end-date', - description: '', - learner_has_access: true, - link: '', - title: 'Course Ends', - extra_info: null, - }, - { - date: '2030-09-01T00:00:00Z', - date_type: 'verification-deadline-date', - description: 'You must successfully complete verification before this date to qualify for a Verified Certificate.', - learner_has_access: false, - link: 'https://example.com/', - title: 'Verification Deadline', - extra_info: null, + extraInfo: '', }, ], - has_ended: false, + missed_deadlines: false, + missed_gated_content: false, learner_is_full_access: true, + user_timezone: null, + verified_upgrade_link: 'http://localhost:18130/basket/add/?sku=8CF08E5', }); diff --git a/src/course-home/data/__factories__/index.js b/src/course-home/data/__factories__/index.js index ae35f0aa16..a2680575c5 100644 --- a/src/course-home/data/__factories__/index.js +++ b/src/course-home/data/__factories__/index.js @@ -1,7 +1,3 @@ import './courseHomeMetadata.factory'; -import './badgeProgress.factory'; -import './badgeProgressTabData.factory'; import './datesTabData.factory'; import './outlineTabData.factory'; -import './progressTabData.factory'; -import './upgradeNotificationData.factory'; diff --git a/src/course-home/data/__factories__/outlineTabData.factory.js b/src/course-home/data/__factories__/outlineTabData.factory.js index ee501162f2..c9f5d39be7 100644 --- a/src/course-home/data/__factories__/outlineTabData.factory.js +++ b/src/course-home/data/__factories__/outlineTabData.factory.js @@ -1,63 +1,16 @@ import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies -import { buildMinimalCourseBlocks } from '../../../shared/data/__factories__/courseBlocks.factory'; +import '../../../courseware/data/__factories__/courseBlocks.factory'; Factory.define('outlineTabData') .option('courseId', 'course-v1:edX+DemoX+Demo_Course') .option('host', 'http://localhost:18000') - .option('date_blocks', []) - .attr('course_blocks', ['courseId'], courseId => { - const { courseBlocks } = buildMinimalCourseBlocks(courseId); - return { - blocks: courseBlocks.blocks, - }; - }) - .attr('dates_widget', ['date_blocks'], (dateBlocks) => ({ - course_date_blocks: dateBlocks, + .attr('course_tools', ['host', 'courseId'], (host, courseId) => ({ + analytics_id: 'edx.bookmarks', + title: 'Bookmarks', + url: `${host}/courses/${courseId}/bookmarks/`, })) - .attr('resume_course', ['host', 'courseId'], (host, courseId) => ({ - has_visited_course: false, - url: `${host}/courses/${courseId}/jump_to/block-v1:edX+Test+Block@12345abcde`, + .attr('course_blocks', ['courseId'], courseId => ({ + blocks: Factory.build('courseBlocks', { courseId }).blocks, })) - .attr('verified_mode', ['host'], (host) => ({ - access_expiration_date: '2050-01-01T12:00:00', - currency: 'USD', - currency_symbol: '$', - price: 149, - sku: 'ABCD1234', - upgrade_url: `${host}/dashboard`, - })) - .attrs({ - has_scheduled_content: null, - access_expiration: null, - can_show_upgrade_sock: false, - cert_data: { - cert_status: null, - cert_web_view_url: null, - certificate_available_date: null, - download_url: null, - }, - course_goals: { - goal_options: [], - selected_goal: null, - }, - course_tools: [ - { - analytics_id: 'edx.bookmarks', - title: 'Bookmarks', - url: 'https://example.com/bookmarks', - }, - ], - dates_banner_info: { - content_type_gating_enabled: false, - missed_gated_content: false, - missed_deadlines: false, - }, - enroll_alert: { - can_enroll: true, - extra_text: 'Contact the administrator.', - }, - handouts_html: '
  • Handout 1
', - offer: null, - welcome_message_html: '

Welcome to this course!

', - }); + .attr('handouts_html', [], () => '
  • Handout 1
'); diff --git a/src/course-home/data/__snapshots__/redux.test.js.snap b/src/course-home/data/__snapshots__/redux.test.js.snap index 0e5649f876..9dad7db432 100644 --- a/src/course-home/data/__snapshots__/redux.test.js.snap +++ b/src/course-home/data/__snapshots__/redux.test.js.snap @@ -1,14 +1,26 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Data layer integration tests Should initialize store 1`] = ` +Object { + "courseHome": Object { + "courseId": null, + "courseStatus": "loading", + }, + "courseware": Object { + "courseId": null, + "courseStatus": "loading", + "sequenceId": null, + "sequenceStatus": "loading", + }, + "models": Object {}, +} +`; + exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize, and save metadata 1`] = ` Object { "courseHome": Object { "courseId": "course-v1:edX+DemoX+Demo_Course_1", "courseStatus": "loaded", - "targetUserId": undefined, - "toastBodyLink": null, - "toastBodyText": null, - "toastHeader": "", }, "courseware": Object { "courseId": null, @@ -17,29 +29,17 @@ Object { "sequenceStatus": "loading", }, "models": Object { - "courseHomeMeta": Object { + "courses": 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, - }, + "courseId": "course-v1:edX+DemoX+Demo_Course_1", "id": "course-v1:edX+DemoX+Demo_Course_1", - "isEnrolled": false, - "isMasquerading": false, "isSelfPaced": false, "isStaff": false, "number": "DemoX", "org": "edX", - "originalUserIsStaff": false, - "start": "2013-02-05T05:00:00Z", "tabs": Array [ Object { - "slug": "outline", + "slug": "courseware", "title": "Course", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/course/", }, @@ -63,249 +63,37 @@ Object { "title": "Instructor", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/instructor", }, - Object { - "slug": "dates", - "title": "Dates", - "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/dates", - }, - Object { - "slug": "badges-progress", - "title": "Badges", - "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/badges-progress", - }, ], "title": "Demonstration Course", - "userTimezone": "UTC", - "verifiedMode": Object { - "currencySymbol": "$", - "price": 10, - "upgradeUrl": "test", - }, }, }, "dates": Object { "course-v1:edX+DemoX+Demo_Course_1": Object { "courseDateBlocks": Array [ Object { - "date": "2020-05-01T17:59:41Z", + "date": "2013-02-05T05:00:00Z", "dateType": "course-start-date", "description": "", - "extraInfo": null, + "extraInfo": "", "learnerHasAccess": true, "link": "", "title": "Course Starts", }, - Object { - "assignmentType": "Homework", - "complete": true, - "date": "2020-05-04T02:59:40.942669Z", - "dateType": "assignment-due-date", - "description": "", - "extraInfo": null, - "learnerHasAccess": true, - "title": "Multi Badges Completed", - }, - Object { - "assignmentType": "Homework", - "date": "2020-05-05T02:59:40.942669Z", - "dateType": "assignment-due-date", - "description": "", - "extraInfo": null, - "learnerHasAccess": true, - "title": "Multi Badges Past Due", - }, - Object { - "assignmentType": "Homework", - "date": "2020-05-27T02:59:40.942669Z", - "dateType": "assignment-due-date", - "description": "", - "extraInfo": null, - "learnerHasAccess": true, - "link": "https://example.com/", - "title": "Both Past Due 1", - }, - Object { - "assignmentType": "Homework", - "date": "2020-05-27T02:59:40.942669Z", - "dateType": "assignment-due-date", - "description": "", - "extraInfo": null, - "learnerHasAccess": true, - "link": "https://example.com/", - "title": "Both Past Due 2", - }, - Object { - "assignmentType": "Homework", - "complete": true, - "date": "2020-05-28T08:59:40.942669Z", - "dateType": "assignment-due-date", - "description": "", - "extraInfo": null, - "learnerHasAccess": true, - "link": "https://example.com/", - "title": "One Completed/Due 1", - }, - Object { - "assignmentType": "Homework", - "date": "2020-05-28T08:59:40.942669Z", - "dateType": "assignment-due-date", - "description": "", - "extraInfo": null, - "learnerHasAccess": true, - "link": "https://example.com/", - "title": "One Completed/Due 2", - }, - Object { - "assignmentType": "Homework", - "complete": true, - "date": "2020-05-29T08:59:40.942669Z", - "dateType": "assignment-due-date", - "description": "", - "extraInfo": null, - "learnerHasAccess": true, - "link": "https://example.com/", - "title": "Both Completed 1", - }, - Object { - "assignmentType": "Homework", - "complete": true, - "date": "2020-05-29T08:59:40.942669Z", - "dateType": "assignment-due-date", - "description": "", - "extraInfo": null, - "learnerHasAccess": true, - "link": "https://example.com/", - "title": "Both Completed 2", - }, - Object { - "date": "2020-06-16T17:59:40.942669Z", - "dateType": "verified-upgrade-deadline", - "description": "Don't miss the opportunity to highlight your new knowledge and skills by earning a verified certificate.", - "extraInfo": null, - "learnerHasAccess": true, - "link": "https://example.com/", - "title": "Upgrade to Verified Certificate", - }, - Object { - "assignmentType": "Homework", - "date": "2030-08-17T05:59:40.942669Z", - "dateType": "assignment-due-date", - "description": "", - "extraInfo": null, - "learnerHasAccess": false, - "link": "https://example.com/", - "title": "One Verified 1", - }, - Object { - "assignmentType": "Homework", - "date": "2030-08-17T05:59:40.942669Z", - "dateType": "assignment-due-date", - "description": "", - "extraInfo": null, - "learnerHasAccess": true, - "link": "https://example.com/", - "title": "One Verified 2", - }, - Object { - "assignmentType": "Homework", - "date": "2030-08-17T05:59:40.942669Z", - "dateType": "assignment-due-date", - "description": "", - "extraInfo": "ORA Dates are set by the instructor, and can't be changed", - "learnerHasAccess": true, - "link": "https://example.com/", - "title": "ORA Verified 2", - }, - Object { - "assignmentType": "Homework", - "date": "2030-08-18T05:59:40.942669Z", - "dateType": "assignment-due-date", - "description": "", - "extraInfo": null, - "learnerHasAccess": false, - "link": "https://example.com/", - "title": "Both Verified 1", - }, - Object { - "assignmentType": "Homework", - "date": "2030-08-18T05:59:40.942669Z", - "dateType": "assignment-due-date", - "description": "", - "extraInfo": null, - "learnerHasAccess": false, - "link": "https://example.com/", - "title": "Both Verified 2", - }, - Object { - "assignmentType": "Homework", - "date": "2030-08-19T05:59:40.942669Z", - "dateType": "assignment-due-date", - "description": "", - "learnerHasAccess": true, - "title": "One Unreleased 1", - }, - Object { - "assignmentType": "Homework", - "date": "2030-08-19T05:59:40.942669Z", - "dateType": "assignment-due-date", - "description": "", - "extraInfo": null, - "learnerHasAccess": true, - "link": "https://example.com/", - "title": "One Unreleased 2", - }, - Object { - "assignmentType": "Homework", - "date": "2030-08-20T05:59:40.942669Z", - "dateType": "assignment-due-date", - "description": "", - "extraInfo": null, - "learnerHasAccess": true, - "title": "Both Unreleased 1", - }, - Object { - "assignmentType": "Homework", - "date": "2030-08-20T05:59:40.942669Z", - "dateType": "assignment-due-date", - "description": "", - "extraInfo": null, - "learnerHasAccess": true, - "title": "Both Unreleased 2", - }, - Object { - "date": "2030-08-23T00:00:00Z", - "dateType": "course-end-date", - "description": "", - "extraInfo": null, - "learnerHasAccess": true, - "link": "", - "title": "Course Ends", - }, - Object { - "date": "2030-09-01T00:00:00Z", - "dateType": "verification-deadline-date", - "description": "You must successfully complete verification before this date to qualify for a Verified Certificate.", - "extraInfo": null, - "learnerHasAccess": false, - "link": "https://example.com/", - "title": "Verification Deadline", - }, ], "datesBannerInfo": Object { "contentTypeGatingEnabled": false, "missedDeadlines": false, "missedGatedContent": false, - "verifiedUpgradeLink": "http://localhost:18130/basket/add/?sku=8CF08E5", }, - "hasEnded": false, "id": "course-v1:edX+DemoX+Demo_Course_1", "learnerIsFullAccess": true, + "missedDeadlines": false, + "missedGatedContent": false, + "userTimezone": null, + "verifiedUpgradeLink": "http://localhost:18130/basket/add/?sku=8CF08E5", }, }, }, - "recommendations": Object { - "recommendationsStatus": "loading", - }, } `; @@ -314,10 +102,6 @@ Object { "courseHome": Object { "courseId": "course-v1:edX+DemoX+Demo_Course_1", "courseStatus": "loaded", - "targetUserId": undefined, - "toastBodyLink": null, - "toastBodyText": null, - "toastHeader": "", }, "courseware": Object { "courseId": null, @@ -326,29 +110,17 @@ Object { "sequenceStatus": "loading", }, "models": Object { - "courseHomeMeta": Object { + "courses": 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, - }, + "courseId": "course-v1:edX+DemoX+Demo_Course_1", "id": "course-v1:edX+DemoX+Demo_Course_1", - "isEnrolled": false, - "isMasquerading": false, "isSelfPaced": false, "isStaff": false, "number": "DemoX", "org": "edX", - "originalUserIsStaff": false, - "start": "2013-02-05T05:00:00Z", "tabs": Array [ Object { - "slug": "outline", + "slug": "courseware", "title": "Course", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/course/", }, @@ -372,317 +144,63 @@ Object { "title": "Instructor", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/instructor", }, - Object { - "slug": "dates", - "title": "Dates", - "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/dates", - }, - Object { - "slug": "badges-progress", - "title": "Badges", - "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/badges-progress", - }, ], "title": "Demonstration Course", - "userTimezone": "UTC", - "verifiedMode": Object { - "currencySymbol": "$", - "price": 10, - "upgradeUrl": "test", - }, }, }, "outline": Object { "course-v1:edX+DemoX+Demo_Course_1": Object { - "accessExpiration": null, - "canShowUpgradeSock": false, - "certData": Object { - "certStatus": null, - "certWebViewUrl": null, - "certificateAvailableDate": null, - "downloadUrl": null, - }, "courseBlocks": Object { "courses": Object { - "block-v1:edX+DemoX+Demo_Course+type@course+block@bcdabcdabcdabcdabcdabcdabcdabcd3": Object { - "hasScheduledContent": false, + "block-v1:edX+DemoX+Demo_Course+type@course+block@bcdabcdabcdabcdabcdabcdabcdabcd4": Object { "id": "course-v1:edX+DemoX+Demo_Course_1", "sectionIds": Array [ - "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2", + "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd3", ], - "title": "bcdabcdabcdabcdabcdabcdabcdabcd3", + "title": "bcdabcdabcdabcdabcdabcdabcdabcd4", }, }, "sections": Object { - "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2": Object { - "complete": false, + "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd3": Object { "courseId": "course-v1:edX+DemoX+Demo_Course_1", - "id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2", - "resumeBlock": false, + "id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd3", "sequenceIds": Array [ - "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1", + "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd2", ], - "title": "Title of Section", + "title": "bcdabcdabcdabcdabcdabcdabcdabcd3", }, }, "sequences": Object { - "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1": Object { - "complete": false, - "description": null, - "due": null, - "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", - "sectionId": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2", - "showLink": true, - "title": "Title of Sequence", + "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd2": Object { + "id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd2", + "lmsWebUrl": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd2", + "sectionId": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd3", + "title": "bcdabcdabcdabcdabcdabcdabcdabcd2", + "unitIds": Array [ + "block-v1:edX+DemoX+Demo_Course+type@vertical+block@bcdabcdabcdabcdabcdabcdabcdabcd1", + ], }, }, - }, - "courseGoals": Object { - "goalOptions": Array [], - "selectedGoal": null, - }, - "courseTools": Array [ - Object { - "analyticsId": "edx.bookmarks", - "title": "Bookmarks", - "url": "https://example.com/bookmarks", + "units": Object { + "block-v1:edX+DemoX+Demo_Course+type@vertical+block@bcdabcdabcdabcdabcdabcdabcdabcd1": Object { + "graded": false, + "id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@bcdabcdabcdabcdabcdabcdabcdabcd1", + "lmsWebUrl": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@vertical+block@bcdabcdabcdabcdabcdabcdabcdabcd1", + "sequenceId": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd2", + "title": "bcdabcdabcdabcdabcdabcdabcdabcd1", + }, }, - ], - "datesBannerInfo": Object { - "contentTypeGatingEnabled": false, - "missedDeadlines": false, - "missedGatedContent": false, }, - "datesWidget": Object { - "courseDateBlocks": Array [], + "courseTools": Object { + "analyticsId": "edx.bookmarks", + "title": "Bookmarks", + "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/bookmarks/", }, - "enrollAlert": Object { - "canEnroll": true, - "extraText": "Contact the administrator.", - }, - "enrollmentMode": undefined, + "datesWidget": undefined, "handoutsHtml": "
  • Handout 1
", - "hasEnded": undefined, - "hasScheduledContent": null, "id": "course-v1:edX+DemoX+Demo_Course_1", - "offer": null, - "resumeCourse": Object { - "hasVisitedCourse": false, - "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+Test+Block@12345abcde", - }, - "timeOffsetMillis": 0, - "userHasPassingGrade": undefined, - "verifiedMode": Object { - "accessExpirationDate": "2050-01-01T12:00:00", - "currency": "USD", - "currencySymbol": "$", - "price": 149, - "sku": "ABCD1234", - "upgradeUrl": "http://localhost:18000/dashboard", - }, - "welcomeMessageHtml": "

Welcome to this course!

", }, }, }, - "recommendations": Object { - "recommendationsStatus": "loading", - }, -} -`; - -exports[`Data layer integration tests Test fetchProgressTab Should fetch, normalize, and save metadata 1`] = ` -Object { - "courseHome": Object { - "courseId": "course-v1:edX+DemoX+Demo_Course_1", - "courseStatus": "loaded", - "targetUserId": undefined, - "toastBodyLink": null, - "toastBodyText": null, - "toastHeader": "", - }, - "courseware": Object { - "courseId": null, - "courseStatus": "loading", - "sequenceId": null, - "sequenceStatus": "loading", - }, - "models": 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, - "isMasquerading": false, - "isSelfPaced": false, - "isStaff": false, - "number": "DemoX", - "org": "edX", - "originalUserIsStaff": false, - "start": "2013-02-05T05:00:00Z", - "tabs": Array [ - Object { - "slug": "outline", - "title": "Course", - "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/course/", - }, - Object { - "slug": "discussion", - "title": "Discussion", - "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/discussion/forum/", - }, - Object { - "slug": "wiki", - "title": "Wiki", - "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/course_wiki", - }, - Object { - "slug": "progress", - "title": "Progress", - "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/progress", - }, - Object { - "slug": "instructor", - "title": "Instructor", - "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/instructor", - }, - Object { - "slug": "dates", - "title": "Dates", - "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/dates", - }, - Object { - "slug": "badges-progress", - "title": "Badges", - "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/badges-progress", - }, - ], - "title": "Demonstration Course", - "userTimezone": "UTC", - "verifiedMode": Object { - "currencySymbol": "$", - "price": 10, - "upgradeUrl": "test", - }, - }, - }, - "progress": Object { - "course-v1:edX+DemoX+Demo_Course_1": Object { - "accessExpiration": null, - "certificateData": Object {}, - "completionSummary": Object { - "completeCount": 1, - "incompleteCount": 1, - "lockedCount": 0, - }, - "courseGrade": Object { - "isPassing": true, - "letterGrade": "pass", - "percent": 1, - "visiblePercent": 1, - }, - "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 { - "averageGrade": 1, - "numDroppable": 1, - "shortLabel": "HW", - "type": "Homework", - "weight": 1, - "weightedGrade": 1, - }, - ], - "gradeRange": Object { - "pass": 0.75, - }, - }, - "hasScheduledContent": false, - "id": "course-v1:edX+DemoX+Demo_Course_1", - "sectionScores": Array [ - Object { - "displayName": "First section", - "subsections": Array [ - Object { - "assignmentType": "Homework", - "blockKey": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345", - "displayName": "First subsection", - "hasGradedAssignment": true, - "learnerHasAccess": true, - "numPointsEarned": 0, - "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", - }, - ], - }, - Object { - "displayName": "Second section", - "subsections": Array [ - Object { - "assignmentType": "Homework", - "displayName": "Second subsection", - "hasGradedAssignment": true, - "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", - }, - ], - }, - ], - "studioUrl": "http://studio.edx.org/settings/grading/course-v1:edX+Test+run", - "userHasPassingGrade": false, - "verificationData": Object { - "link": null, - "status": "none", - "statusDate": null, - }, - "verifiedMode": null, - }, - }, - }, - "recommendations": Object { - "recommendationsStatus": "loading", - }, } `; diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js index 672b7ca4f8..670c4fa45f 100644 --- a/src/course-home/data/api.js +++ b/src/course-home/data/api.js @@ -212,10 +212,28 @@ export async function getBadgeProgressTabData(courseId) { } } +export async function getBadgeLeaderboardTabData(courseId) { + // Todo: Need to define an Badge Leaderboard API endpoint for the LMS + // and return the result. + const url = `${getConfig().LMS_BASE_URL}/api/badges/v1/leaderboard/courses/${courseId}`; + try { + const { data } = await getAuthenticatedHttpClient().get(url); + return camelCaseObject(data); + } catch (error) { + const { httpErrorStatus } = error && error.customAttributes; + if (httpErrorStatus === 404) { + // global.location.replace(`${getConfig().LMS_BASE_URL}/courses/${courseId}/badges/progress`); + return {}; + } + throw error; + } +} + // For debugging purposes, you might like to see a fully loaded dates tab. // Just uncomment the next few lines and the immediate 'return' in the function below // import { Factory } from 'rosie'; // import './__factories__'; + export async function getDatesTabData(courseId) { // return camelCaseObject(Factory.build('datesTabData')); const url = `${getConfig().LMS_BASE_URL}/api/course_home/dates/${courseId}`; diff --git a/src/course-home/data/index.js b/src/course-home/data/index.js index a19babbc92..b3eaab85bc 100644 --- a/src/course-home/data/index.js +++ b/src/course-home/data/index.js @@ -1,5 +1,6 @@ export { fetchBadgeProgressTab, + fetchBadgeLeaderboardTab, fetchDatesTab, fetchOutlineTab, fetchProgressTab, diff --git a/src/course-home/data/redux.test.js b/src/course-home/data/redux.test.js index 14bc8b15d5..db87abf66e 100644 --- a/src/course-home/data/redux.test.js +++ b/src/course-home/data/redux.test.js @@ -6,21 +6,32 @@ import { getConfig } from '@edx/frontend-platform'; import * as thunks from './thunks'; -import { appendBrowserTimezoneToUrl, executeThunk } from '../../utils'; +import executeThunk from '../../utils'; -import { initializeMockApp } from '../../setupTest'; +import './__factories__'; +import '../../courseware/data/__factories__/courseMetadata.factory'; +import initializeMockApp from '../../setupTest'; import initializeStore from '../../store'; -import { debug } from 'util'; const { loggingService } = initializeMockApp(); const axiosMock = new MockAdapter(getAuthenticatedHttpClient()); describe('Data layer integration tests', () => { - const courseHomeMetadata = Factory.build('courseHomeMetadata'); - const { id: courseId } = courseHomeMetadata; - let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/course_home/course_metadata/${courseId}`; - courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl); + const courseMetadata = Factory.build('courseMetadata'); + const courseHomeMetadata = Factory.build( + 'courseHomeMetadata', { + course_id: courseMetadata.id, + }, + { courseTabs: courseMetadata.tabs }, + ); + + const courseId = courseMetadata.id; + const courseBaseUrl = `${getConfig().LMS_BASE_URL}/api/courseware/course`; + const courseMetadataBaseUrl = `${getConfig().LMS_BASE_URL}/api/course_home/v1/course_metadata`; + + const courseUrl = `${courseBaseUrl}/${courseId}`; + const courseMetadataUrl = `${courseMetadataBaseUrl}/${courseId}`; let store; @@ -31,40 +42,15 @@ describe('Data layer integration tests', () => { store = initializeStore(); }); - describe('Test fetchBadgeProgressTab', () => { - const badgeProgressBaseUrl = `${getConfig().LMS_BASE_URL}/api/badges/v1/progress`; - it('Should fail to fetch if error occurs', async () => { - axiosMock.onGet(courseMetadataUrl).networkError(); - axiosMock.onGet(`${badgeProgressBaseUrl}/${courseId}`).networkError(); - - await executeThunk(thunks.fetchBadgeProgressTab(courseId), store.dispatch); - - expect(loggingService.logError).toHaveBeenCalled(); - expect(store.getState().courseHome.courseStatus).toEqual('failed'); - }); - - // it('Should fetch, normalize, and save metadata', async () => { - // const badgeProgressTabData = Factory.build('badgeProgressTabData'); - - // console.warn(badgeProgressTabData); - - // const badgeProgressUrl = `${badgeProgressBaseUrl}/${courseId}`; - - // axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeMetadata); - // axiosMock.onGet(badgeProgressUrl).reply(200, badgeProgressTabData); - - // await executeThunk(thunks.fetchBadgeProgressTab(courseId), store.dispatch); - - // const state = store.getState(); - // expect(state.courseHome.courseStatus).toEqual('loaded'); - // expect(state).toMatchSnapshot(); - // }); - }); + it('Should initialize store', () => { + expect(store.getState()).toMatchSnapshot(); + }); describe('Test fetchDatesTab', () => { - const datesBaseUrl = `${getConfig().LMS_BASE_URL}/api/course_home/dates`; -`` + const datesBaseUrl = `${getConfig().LMS_BASE_URL}/api/course_home/v1/dates`; + it('Should fail to fetch if error occurs', async () => { + axiosMock.onGet(courseUrl).networkError(); axiosMock.onGet(courseMetadataUrl).networkError(); axiosMock.onGet(`${datesBaseUrl}/${courseId}`).networkError(); @@ -79,6 +65,7 @@ describe('Data layer integration tests', () => { const datesUrl = `${datesBaseUrl}/${courseId}`; + axiosMock.onGet(courseUrl).reply(200, courseMetadata); axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeMetadata); axiosMock.onGet(datesUrl).reply(200, datesTabData); @@ -91,9 +78,10 @@ describe('Data layer integration tests', () => { }); describe('Test fetchOutlineTab', () => { - const outlineBaseUrl = `${getConfig().LMS_BASE_URL}/api/course_home/outline`; + const outlineBaseUrl = `${getConfig().LMS_BASE_URL}/api/course_home/v1/outline`; it('Should result in fetch failure if error occurs', async () => { + axiosMock.onGet(courseUrl).networkError(); axiosMock.onGet(courseMetadataUrl).networkError(); axiosMock.onGet(`${outlineBaseUrl}/${courseId}`).networkError(); @@ -108,6 +96,7 @@ describe('Data layer integration tests', () => { const outlineUrl = `${outlineBaseUrl}/${courseId}`; + axiosMock.onGet(courseUrl).reply(200, courseMetadata); axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeMetadata); axiosMock.onGet(outlineUrl).reply(200, outlineTabData); @@ -119,89 +108,21 @@ describe('Data layer integration tests', () => { }); }); - describe('Test fetchProgressTab', () => { - const progressBaseUrl = `${getConfig().LMS_BASE_URL}/api/course_home/progress`; - - it('Should result in fetch failure if error occurs', async () => { - axiosMock.onGet(courseMetadataUrl).networkError(); - axiosMock.onGet(`${progressBaseUrl}/${courseId}`).networkError(); - - await executeThunk(thunks.fetchProgressTab(courseId), store.dispatch); - - expect(loggingService.logError).toHaveBeenCalled(); - expect(store.getState().courseHome.courseStatus).toEqual('failed'); - }); - - it('Should fetch, normalize, and save metadata', async () => { - const progressTabData = Factory.build('progressTabData', { courseId }); - - const progressUrl = `${progressBaseUrl}/${courseId}`; - - axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeMetadata); - axiosMock.onGet(progressUrl).reply(200, progressTabData); - - await executeThunk(thunks.fetchProgressTab(courseId), store.dispatch); - - const state = store.getState(); - expect(state.courseHome.courseStatus).toEqual('loaded'); - expect(state).toMatchSnapshot(); - }); - - it('Should handle the url including a targetUserId', async () => { - const progressTabData = Factory.build('progressTabData', { courseId }); - const targetUserId = 2; - const progressUrl = `${progressBaseUrl}/${courseId}/${targetUserId}/`; - - axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeMetadata); - axiosMock.onGet(progressUrl).reply(200, progressTabData); - - await executeThunk(thunks.fetchProgressTab(courseId, 2), store.dispatch); - - const state = store.getState(); - expect(state.courseHome.targetUserId).toEqual(2); - }); - }); - - describe('Test saveCourseGoal', () => { - it('Should save course goal', async () => { - const goalUrl = `${getConfig().LMS_BASE_URL}/api/course_home/save_course_goal`; - axiosMock.onPost(goalUrl).reply(200, {}); - - await thunks.saveCourseGoal(courseId, 'unsure'); - - expect(axiosMock.history.post[0].url).toEqual(goalUrl); - expect(axiosMock.history.post[0].data).toEqual(`{"course_id":"${courseId}","goal_key":"unsure"}`); - }); - }); - describe('Test resetDeadlines', () => { it('Should reset course deadlines', async () => { const resetUrl = `${getConfig().LMS_BASE_URL}/api/course_experience/v1/reset_course_deadlines`; - const model = 'dates'; - axiosMock.onPost(resetUrl).reply(201, {}); + axiosMock.onPost(resetUrl).reply(201); const getTabDataMock = jest.fn(() => ({ type: 'MOCK_ACTION', })); - await executeThunk(thunks.resetDeadlines(courseId, model, getTabDataMock), store.dispatch); + await executeThunk(thunks.resetDeadlines(courseId, getTabDataMock), store.dispatch); expect(axiosMock.history.post[0].url).toEqual(resetUrl); - expect(axiosMock.history.post[0].data).toEqual(`{"course_key":"${courseId}","research_event_data":{"location":"dates-tab"}}`); + expect(axiosMock.history.post[0].data).toEqual(`{"course_key":"${courseId}"}`); expect(getTabDataMock).toHaveBeenCalledWith(courseId); }); }); - - describe('Test dismissWelcomeMessage', () => { - it('Should dismiss welcome message', async () => { - const dismissUrl = `${getConfig().LMS_BASE_URL}/api/course_home/dismiss_welcome_message`; - axiosMock.onPost(dismissUrl).reply(201); - - await executeThunk(thunks.dismissWelcomeMessage(courseId), store.dispatch); - - expect(axiosMock.history.post[0].url).toEqual(dismissUrl); - expect(axiosMock.history.post[0].data).toEqual(`{"course_id":"${courseId}"}`); - }); - }); }); diff --git a/src/course-home/data/thunks.js b/src/course-home/data/thunks.js index 2fd9bbd37b..665308ec3a 100644 --- a/src/course-home/data/thunks.js +++ b/src/course-home/data/thunks.js @@ -4,6 +4,7 @@ import { executePostFromPostEvent, getCourseHomeCourseMetadata, getBadgeProgressTabData, + getBadgeLeaderboardTabData, getDatesTabData, getOutlineTabData, getProgressTabData, @@ -79,7 +80,11 @@ export function fetchTab(courseId, tab, getTabData, targetUserId) { } export function fetchBadgeProgressTab(courseId) { - return fetchTab(courseId, 'badges-progress', getBadgeProgressTabData); + return fetchTab(courseId, 'badge-progress', getBadgeProgressTabData); +} + +export function fetchBadgeLeaderboardTab(courseId) { + return fetchTab(courseId, 'badge-leaderboard', getBadgeLeaderboardTabData); } export function fetchDatesTab(courseId) { diff --git a/src/index.jsx b/src/index.jsx index 7869da0c54..63cb5b128e 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -20,13 +20,13 @@ import OutlineTab from './course-home/outline-tab'; import { CourseExit } from './courseware/course/course-exit'; import CoursewareContainer from './courseware'; import CoursewareRedirectLandingPage from './courseware/CoursewareRedirectLandingPage'; +import { BadgeProgressTab, BadgeLeaderboardTab } from './course-home/badges-tab'; import DatesTab from './course-home/dates-tab'; import GoalUnsubscribe from './course-home/goal-unsubscribe'; -import BadgeProgressTab from './course-home/badges-tab/BadgeProgressTab'; import ProgressTab from './course-home/progress-tab/ProgressTab'; import { TabContainer } from './tab-page'; -import { fetchDatesTab, fetchOutlineTab, fetchProgressTab, fetchBadgeProgressTab } from './course-home/data'; +import { fetchBadgeProgressTab, fetchBadgeLeaderboardTab, fetchDatesTab, fetchOutlineTab, fetchProgressTab } from './course-home/data'; import { fetchCourse } from './courseware/data'; import initializeStore from './store'; import NoticesProvider from './generic/notices'; @@ -45,10 +45,15 @@ subscribe(APP_READY, () => {
- + + + + + + diff --git a/src/index.scss b/src/index.scss index 167afaa55e..e12a8baf4d 100755 --- a/src/index.scss +++ b/src/index.scss @@ -54,6 +54,63 @@ } } +.badge-tabs-navigation { + + border-bottom: solid 3px theme-color('dark', 400); + background-color: theme-color('gray', 200); + + .badge-nav-tabs { + + margin: 0 20px 0 20px; + + .dropdown-menu { + .nav-link { + display: block; + } + } + .nav-link { + + display: inline-block; + + &.active { + background-color: theme-color('brand', 200); + } + + &.disabled { + &:hover, + &:focus, + &.active { + background-color: none !important; + color: theme-color('light', 400); + } + } + + &:hover, + &:focus { + background-color: theme-color('brand', 400); + color: white; + } + + } + + } + + .logo { + display: block; + box-sizing: content-box; + position: relative; + top: .10em; + height: 1.75rem; + margin-right: 1rem; + margin-top: 0.5rem; + img { + display: block; + height: 100%; + } + } + +} + .nav-underline-tabs { margin: 0 0 -1px; @@ -112,7 +169,7 @@ border-radius: 0; border: solid 1px #eaeaea; border-left-width: 0; - position: relative; + position: relative; font-weight: 400; padding: 0 0.375rem; height: 3rem; From af46db02de0d5da09248e7003b18f381ee3d97f1 Mon Sep 17 00:00:00 2001 From: Zachary Trabookis Date: Wed, 9 Mar 2022 13:57:12 -0500 Subject: [PATCH 03/17] feat(badges): Created progress banner to handle messaging between instructor and learner. --- .../badges-tab/BadgeProgressTab.jsx | 11 ++++- .../badge-progress/BadgeProgressBanner.jsx | 44 +++++++++++++++++++ .../badges-tab/badge-progress/index.js | 2 + src/course-home/data/slice.js | 2 +- src/course-home/data/thunks.js | 2 +- 5 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 src/course-home/badges-tab/badge-progress/BadgeProgressBanner.jsx create mode 100644 src/course-home/badges-tab/badge-progress/index.js diff --git a/src/course-home/badges-tab/BadgeProgressTab.jsx b/src/course-home/badges-tab/BadgeProgressTab.jsx index de2c946516..f89685aaa5 100644 --- a/src/course-home/badges-tab/BadgeProgressTab.jsx +++ b/src/course-home/badges-tab/BadgeProgressTab.jsx @@ -12,7 +12,7 @@ import { useModel } from '../../generic/model-store'; import { debug } from 'util'; import { BadgeTabsNavigation } from './badge-header'; -// import { BadgeProgressBanner, BadgeProgressCard, BadgeProgressCourseList } from './badge-progress'; +import { BadgeProgressBanner } from './badge-progress'; // import { headingMapper } from './utils'; @@ -45,7 +45,10 @@ function BadgeProgressTab({ intl }) { const checkBadgeProgressExists = ( progress ) => { let _badgeProgressState = []; + + debugger; Object.values(progress).forEach(student => { + debugger; if (typeof student === 'object' && Array.isArray(student.progress)) { if (student.progress.length > 0) { _badgeProgressState.push(student); @@ -57,8 +60,9 @@ function BadgeProgressTab({ intl }) { } useEffect(() => { - let _badgeProgressState = checkBadgeProgressExists(badgeProgressState); + let _badgeProgressState = checkBadgeProgressExists(badgeProgressState.value); + debugger; if ( _badgeProgressState.length ) { setProgress(_badgeProgressState); } else { @@ -110,10 +114,13 @@ function BadgeProgressTab({ intl }) { const userRoleNames = roles ? roles.map(role => role.split(':')[0]) : []; + debugger; + return ( <>
+
diff --git a/src/course-home/badges-tab/badge-progress/BadgeProgressBanner.jsx b/src/course-home/badges-tab/badge-progress/BadgeProgressBanner.jsx new file mode 100644 index 0000000000..df470dca7c --- /dev/null +++ b/src/course-home/badges-tab/badge-progress/BadgeProgressBanner.jsx @@ -0,0 +1,44 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +const BadgeProgressBanner = ({ hasProgress, hasRights }) => { + const indicatorProgress = (hasProgress ? '' : 'no-progress'); + + const getPathTitle = () => (hasRights ? 'Class Progress' : 'My Learning Path'); + + const getPathSubHeading = () => ( + hasRights + ? "Here is all learner progress through the badges available for this course. Click each badge to learn more about an individual student's credential." + : "Here is your progress through the badges available for this course. Click each badge to learn more or to save and share badges you've earned." + ); + + // d-flex justify-content-left + return ( +
+ { hasProgress && ( + <> +
+

{getPathTitle()}

+
+
+

+ {getPathSubHeading()} +

+
+ + )} + + { !hasProgress && ( +

This course either has no learner progress or is not setup for badging.

+ )} +
+ ); +}; + +BadgeProgressBanner.propTypes = { + hasProgress: PropTypes.bool.isRequired, + hasRights: PropTypes.bool.isRequired, +}; + +export default BadgeProgressBanner; diff --git a/src/course-home/badges-tab/badge-progress/index.js b/src/course-home/badges-tab/badge-progress/index.js new file mode 100644 index 0000000000..4b7595a009 --- /dev/null +++ b/src/course-home/badges-tab/badge-progress/index.js @@ -0,0 +1,2 @@ +/* eslint-disable import/prefer-default-export */ +export { default as BadgeProgressBanner } from './BadgeProgressBanner'; diff --git a/src/course-home/data/slice.js b/src/course-home/data/slice.js index b195b420ed..b8efcfe565 100644 --- a/src/course-home/data/slice.js +++ b/src/course-home/data/slice.js @@ -9,7 +9,7 @@ export const DENIED = 'denied'; const slice = createSlice({ name: 'course-home', initialState: { - courseStatus: 'loading', + courseStatus: LOADING, courseId: null, toastBodyText: null, toastBodyLink: null, diff --git a/src/course-home/data/thunks.js b/src/course-home/data/thunks.js index 665308ec3a..55f84c63ab 100644 --- a/src/course-home/data/thunks.js +++ b/src/course-home/data/thunks.js @@ -58,7 +58,7 @@ export function fetchTab(courseId, tab, getTabData, targetUserId) { modelType: tab, model: { id: courseId, - ...tabDataResult.value, + ...tabDataResult, }, })); } else { From a7564a4ff7129a7531c021c740975aa227c20521 Mon Sep 17 00:00:00 2001 From: Zachary Trabookis Date: Wed, 9 Mar 2022 14:38:22 -0500 Subject: [PATCH 04/17] feat(badges): Setup initial Badge Progress for learner and administrator. --- .../badges-tab/BadgeProgressTab.jsx | 126 +++++-------- .../badge-progress/BadgeProgressTab.scss | 62 +++++++ .../{ => banner}/BadgeProgressBanner.jsx | 0 .../badge-progress/banner/index.scss | 25 +++ .../badge-progress/card/BadgeProgressCard.jsx | 110 +++++++++++ .../card/BadgeProgressCardStatus.jsx | 42 +++++ .../badges-tab/badge-progress/card/index.scss | 172 ++++++++++++++++++ .../course-list/BadgeProgressCourseList.jsx | 99 ++++++++++ .../BadgeProgressCourseListItem.jsx | 48 +++++ .../badge-progress/course-list/index.scss | 87 +++++++++ .../badges-tab/badge-progress/index.js | 5 +- src/course-home/badges-tab/utils.js | 43 +++++ src/index.scss | 58 +----- src/utils/empty.js | 10 + 14 files changed, 749 insertions(+), 138 deletions(-) create mode 100644 src/course-home/badges-tab/badge-progress/BadgeProgressTab.scss rename src/course-home/badges-tab/badge-progress/{ => banner}/BadgeProgressBanner.jsx (100%) create mode 100644 src/course-home/badges-tab/badge-progress/banner/index.scss create mode 100644 src/course-home/badges-tab/badge-progress/card/BadgeProgressCard.jsx create mode 100644 src/course-home/badges-tab/badge-progress/card/BadgeProgressCardStatus.jsx create mode 100644 src/course-home/badges-tab/badge-progress/card/index.scss create mode 100644 src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseList.jsx create mode 100644 src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListItem.jsx create mode 100644 src/course-home/badges-tab/badge-progress/course-list/index.scss create mode 100644 src/course-home/badges-tab/utils.js create mode 100644 src/utils/empty.js diff --git a/src/course-home/badges-tab/BadgeProgressTab.jsx b/src/course-home/badges-tab/BadgeProgressTab.jsx index f89685aaa5..d5fba30bda 100644 --- a/src/course-home/badges-tab/BadgeProgressTab.jsx +++ b/src/course-home/badges-tab/BadgeProgressTab.jsx @@ -4,7 +4,6 @@ import { useSelector } from 'react-redux'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { StatusAlert } from '@edx/paragon'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; -// import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons'; @@ -12,9 +11,9 @@ import { useModel } from '../../generic/model-store'; import { debug } from 'util'; import { BadgeTabsNavigation } from './badge-header'; -import { BadgeProgressBanner } from './badge-progress'; +import { BadgeProgressBanner, BadgeProgressCard, BadgeProgressCourseList } from './badge-progress'; -// import { headingMapper } from './utils'; +import { headingMapper } from './utils'; function BadgeProgressTab({ intl }) { @@ -24,7 +23,6 @@ function BadgeProgressTab({ intl }) { courseId, } = useSelector(state => state.courseHome); - // username const { administrator, username, @@ -34,7 +32,6 @@ function BadgeProgressTab({ intl }) { const hasInstructorStaffRights = () => administrator; const [progress, setProgress] = useState([]); - // const badgeProgressState = useModel('badges-progress', courseId); const { id, @@ -42,97 +39,68 @@ function BadgeProgressTab({ intl }) { } = useModel('badge-progress', courseId); const hasBadgeProgress = () => progress && progress.length > 0; - - const checkBadgeProgressExists = ( progress ) => { - let _badgeProgressState = []; - - debugger; - Object.values(progress).forEach(student => { - debugger; - if (typeof student === 'object' && Array.isArray(student.progress)) { - if (student.progress.length > 0) { - _badgeProgressState.push(student); + useEffect(() => { + let classProgressExists = 0; + if (hasInstructorStaffRights()) { + badgeProgressState.value.forEach(student => { + if (student.progress.length) { + classProgressExists += 1; } + }); + if (classProgressExists) { + setProgress(badgeProgressState.value); } - }); - - return _badgeProgressState; - } - - useEffect(() => { - let _badgeProgressState = checkBadgeProgressExists(badgeProgressState.value); - - debugger; - if ( _badgeProgressState.length ) { - setProgress(_badgeProgressState); } else { - console.log("BadgeProgressTab: Could not find any course badge progress."); + setProgress(badgeProgressState.value); } + }, [courseId, administrator]); - // if ( badgeProgressState ) { - // let checkBadgeProgressExists = badgeProgressState.some(x => x.progress.length > 0); - // debugger; - // if ( checkBadgeProgressExists ) { - // debugger; - // setProgress(badgeProgressState); - // } - // } - - // setProgress(badgeProgressState); - - // let classBadgeProgressExists = 0; - // let badgeProgressStateUpdated = []; - - // if (hasInstructorStaffRights()) { - // // Loop through all student's and build new state by removing added course_id from fetchTabData. - // debugger; - // Object.values(badgeProgressState).forEach(student => { - // if (typeof student === 'object' && Array.isArray(student.progress)) { - // badgeProgressStateUpdated.push(student); - // classBadgeProgressExists += student.progress.length; - // debugger; - // } - // }); - // debugger; - // if (classBadgeProgressExists) { - // debugger; - // setProgress(badgeProgressStateUpdated); - // } - // } else { - // // Loop through all student's and build new state by removing added course_id from fetchTabData. - // Object.values(badgeProgressState).forEach(value => { - // if (typeof value === 'object' && Array.isArray(value.progress)) { - // badgeProgressStateUpdated.push(value); - // } - // }); - // setProgress(badgeProgressStateUpdated); - // } - }, []); //, courseId, administrator]); - - const renderBadgeProgress = () => { +const renderBadgeProgress = () => { const defaultAssignmentFilter = 'All'; - const userRoleNames = roles ? roles.map(role => role.split(':')[0]) : []; - - debugger; + if (hasInstructorStaffRights()) { + return ( + <> + + + + + ); + } return ( <>
- +
-
+
+
+ {progress && ( +
+ {progress.map(learnerProgress => ( + + ))} +
+ )} +
+
+ {/*
- the user is {username} + the user is {username} and they are enrolled as an {enrollmentMode} learner {administrator - &&
the user is admin
} - {roles &&
{userRoleNames}
} + &&

the user is admin

}
-
+
*/}
- + ); }; @@ -167,5 +135,3 @@ BadgeProgressTab.propTypes = { }; export default injectIntl(BadgeProgressTab); -// export default BadgeProgressTab; - diff --git a/src/course-home/badges-tab/badge-progress/BadgeProgressTab.scss b/src/course-home/badges-tab/badge-progress/BadgeProgressTab.scss new file mode 100644 index 0000000000..0537e25ea0 --- /dev/null +++ b/src/course-home/badges-tab/badge-progress/BadgeProgressTab.scss @@ -0,0 +1,62 @@ + +.badge-tabs-navigation { + + border-bottom: solid 3px theme-color('dark', 400); + background-color: theme-color('gray', 200); + + .badge-nav-tabs { + + margin: 0 20px 0 20px; + + .dropdown-menu { + .nav-link { + display: block; + } + } + .nav-link { + + display: inline-block; + + &.active { + background-color: theme-color('brand', 200); + } + + &.disabled { + &:hover, + &:focus, + &.active { + background-color: none !important; + color: theme-color('light', 400); + } + } + + &:hover, + &:focus { + background-color: theme-color('brand', 400); + color: white; + } + + } + + } + + .logo { + display: block; + box-sizing: content-box; + position: relative; + top: .10em; + height: 1.75rem; + margin-right: 1rem; + margin-top: 0.5rem; + img { + display: block; + height: 100%; + } + } + +} + +// Import badge-progress-specific sass files +@import './banner/index.scss'; +@import './card/index.scss'; +@import './course-list/index.scss'; diff --git a/src/course-home/badges-tab/badge-progress/BadgeProgressBanner.jsx b/src/course-home/badges-tab/badge-progress/banner/BadgeProgressBanner.jsx similarity index 100% rename from src/course-home/badges-tab/badge-progress/BadgeProgressBanner.jsx rename to src/course-home/badges-tab/badge-progress/banner/BadgeProgressBanner.jsx diff --git a/src/course-home/badges-tab/badge-progress/banner/index.scss b/src/course-home/badges-tab/badge-progress/banner/index.scss new file mode 100644 index 0000000000..26ed245a3a --- /dev/null +++ b/src/course-home/badges-tab/badge-progress/banner/index.scss @@ -0,0 +1,25 @@ + +/* BadgeProgressBanner + --------------------------------------- */ + +.learningpath-empty { + + .learningpath-empty-header { + + span { + margin-right: 10px; + } + + } + } + + .learningpath { + background: #f2f2f2; + + h2 { + font-weight: lighter; + font-size: 30px; + } + + } + \ No newline at end of file diff --git a/src/course-home/badges-tab/badge-progress/card/BadgeProgressCard.jsx b/src/course-home/badges-tab/badge-progress/card/BadgeProgressCard.jsx new file mode 100644 index 0000000000..228bb0b493 --- /dev/null +++ b/src/course-home/badges-tab/badge-progress/card/BadgeProgressCard.jsx @@ -0,0 +1,110 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { isEmptyObject } from '../../../../utils/empty'; +import BadgeProgressCardStatus from './BadgeProgressCardStatus'; +// import ProgressDetails from '../ProgressDetails'; + +const BadgeProgressCard = (props) => { + const { data, minimal } = props; + + const isProgressComplete = () => { + if (isEmptyObject(data.assertion)) { + return false; + } + return data.assertion.imageUrl.length > 0; + }; + + const getBadgeImage = () => { + const { assertionUrl } = data.assertion; + + return ( + <> + {assertionUrl && ( + /* */ +
Assertion Url
+ )} + {!assertionUrl && ( + {data.badgeClass.displayName} + )} + + ); + }; + + return ( + <> + {minimal && ( +
+
+ {getBadgeImage('minimal')} +
+
+ )} + {!minimal && ( +
+
+
+ +
+
+ {getBadgeImage()} + +
+
{data.badgeClass.displayName}
+
+
+
+
+ )} + + ); +}; + +BadgeProgressCard.defaultProps = { + minimal: '', +}; + +BadgeProgressCard.propTypes = { + data: PropTypes.shape({ + courseId: PropTypes.string, + blockId: PropTypes.string, + blockDisplayName: PropTypes.string, + blockOrder: PropTypes.number, + eventType: PropTypes.string, + badgeClass: PropTypes.shape({ + slug: PropTypes.string, + issuingComponent: PropTypes.string, + displayName: PropTypes.string, + courseId: PropTypes.string, + description: PropTypes.string, + criteria: PropTypes.string, + image: PropTypes.string, + }), + assertion: PropTypes.shape({ + issuedOn: PropTypes.string, + expires: PropTypes.string, + revoked: PropTypes.bool, + imageUrl: PropTypes.string, + assertionUrl: PropTypes.string, + recipient: PropTypes.shape({ + plaintextIdentity: PropTypes.string, + }), + issuer: PropTypes.shape({ + entityType: PropTypes.string, + entityId: PropTypes.string, + openBadgeId: PropTypes.string, + name: PropTypes.string, + image: PropTypes.string, + email: PropTypes.string, + description: PropTypes.string, + url: PropTypes.string, + }), + }), + }).isRequired, + minimal: PropTypes.string, +}; + +export default BadgeProgressCard; diff --git a/src/course-home/badges-tab/badge-progress/card/BadgeProgressCardStatus.jsx b/src/course-home/badges-tab/badge-progress/card/BadgeProgressCardStatus.jsx new file mode 100644 index 0000000000..762cf71d91 --- /dev/null +++ b/src/course-home/badges-tab/badge-progress/card/BadgeProgressCardStatus.jsx @@ -0,0 +1,42 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faCheckCircle } from '@fortawesome/free-solid-svg-icons'; +import { faCircle } from '@fortawesome/free-regular-svg-icons'; + +const BadgeProgressCardStatus = (props) => { + const { status, title } = props; + + const getStatusIndicator = () => { + const indicatorIcon = (status ? faCheckCircle : faCircle); + const indicatorStatus = (status ? 'complete' : 'incomplete'); + return ( + + ); + }; + + const getStatusTitle = () => { + const stripNumPrefix = title.replace(/[0-9]+\./g, ''); + return ( +
+ {stripNumPrefix} +
+ ); + }; + + return ( +
+ {getStatusIndicator()} + {getStatusTitle()} +
+ ); +}; + +BadgeProgressCardStatus.propTypes = { + status: PropTypes.bool.isRequired, + title: PropTypes.string.isRequired, +}; + +export default BadgeProgressCardStatus; diff --git a/src/course-home/badges-tab/badge-progress/card/index.scss b/src/course-home/badges-tab/badge-progress/card/index.scss new file mode 100644 index 0000000000..a930198162 --- /dev/null +++ b/src/course-home/badges-tab/badge-progress/card/index.scss @@ -0,0 +1,172 @@ +// https://stackoverflow.com/questions/11989546/wrap-a-text-within-only-two-lines-inside-div +@mixin multiLineEllipsis($lineHeight: 1.2rem, $lineCount: 2, $bgColor: white, $padding-right: 0.3125rem, $width: 1rem, $ellipsis-right: 0) { + overflow: hidden; /* hide text if it is more than $lineCount lines */ + position: relative; /* for set '...' in absolute position */ + line-height: $lineHeight; /* use this value to count block height */ + max-height: $lineHeight * $lineCount; /* max-height = line-height * lines max number */ + padding-right: $padding-right; /* place for '...' */ + white-space: normal; /* overwrite any white-space styles */ + // word-break: break-all; /* will break each letter in word */ + text-overflow: ellipsis; /* show ellipsis if text is broken */ + + &::before { + content: '...'; /* create the '...'' points in the end */ + position: absolute; + right: $ellipsis-right; + bottom: 0; + } + + &::after { + content: ''; /* hide '...'' if we have text, which is less than or equal to max lines and add $bgColor */ + position: absolute; + right: 0; + width: $width; + height: 1rem * $lineCount; + margin-top: 0.2rem; + background: $bgColor; /* because we are cutting off the diff we need to add the color back. */ + } +} + +/* BadgeProgressCard + --------------------------------------- */ +.card { + //border-radius: 1rem; + border: none; + + .card-header { + padding: 0.5rem 0.5rem; + background-color: transparent; + border-radius: 5px; + box-shadow: 1px 1px 5px 0 rgba(46,61,73,.2); + font-weight: bold; + height: 85px; /* 120px */ + overflow: hidden; + line-height: 1.2; + } + + .card-badge { + + border-radius: 5px; + box-shadow: 1px 1px 5px 0 rgba(46,61,73,.2); + height: 100%; + + .card-img-top { + width: 100%; + height: 12vw; + object-fit: contain; + margin: 5px 0px; + + @media only screen and (min-width: 320px) { + height: 50vw; + } + @media only screen and (min-width: 768px) { + height: 16vw; + } + @media only screen and (min-width: 1024px) { + height: 12vw; + } + @media only screen and (min-width: 1200px) { + height: 10vw; + } + @media only screen and (min-width: 1400px) { + height: 8vw; + } + @media only screen and (min-width: 2560px) { + height: 5vw; + } + } + + .minimal { + height: 70px; + } + + .card-body { + padding: .75rem 1.25rem; + border-top: 1px solid #8080805c; + // height: 40px; + text-align: center; + background-color: rgba(237,237,237,1); + + .card-title { + font-size: 0.90rem; + // text-overflow: ellipsis; + // overflow: hidden; + // white-space: nowrap; + @include multiLineEllipsis($lineCount: 2, $bgColor: rgba(237,237,237,1)); + padding: 0px 10px !important; + min-height: 40px; + } + + } + + } + + } + + .asserted { + color: rgba(0,0,0,1); + + &:hover { + background-color: rgba(13,129,1,0.2); + } + } + + .not-asserted { + /* https://stackoverflow.com/questions/35374021/css-grayscale-filter-changing-logos-to-different-shades */ + + //color: rgba(132, 132, 132, 0.15); + -webkit-filter: grayscale(100%) brightness(60%) contrast(100%); /* Safari 6.0 - 9.0 */ + filter: grayscale(100%) brightness(60%) contrast(100%); + filter: alpha(opacity=50); + opacity: 0.50 !important; + //filter: alpha(opacity=15); + } + + + +/* BadgeProgressCardStatus + --------------------------------------- */ + +.card-status { + position: relative; + top: -5px; + display: flex; + align-items: center; + margin: 0px 10px; + + .card-status-icon { + display: inline-block; + vertical-align: middle; + width: 15%; + margin: 16px 16px 16px 0px; + //position: relative; + //top: 5px; + + &.complete { + color: rgb(0, 129, 0); + } + + &.incomplete { + color: rgb(165, 165, 165); + } + + } + + /* + h3 { + max-width: 280px; + display: inline-block; + @include line-clamp(2,2); + } + */ + .card-status-title { + font-size: 14px; + text-align: left; + display: inline-block; + vertical-align: middle; + text-transform: uppercase; + @include multiLineEllipsis($lineCount: 3, $padding-right: 1rem) + } + + } + \ No newline at end of file diff --git a/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseList.jsx b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseList.jsx new file mode 100644 index 0000000000..2b2094aed6 --- /dev/null +++ b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseList.jsx @@ -0,0 +1,99 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Table } from '@edx/paragon'; + +import BadgeProgressCourseListItem from './BadgeProgressCourseListItem'; + +const BadgeProgressCourseList = (props) => { + const { data, headings } = props; + + const getProgressCourseListData = () => { + const results = []; + + data.forEach((item) => { + const learnerData = { + username: `'${item.userName}' (${item.email})`, + }; + + item.progress.forEach((i) => { + learnerData[i.blockId] = ( + + ); + }); + + results.push(learnerData); + }); + + return results; + }; + + // eslint-disable-next-line no-unused-vars + const sortProgressByCourseBlockOrder = (progress) => { + if (progress) { + return progress.sort((a, b) => { + if (a.block_order < b.block_order) { return -1; } + if (a.block_order > b.block_order) { return 1; } + return 0; + }); + } + return 0; + }; + + return ( + <> +
+ + + + ); +}; + +BadgeProgressCourseList.propTypes = { + data: PropTypes.arrayOf(PropTypes.shape({ + courseId: PropTypes.string, + blockId: PropTypes.string, + blockDisplayName: PropTypes.string, + blockOrder: PropTypes.number, + eventType: PropTypes.string, + badgeClass: PropTypes.shape({ + slug: PropTypes.string, + issuingComponent: PropTypes.string, + displayName: PropTypes.string, + courseId: PropTypes.string, + description: PropTypes.string, + criteria: PropTypes.string, + image: PropTypes.string, + }), + assertion: PropTypes.shape({ + issuedOn: PropTypes.string, + expires: PropTypes.string, + revoked: PropTypes.bool, + imageUrl: PropTypes.string, + assertionUrl: PropTypes.string, + recipient: PropTypes.shape({ + plaintextIdentity: PropTypes.string, + }), + issuer: PropTypes.shape({ + entityType: PropTypes.string, + entityId: PropTypes.string, + openBadgeId: PropTypes.string, + name: PropTypes.string, + image: PropTypes.string, + email: PropTypes.string, + description: PropTypes.string, + url: PropTypes.string, + }), + }), + })).isRequired, + headings: PropTypes.arrayOf(PropTypes.shape({ + label: PropTypes.string, + key: PropTypes.string, + })).isRequired, +}; + +export default BadgeProgressCourseList; diff --git a/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListItem.jsx b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListItem.jsx new file mode 100644 index 0000000000..90d27c22ee --- /dev/null +++ b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListItem.jsx @@ -0,0 +1,48 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import BadgeProgressCard from '../card/BadgeProgressCard'; + +const BadgeProgressCourseListItem = ({ badge }) => ( + +); + +BadgeProgressCourseListItem.propTypes = { + badge: PropTypes.shape({ + courseId: PropTypes.string, + blockId: PropTypes.string, + blockDisplayName: PropTypes.string, + blockOrder: PropTypes.number, + eventType: PropTypes.string, + badgeClass: PropTypes.shape({ + slug: PropTypes.string, + issuingComponent: PropTypes.string, + displayName: PropTypes.string, + courseId: PropTypes.string, + description: PropTypes.string, + criteria: PropTypes.string, + image: PropTypes.string, + }), + assertion: PropTypes.shape({ + issuedOn: PropTypes.string, + expires: PropTypes.string, + revoked: PropTypes.bool, + imageUrl: PropTypes.string, + assertionUrl: PropTypes.string, + recipient: PropTypes.shape({ + plaintextIdentity: PropTypes.string, + }), + issuer: PropTypes.shape({ + entityType: PropTypes.string, + entityId: PropTypes.string, + openBadgeId: PropTypes.string, + name: PropTypes.string, + image: PropTypes.string, + email: PropTypes.string, + description: PropTypes.string, + url: PropTypes.string, + }), + }), + }).isRequired, +}; + +export default BadgeProgressCourseListItem; diff --git a/src/course-home/badges-tab/badge-progress/course-list/index.scss b/src/course-home/badges-tab/badge-progress/course-list/index.scss new file mode 100644 index 0000000000..3bfce50379 --- /dev/null +++ b/src/course-home/badges-tab/badge-progress/course-list/index.scss @@ -0,0 +1,87 @@ + +/* BadgeProgressCourseList + --------------------------------------- */ +.badge-progress-course-list { + + thead { + border-bottom: solid 3px theme-color('dark', 400); + background-color: theme-color('gray', 200); + } + +} + +.thead-overflow-hidden { + + thead { + + th { + overflow: hidden visible; + overflow: -moz-hidden-unscrollable; + text-overflow: clip; + text-transform: uppercase; + + /* Controls the max-width of the badges in table view using table-responsive as well. + This is in change of using 'hasFixedColumnWidths' for the table. + */ + + @media only screen and (min-width: 320px) { + max-width: 100px; + } + @media only screen and (min-width: 768px) { + max-width: 100px; + } + @media only screen and (min-width: 1024px) { + max-width: 100px; + } + @media only screen and (min-width: 1200px) { + max-width: 140px; + } + @media only screen and (min-width: 1400px) { + max-width: 140px; + } + @media only screen and (min-width: 2560px) { + max-width: 140px; + } + } + + } + +} + +/* BadgeProgressCourseListItem + --------------------------------------- */ +.progress-list-item { + + .badge-name { + + min-width: 400px; + + img { + max-height: 50px; + max-width: 50px; + } + + span { + display: inline-block; + margin-left: 20px; + } + } + +} + +.asserted { + color: rgba(0,0,0,1); +} + +.not-asserted { + color: rgba(0,0,0,0.25); + + .badge-name { + + img { + opacity: 0.25; + filter: alpha(opacity=25); + } + } +} + diff --git a/src/course-home/badges-tab/badge-progress/index.js b/src/course-home/badges-tab/badge-progress/index.js index 4b7595a009..60bd7d3ca6 100644 --- a/src/course-home/badges-tab/badge-progress/index.js +++ b/src/course-home/badges-tab/badge-progress/index.js @@ -1,2 +1,5 @@ /* eslint-disable import/prefer-default-export */ -export { default as BadgeProgressBanner } from './BadgeProgressBanner'; +export { default as BadgeProgressBanner } from './banner/BadgeProgressBanner'; +export { default as BadgeProgressCard } from './card/BadgeProgressCard'; +export { default as BadgeProgressCourseList } from './course-list/BadgeProgressCourseList'; +export { default as BadgeProgressCourseListItem } from './course-list/BadgeProgressCourseListItem'; diff --git a/src/course-home/badges-tab/utils.js b/src/course-home/badges-tab/utils.js new file mode 100644 index 0000000000..11d43491a8 --- /dev/null +++ b/src/course-home/badges-tab/utils.js @@ -0,0 +1,43 @@ +/* eslint-disable import/prefer-default-export */ + +const headingMapper = (filterKey, data) => { + // eslint-disable-next-line no-unused-vars + const dataSortable = data.slice(); + + function all(entry) { + if (entry) { + const results = [{ + label: 'Student', + key: 'username', + width: 'col-2', + }]; + + const progressHeadings = entry.progress + .filter(blocks => blocks.blockDisplayName) + .map(b => ({ + label: b.blockDisplayName.replace(/[0-9]+\./g, ''), + key: b.blockId, + width: 'col-1', + })); + + return results.concat(progressHeadings); + } + return []; + } + + // Todo: Need to implement this. + // eslint-disable-next-line no-unused-vars + function some(entry) { + return [{ + label: '', + key: '', + width: 'col-1', + }]; + } + + return filterKey === 'All' ? all : some; +}; + +export { + headingMapper, +}; diff --git a/src/index.scss b/src/index.scss index e12a8baf4d..f0d2fcfef0 100755 --- a/src/index.scss +++ b/src/index.scss @@ -54,63 +54,6 @@ } } -.badge-tabs-navigation { - - border-bottom: solid 3px theme-color('dark', 400); - background-color: theme-color('gray', 200); - - .badge-nav-tabs { - - margin: 0 20px 0 20px; - - .dropdown-menu { - .nav-link { - display: block; - } - } - .nav-link { - - display: inline-block; - - &.active { - background-color: theme-color('brand', 200); - } - - &.disabled { - &:hover, - &:focus, - &.active { - background-color: none !important; - color: theme-color('light', 400); - } - } - - &:hover, - &:focus { - background-color: theme-color('brand', 400); - color: white; - } - - } - - } - - .logo { - display: block; - box-sizing: content-box; - position: relative; - top: .10em; - height: 1.75rem; - margin-right: 1rem; - margin-top: 0.5rem; - img { - display: block; - height: 100%; - } - } - -} - .nav-underline-tabs { margin: 0 0 -1px; @@ -422,6 +365,7 @@ @import "shared/streak-celebration/StreakCelebrationModal.scss"; @import "courseware/course/content-tools/calculator/calculator.scss"; @import "courseware/course/content-tools/contentTools.scss"; +@import 'course-home/badges-tab/badge-progress/BadgeProgressTab.scss'; @import "course-home/dates-tab/timeline/Day.scss"; @import "generic/upgrade-notification/UpgradeNotification.scss"; @import "course-home/outline-tab/widgets/ProctoringInfoPanel.scss"; diff --git a/src/utils/empty.js b/src/utils/empty.js new file mode 100644 index 0000000000..16351140ec --- /dev/null +++ b/src/utils/empty.js @@ -0,0 +1,10 @@ +/* eslint-disable import/prefer-default-export */ + +export const isEmptyObject = (obj) => { + Object.keys(obj).forEach(k => { + if (Object.prototype.hasOwnProperty.call(obj, k)) { + return false; + } + return true; + }); +}; From ff019afd8aee3a8555b14c7dc4605dcbd9b6ec33 Mon Sep 17 00:00:00 2001 From: Zachary Trabookis Date: Wed, 9 Mar 2022 17:17:28 -0500 Subject: [PATCH 05/17] feat(badges): Added in Badge Progress Card Details. --- package-lock.json | 631 +++++++++++++++++- package.json | 6 + .../badges-tab/BadgeProgressTab.jsx | 16 +- .../logo-badgr-black.svg} | 0 .../badges-tab/assets/logo-badgr-white.svg | 10 + .../badge-header/BadgeTabsNavigation.jsx | 23 +- .../badge-progress/card/BadgeProgressCard.jsx | 17 +- .../card/BadgeProgressCardDetailsModal.jsx | 221 ++++++ .../card/BadgeProgressCardStatus.jsx | 55 +- .../badges-tab/badge-progress/card/index.scss | 252 +++++++ .../course-list/BadgeProgressCourseList.jsx | 3 +- .../BadgeProgressCourseListItem.jsx | 1 + src/course-home/badges-tab/logos.jsx | 24 + src/course-home/badges-tab/messages.js | 16 + src/tab-page/TabPage.jsx | 23 +- 15 files changed, 1251 insertions(+), 47 deletions(-) rename src/course-home/badges-tab/{badge-header/assets/logo-badgr-light.svg => assets/logo-badgr-black.svg} (100%) create mode 100644 src/course-home/badges-tab/assets/logo-badgr-white.svg create mode 100644 src/course-home/badges-tab/badge-progress/card/BadgeProgressCardDetailsModal.jsx create mode 100644 src/course-home/badges-tab/logos.jsx create mode 100644 src/course-home/badges-tab/messages.js diff --git a/package-lock.json b/package-lock.json index da875cc170..5dcedfbf68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5762,6 +5762,14 @@ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", "integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==" }, + "@types/debug": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", + "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", + "requires": { + "@types/ms": "*" + } + }, "@types/eslint": { "version": "8.2.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.2.1.tgz", @@ -5837,6 +5845,14 @@ "@types/node": "*" } }, + "@types/hast": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz", + "integrity": "sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==", + "requires": { + "@types/unist": "*" + } + }, "@types/hoist-non-react-statics": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", @@ -5909,6 +5925,19 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "@types/mdast": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz", + "integrity": "sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==", + "requires": { + "@types/unist": "*" + } + }, + "@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==" + }, "@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -5920,6 +5949,11 @@ "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", "dev": true }, + "@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" + }, "@types/needle": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/@types/needle/-/needle-2.5.2.tgz", @@ -6121,6 +6155,11 @@ "source-map": "^0.6.1" } }, + "@types/unist": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", + "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==" + }, "@types/warning": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz", @@ -7270,6 +7309,11 @@ } } }, + "bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==" + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -8177,6 +8221,11 @@ "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true }, + "character-entities": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.1.tgz", + "integrity": "sha512-OzmutCf2Kmc+6DrFrrPS8/tDh2+DpnrfzdICHWhcVC9eOd0N1PXmQEE1a8iM4IziIAG+8tmTq3K+oo0ubH6RRQ==" + }, "chardet": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", @@ -8548,6 +8597,11 @@ "delayed-stream": "~1.0.0" } }, + "comma-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.2.tgz", + "integrity": "sha512-G5yTt3KQN4Yn7Yk4ed73hlZ1evrFKXeUW3086p3PRFNp7m2vIjI6Pg+Kgb+oyzhd9F2qdcoj67+y3SdxL5XWsg==" + }, "commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -9106,6 +9160,14 @@ "integrity": "sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw==", "dev": true }, + "decode-named-character-reference": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.1.tgz", + "integrity": "sha512-YV/0HQHreRwKb7uBopyIkLG17jG6Sv2qUchk9qSoVJ2f+flwRsPNBO0hAnjt6mTNYUT+vw9Gy2ihXg4sUWPi2w==", + "requires": { + "character-entities": "^2.0.0" + } + }, "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", @@ -9521,6 +9583,11 @@ "resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz", "integrity": "sha1-PvqHMj67hj5mls67AILUj/PW96E=" }, + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==" + }, "diff-sequences": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", @@ -10826,8 +10893,7 @@ "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "extend-shallow": { "version": "3.0.2", @@ -12004,6 +12070,11 @@ } } }, + "hast-util-whitespace": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.0.tgz", + "integrity": "sha512-Pkw+xBHuV6xFeJprJe2BBEoDV+AvQySaz3pPDRUs5PNZEMQjpXJJueqrpcHIXxnWTcAGi/UOCgVShlkY6kLoqg==" + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -12936,6 +13007,11 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, + "inline-style-parser": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", + "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" + }, "inquirer": { "version": "7.3.3", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", @@ -17197,12 +17273,80 @@ "is-buffer": "~1.1.6" } }, + "mdast-util-definitions": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.0.tgz", + "integrity": "sha512-5hcR7FL2EuZ4q6lLMUK5w4lHT2H3vqL9quPvYZ/Ku5iifrirfMHiGdhxdXMUbUkDmz5I+TYMd7nbaxUhbQkfpQ==", + "requires": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "unist-util-visit": "^3.0.0" + }, + "dependencies": { + "unist-util-visit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-3.1.0.tgz", + "integrity": "sha512-Szoh+R/Ll68QWAyQyZZpQzZQm2UPbxibDvaY8Xc9SUtYgPsDzx5AWSk++UUt2hJuow8mvwR+rG+LQLw+KsuAKA==", + "requires": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^4.0.0" + } + } + } + }, + "mdast-util-from-markdown": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.2.0.tgz", + "integrity": "sha512-iZJyyvKD1+K7QX1b5jXdE7Sc5dtoTry1vzV28UZZe8Z1xVnB/czKntJ7ZAkG0tANqRnBF6p3p7GpU1y19DTf2Q==", + "requires": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + } + }, + "mdast-util-to-hast": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.1.1.tgz", + "integrity": "sha512-qE09zD6ylVP14jV4mjLIhDBOrpFdShHZcEsYvvKGABlr9mGbV7mTlRWdoFxL/EYSTNDiC9GZXy7y8Shgb9Dtzw==", + "requires": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "@types/mdurl": "^1.0.0", + "mdast-util-definitions": "^5.0.0", + "mdurl": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "unist-builder": "^3.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + } + }, + "mdast-util-to-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz", + "integrity": "sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA==" + }, "mdn-data": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", "dev": true }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=" + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -17379,6 +17523,233 @@ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" }, + "micromark": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.0.10.tgz", + "integrity": "sha512-ryTDy6UUunOXy2HPjelppgJ2sNfcPz1pLlMdA6Rz9jPzhLikWXv/irpWV/I2jd68Uhmny7hHxAlAhk4+vWggpg==", + "requires": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + }, + "dependencies": { + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "micromark-core-commonmark": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.0.6.tgz", + "integrity": "sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA==", + "requires": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "micromark-factory-destination": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz", + "integrity": "sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw==", + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-factory-label": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.0.2.tgz", + "integrity": "sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg==", + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "micromark-factory-space": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.0.0.tgz", + "integrity": "sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew==", + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-factory-title": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.0.2.tgz", + "integrity": "sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A==", + "requires": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "micromark-factory-whitespace": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.0.0.tgz", + "integrity": "sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==", + "requires": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-util-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.1.0.tgz", + "integrity": "sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg==", + "requires": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-util-chunked": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.0.0.tgz", + "integrity": "sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==", + "requires": { + "micromark-util-symbol": "^1.0.0" + } + }, + "micromark-util-classify-character": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.0.0.tgz", + "integrity": "sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==", + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-util-combine-extensions": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.0.0.tgz", + "integrity": "sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA==", + "requires": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-util-decode-numeric-character-reference": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.0.0.tgz", + "integrity": "sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==", + "requires": { + "micromark-util-symbol": "^1.0.0" + } + }, + "micromark-util-decode-string": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.0.2.tgz", + "integrity": "sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q==", + "requires": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "micromark-util-encode": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.0.1.tgz", + "integrity": "sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA==" + }, + "micromark-util-html-tag-name": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.0.0.tgz", + "integrity": "sha512-NenEKIshW2ZI/ERv9HtFNsrn3llSPZtY337LID/24WeLqMzeZhBEE6BQ0vS2ZBjshm5n40chKtJ3qjAbVV8S0g==" + }, + "micromark-util-normalize-identifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.0.0.tgz", + "integrity": "sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg==", + "requires": { + "micromark-util-symbol": "^1.0.0" + } + }, + "micromark-util-resolve-all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.0.0.tgz", + "integrity": "sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw==", + "requires": { + "micromark-util-types": "^1.0.0" + } + }, + "micromark-util-sanitize-uri": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.0.0.tgz", + "integrity": "sha512-cCxvBKlmac4rxCGx6ejlIviRaMKZc0fWm5HdCHEeDWRSkn44l6NdYVRyU+0nT1XC72EQJMZV8IPHF+jTr56lAg==", + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "micromark-util-subtokenize": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.0.2.tgz", + "integrity": "sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA==", + "requires": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "micromark-util-symbol": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.0.1.tgz", + "integrity": "sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ==" + }, + "micromark-util-types": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.0.2.tgz", + "integrity": "sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w==" + }, "micromatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", @@ -17406,14 +17777,22 @@ "mime-db": { "version": "1.47.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz", - "integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==" + "integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==", + "dev": true }, "mime-types": { - "version": "2.1.30", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.30.tgz", - "integrity": "sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==", + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", "requires": { - "mime-db": "1.47.0" + "mime-db": "1.51.0" + }, + "dependencies": { + "mime-db": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==" + } } }, "mimic-fn": { @@ -17520,6 +17899,19 @@ "minimist": "^1.2.5" } }, + "moment": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", + "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" + }, + "moment-timezone": { + "version": "0.5.34", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.34.tgz", + "integrity": "sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==", + "requires": { + "moment": ">= 2.9.0" + } + }, "mozjpeg": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/mozjpeg/-/mozjpeg-7.1.1.tgz", @@ -19194,6 +19586,11 @@ "warning": "^4.0.0" } }, + "property-information": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.1.1.tgz", + "integrity": "sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w==" + }, "proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -19660,6 +20057,14 @@ "scheduler": "^0.20.2" } }, + "react-download-link": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-download-link/-/react-download-link-2.3.0.tgz", + "integrity": "sha512-0aoj2DJFBfiD9jtdIn+WAseO1GSYmgkB5y5Ljt3DeC7j1RUlx0rR5y4S+wZwdxGhvgFogCzyh5Pa4W4YE+Pg/Q==", + "requires": { + "prop-types": "^15.6.0" + } + }, "react-error-overlay": { "version": "6.0.10", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.10.tgz", @@ -19731,6 +20136,39 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "react-markdown": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-8.0.0.tgz", + "integrity": "sha512-qbrWpLny6Ef2xHqnYqtot948LXP+4FtC+MWIuaN1kvSnowM+r1qEeEHpSaU0TDBOisQuj+Qe6eFY15cNL3gLAw==", + "requires": { + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^2.0.0", + "prop-types": "^15.0.0", + "property-information": "^6.0.0", + "react-is": "^17.0.0", + "remark-parse": "^10.0.0", + "remark-rehype": "^10.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-object": "^0.3.0", + "unified": "^10.0.0", + "unist-util-visit": "^4.0.0", + "vfile": "^5.0.0" + }, + "dependencies": { + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + } + } + }, + "react-moment": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/react-moment/-/react-moment-1.1.1.tgz", + "integrity": "sha512-WjwvxBSnmLMRcU33do0KixDB+9vP3e84eCse+rd+HNklAMNWyRgZTDEQlay/qK6lcXFPRuEIASJTpEt6pyK7Ww==" + }, "react-overlays": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.1.1.tgz", @@ -20168,6 +20606,27 @@ "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", "dev": true }, + "remark-parse": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.1.tgz", + "integrity": "sha512-1fUyHr2jLsVOkhbvPRBJ5zTKZZyD6yZzYaWCS6BPBdQ8vEMBCH+9zNCDA6tET/zHCi/jLqjCWtlJZUPk+DbnFw==", + "requires": { + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "unified": "^10.0.0" + } + }, + "remark-rehype": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-10.1.0.tgz", + "integrity": "sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==", + "requires": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-to-hast": "^12.1.0", + "unified": "^10.0.0" + } + }, "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -20524,6 +20983,14 @@ } } }, + "sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "requires": { + "mri": "^1.1.0" + } + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -21263,6 +21730,11 @@ "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", "dev": true }, + "space-separated-tokens": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.1.tgz", + "integrity": "sha512-ekwEbFp5aqSPKaqeY1PGrlGQxPNaq+Cnx4+bE2D8sciBQrHpbwoBbawqTN2+6jPs9IdWxxiUcN0K2pkczD3zmw==" + }, "spawn-sync": { "version": "1.0.15", "resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz", @@ -21693,6 +22165,14 @@ "schema-utils": "^3.0.0" } }, + "style-to-object": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz", + "integrity": "sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==", + "requires": { + "inline-style-parser": "0.1.1" + } + }, "stylehacks": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.0.1.tgz", @@ -22526,6 +23006,11 @@ "escape-string-regexp": "^1.0.2" } }, + "trough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz", + "integrity": "sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==" + }, "truncate-html": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/truncate-html/-/truncate-html-1.0.4.tgz", @@ -22700,6 +23185,32 @@ "integrity": "sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ==", "dev": true }, + "unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "requires": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "dependencies": { + "is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==" + }, + "is-plain-obj": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.0.0.tgz", + "integrity": "sha512-NXRbBtUdBioI73y/HmOhogw/U5msYPC9DAtGkJXeFcFWSFZw0mCUsPxk/snTuJHzNKA8kLBK4rH97RMB1BfCXw==" + } + } + }, "union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", @@ -22712,6 +23223,67 @@ "set-value": "^2.0.1" } }, + "unist-builder": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-3.0.0.tgz", + "integrity": "sha512-GFxmfEAa0vi9i5sd0R2kcrI9ks0r82NasRq5QHh2ysGngrc6GiqD5CDf1FjPenY4vApmFASBIIlk/jj5J5YbmQ==", + "requires": { + "@types/unist": "^2.0.0" + } + }, + "unist-util-generated": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.0.tgz", + "integrity": "sha512-TiWE6DVtVe7Ye2QxOVW9kqybs6cZexNwTwSMVgkfjEReqy/xwGpAXb99OxktoWwmL+Z+Epb0Dn8/GNDYP1wnUw==" + }, + "unist-util-is": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.1.1.tgz", + "integrity": "sha512-F5CZ68eYzuSvJjGhCLPL3cYx45IxkqXSetCcRgUXtbcm50X2L9oOWQlfUfDdAf+6Pd27YDblBfdtmsThXmwpbQ==" + }, + "unist-util-position": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.1.tgz", + "integrity": "sha512-mgy/zI9fQ2HlbOtTdr2w9lhVaiFUHWQnZrFF2EUoVOqtAUdzqMtNiD99qA5a1IcjWVR8O6aVYE9u7Z2z1v0SQA==" + }, + "unist-util-stringify-position": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.2.tgz", + "integrity": "sha512-7A6eiDCs9UtjcwZOcCpM4aPII3bAAGv13E96IkawkOAW0OhH+yRxtY0lzo8KiHpzEMfH7Q+FizUmwp8Iqy5EWg==", + "requires": { + "@types/unist": "^2.0.0" + } + }, + "unist-util-visit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.0.tgz", + "integrity": "sha512-n7lyhFKJfVZ9MnKtqbsqkQEk5P1KShj0+//V7mAcoI6bpbUjh3C/OG8HVD+pBihfh6Ovl01m8dkcv9HNqYajmQ==", + "requires": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.0.0" + }, + "dependencies": { + "unist-util-visit-parents": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.0.tgz", + "integrity": "sha512-y+QVLcY5eR/YVpqDsLf/xh9R3Q2Y4HxkZTp7ViLDU6WtJCEcPmRzW1gpdWDCDIqIlhuPDXOgttqPlykrHYDekg==", + "requires": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + } + } + } + }, + "unist-util-visit-parents": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-4.1.1.tgz", + "integrity": "sha512-1xAFJXAKpnnJl8G7K5KgU7FY55y3GcLIXqkzUj5QF/QVP7biUm0K0O2oqVkYsdjzJKifYeWn9+o6piAK2hGSHw==", + "requires": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + } + }, "universal-cookie": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.4.tgz", @@ -23008,6 +23580,24 @@ "dev": true, "optional": true }, + "uvu": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.3.tgz", + "integrity": "sha512-brFwqA3FXzilmtnIyJ+CxdkInkY/i4ErvP7uV0DnUVxQcQ55reuHphorpF+tZoVHK2MniZ/VJzI7zJQoc9T9Yw==", + "requires": { + "dequal": "^2.0.0", + "diff": "^5.0.0", + "kleur": "^4.0.3", + "sade": "^1.7.3" + }, + "dependencies": { + "kleur": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.4.tgz", + "integrity": "sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA==" + } + } + }, "v8-compile-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", @@ -23053,6 +23643,33 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, + "vfile": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.2.tgz", + "integrity": "sha512-w0PLIugRY3Crkgw89TeMvHCzqCs/zpreR31hl4D92y6SOE07+bfJe+dK5Q2akwS+i/c801kzjoOr9gMcTe6IAA==", + "requires": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "dependencies": { + "is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==" + } + } + }, + "vfile-message": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.2.tgz", + "integrity": "sha512-QjSNP6Yxzyycd4SVOtmKKyTsSvClqBPJcd00Z0zuPj3hOIjg0rUPG6DbFGPvUKRgYyaIWLPKpuEclcuvb3H8qA==", + "requires": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + } + }, "w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", diff --git a/package.json b/package.json index 1d8b34c006..b77029645b 100644 --- a/package.json +++ b/package.json @@ -49,11 +49,17 @@ "core-js": "3.18.3", "js-cookie": "3.0.1", "lodash.camelcase": "4.3.0", + "mime-types": "^2.1.34", + "moment": "^2.29.1", + "moment-timezone": "^0.5.34", "prop-types": "15.7.2", "react": "17.0.2", "react-break": "1.3.2", "react-dom": "17.0.2", + "react-download-link": "^2.3.0", "react-helmet": "6.1.0", + "react-markdown": "^8.0.0", + "react-moment": "^1.1.1", "react-redux": "7.2.5", "react-router": "5.2.1", "react-router-dom": "5.3.0", diff --git a/src/course-home/badges-tab/BadgeProgressTab.jsx b/src/course-home/badges-tab/BadgeProgressTab.jsx index d5fba30bda..002cd81ecc 100644 --- a/src/course-home/badges-tab/BadgeProgressTab.jsx +++ b/src/course-home/badges-tab/BadgeProgressTab.jsx @@ -74,6 +74,15 @@ const renderBadgeProgress = () => { ); } + /* +
+
+ the user is {username} and they are enrolled as an {enrollmentMode} learner + {administrator + &&

the user is admin

} +
+
+ */ return ( <>
@@ -91,13 +100,6 @@ const renderBadgeProgress = () => { )}
- {/*
-
- the user is {username} and they are enrolled as an {enrollmentMode} learner - {administrator - &&

the user is admin

} -
-
*/} diff --git a/src/course-home/badges-tab/badge-header/assets/logo-badgr-light.svg b/src/course-home/badges-tab/assets/logo-badgr-black.svg similarity index 100% rename from src/course-home/badges-tab/badge-header/assets/logo-badgr-light.svg rename to src/course-home/badges-tab/assets/logo-badgr-black.svg diff --git a/src/course-home/badges-tab/assets/logo-badgr-white.svg b/src/course-home/badges-tab/assets/logo-badgr-white.svg new file mode 100644 index 0000000000..0bca86431b --- /dev/null +++ b/src/course-home/badges-tab/assets/logo-badgr-white.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/course-home/badges-tab/badge-header/BadgeTabsNavigation.jsx b/src/course-home/badges-tab/badge-header/BadgeTabsNavigation.jsx index ad8fcd6d45..143ccb86d3 100644 --- a/src/course-home/badges-tab/badge-header/BadgeTabsNavigation.jsx +++ b/src/course-home/badges-tab/badge-header/BadgeTabsNavigation.jsx @@ -6,27 +6,8 @@ import classNames from 'classnames'; import messages from './messages'; import Tabs from '../../../generic/tabs/Tabs'; - -import logo from './assets/logo-badgr-light.svg'; - -function LinkedLogo({ - href, - src, - alt, - ...attributes -}) { - return ( - - {alt} - - ); -} - -LinkedLogo.propTypes = { - href: PropTypes.string.isRequired, - src: PropTypes.string.isRequired, - alt: PropTypes.string.isRequired, -}; +import LinkedLogo from '../logos'; +import logo from '../assets/logo-badgr-black.svg'; function BadgeTabsNavigation({ activeTabSlug, className, intl, diff --git a/src/course-home/badges-tab/badge-progress/card/BadgeProgressCard.jsx b/src/course-home/badges-tab/badge-progress/card/BadgeProgressCard.jsx index 228bb0b493..85a2c0f63f 100644 --- a/src/course-home/badges-tab/badge-progress/card/BadgeProgressCard.jsx +++ b/src/course-home/badges-tab/badge-progress/card/BadgeProgressCard.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import { isEmptyObject } from '../../../../utils/empty'; import BadgeProgressCardStatus from './BadgeProgressCardStatus'; -// import ProgressDetails from '../ProgressDetails'; +import BadgeProgressCardDetailsModal from './BadgeProgressCardDetailsModal'; const BadgeProgressCard = (props) => { const { data, minimal } = props; @@ -15,17 +15,25 @@ const BadgeProgressCard = (props) => { return data.assertion.imageUrl.length > 0; }; + const getBadgeProgressCardDetails = (earned) => ( + <> + + + ); + const getBadgeImage = () => { const { assertionUrl } = data.assertion; return ( <> {assertionUrl && ( - /* */ -
Assertion Url
+ progress={data} + minimal={minimal} + badgeProgressCardStatus={getBadgeProgressCardDetails(data.assertion.issuedOn)} + /> )} {!assertionUrl && ( {data.badgeClass.displayName} @@ -85,6 +93,7 @@ BadgeProgressCard.propTypes = { }), assertion: PropTypes.shape({ issuedOn: PropTypes.string, + entityId: PropTypes.string, expires: PropTypes.string, revoked: PropTypes.bool, imageUrl: PropTypes.string, diff --git a/src/course-home/badges-tab/badge-progress/card/BadgeProgressCardDetailsModal.jsx b/src/course-home/badges-tab/badge-progress/card/BadgeProgressCardDetailsModal.jsx new file mode 100644 index 0000000000..4746a9d4df --- /dev/null +++ b/src/course-home/badges-tab/badge-progress/card/BadgeProgressCardDetailsModal.jsx @@ -0,0 +1,221 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import ReactMarkdown from 'react-markdown'; +import { + Button, + Collapsible, + Hyperlink, + Modal, +} from '@edx/paragon'; + +import LinkedLogo from '../../logos'; +import logo from '../../assets/logo-badgr-white.svg'; + +const BadgeProgressCardDetailsModal = (props) => { + const { + parentSelector, + progress, + minimal, + badgeProgressCardStatus, + } = props; + const [modalOpen, setModalOpen] = useState(false); + // const [modalModel, setModalModel] = useState([{}]); + + const getBadgrLogo = () => ( + + ); + + const openModal = () => { + setModalOpen(true); + // setModalModel([{}]); + }; + + const resetModalWrapperState = () => { + setModalOpen(false); + // setModalModel([{}]); + // this.button.focus(); + }; + + const redirectBackpack = () => window.open('https://badgr.com/recipient/badges', '_blank'); + + const renderModal = () => { + /* Todo: May consider going back to `src = progress.assertion.imageUrl` to + reflect actual image earned. I was not able to render + "http://example.com/image.png" because it produces a 404 error. + */ + const childElements = ( + {progress.badgeClass.displayName} + ); + + return ( + <> +
+
+ +
+ +
+
+
+

{progress.badgeClass.displayName}

+
+
+
+ {progress.badgeClass.description} +
+
+
+
+
+ {progress.badgeClass.displayName} + +
+
+ {progress.assertion.issuedOn && progress.blockDisplayName && ( +
+ {badgeProgressCardStatus} +
+ )} + {progress.assertion.recipient.plaintextIdentity && ( +
+

Recipient

+

{progress.assertion.recipient.plaintextIdentity}

+
+ )} + {progress.badgeClass.criteria && ( +
+

Criteria

+ {progress.badgeClass.criteria} +
+ )} + {progress.assertion.issuer && ( +
+

Issuer

+
    +
  • + {progress.assertion.issuer.name} + {progress.assertion.issuer.email} +
  • +
+
+ )} +
+
+
+ +
+
+

Share your Open Badge with Badgr

+

+ Your achievement has been recognized with an , a digital image file with information + embedded in it that uniquely identifies your accomplishments. +

+

+ Badgr is a service that creates and stores Open Badges and lets you share them with others. + To share your badge using Badgr, you can send a link to a web page about your badge to others. + You can also send the badge image file directly to others, and they can use a from Badgr to confirm your accomplishment. + For more options, you must first have a Badgr account. + You should have received an email the first time you received a badge with + instructions about creating a Badgr account. Once you have a Badgr account, you can organize + your badges in a Backpack and access tools to help share your badges on social media, embed + them in web pages, and more. +

+
+
+
+
+
+
    +
  1. Create a account, or to your existing account;
  2. +
  3. ; or
  4. +
  5. + and share it + directly with others. They can verify it's really yours at . +
  6. +
+
+
+ {getBadgrLogo()} +
+
+
+
+
+ + )} + parentSelector={parentSelector} + buttons={[]} + onClose={resetModalWrapperState} + /> + + ); + }; + + return renderModal(); +}; + +BadgeProgressCardDetailsModal.propTypes = { + parentSelector: PropTypes.string, + progress: PropTypes.shape({ + courseId: PropTypes.string, + blockId: PropTypes.string, + blockDisplayName: PropTypes.string, + blockOrder: PropTypes.number, + eventType: PropTypes.string, + badgeClass: PropTypes.shape({ + slug: PropTypes.string, + issuingComponent: PropTypes.string, + displayName: PropTypes.string, + courseId: PropTypes.string, + description: PropTypes.string, + criteria: PropTypes.string, + image: PropTypes.string, + }), + assertion: PropTypes.shape({ + issuedOn: PropTypes.string, + entityId: PropTypes.string, + expires: PropTypes.string, + revoked: PropTypes.bool, + imageUrl: PropTypes.string, + assertionUrl: PropTypes.string, + recipient: PropTypes.shape({ + plaintextIdentity: PropTypes.string, + }), + issuer: PropTypes.shape({ + entityType: PropTypes.string, + entityId: PropTypes.string, + openBadgeId: PropTypes.string, + name: PropTypes.string, + image: PropTypes.string, + email: PropTypes.string, + description: PropTypes.string, + url: PropTypes.string, + }), + }), + }).isRequired, + minimal: PropTypes.string, + badgeProgressCardStatus: PropTypes.oneOfType([PropTypes.object]).isRequired, +}; + +BadgeProgressCardDetailsModal.defaultProps = { + parentSelector: 'body', + minimal: '', +}; + +export default BadgeProgressCardDetailsModal; diff --git a/src/course-home/badges-tab/badge-progress/card/BadgeProgressCardStatus.jsx b/src/course-home/badges-tab/badge-progress/card/BadgeProgressCardStatus.jsx index 762cf71d91..844486e394 100644 --- a/src/course-home/badges-tab/badge-progress/card/BadgeProgressCardStatus.jsx +++ b/src/course-home/badges-tab/badge-progress/card/BadgeProgressCardStatus.jsx @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import Moment from 'react-moment'; import classNames from 'classnames'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -7,7 +8,31 @@ import { faCheckCircle } from '@fortawesome/free-solid-svg-icons'; import { faCircle } from '@fortawesome/free-regular-svg-icons'; const BadgeProgressCardStatus = (props) => { - const { status, title } = props; + const { status, title, earned } = props; + + const getStatusEarned = () => { + // Set the output format for every react-moment instance. + Moment.globalFormat = 'MMMM D, YYYY'; + + // Set the timezone for every instance. + // Moment.globalTimezone = 'America/Los_Angeles'; + + // Set the output timezone for local for every instance. + Moment.globalLocal = true; + + // Use a tag for every react-moment instance. + Moment.globalElement = 'span'; + + return ( + <> + {earned && ( +
+ Earned: {earned} +
+ )} + + ); + }; const getStatusIndicator = () => { const indicatorIcon = (status ? faCheckCircle : faCircle); @@ -27,16 +52,36 @@ const BadgeProgressCardStatus = (props) => { }; return ( -
- {getStatusIndicator()} - {getStatusTitle()} -
+ <> + {!earned && ( +
+ {getStatusIndicator()} + {getStatusTitle()} +
+ )} + {earned && ( +
+
+
+ {getStatusIndicator()} + {getStatusTitle()} + {getStatusEarned()} +
+
+
+ )} + ); }; +BadgeProgressCardStatus.defaultProps = { + earned: '', +}; + BadgeProgressCardStatus.propTypes = { status: PropTypes.bool.isRequired, title: PropTypes.string.isRequired, + earned: PropTypes.string, }; export default BadgeProgressCardStatus; diff --git a/src/course-home/badges-tab/badge-progress/card/index.scss b/src/course-home/badges-tab/badge-progress/card/index.scss index a930198162..ea23b55ef5 100644 --- a/src/course-home/badges-tab/badge-progress/card/index.scss +++ b/src/course-home/badges-tab/badge-progress/card/index.scss @@ -169,4 +169,256 @@ } } + + +/* BadgeProgressCardDetailsModal + --------------------------------------- */ + .modal-progress-details { + + .modal { + + .modal-dialog { + + min-width: 60%; + + .modal-content { + + padding: 15px; + + .modal-body { + + .progress-details { + + text-align: left; + + .progress-details-header { + + .progress-details-title { + + h2 { + font-size: 30px; + font-weight: normal; + } + + } + + .progress-details-description { + + p { + font-size: 16px; + } + + } + + } + + .progress-details-body { + + .progress-details-image { + + img { + max-width: 70%; + margin: 0px 40px; + } + + button { + max-width: 70%; + width: 100%; + margin: 5px 40px; + } + + } + + .progress-details-meta { + + font-size: 16px; + line-height: 8px; + + .progress-details-meta-earned, + .progress-details-meta-recipient, + .progress-details-meta-criteria, + .progress-details-meta-issuer { + + h3 { + color: #767676 !important; + font-size: 18px; + } + + margin-bottom: 20px; + + } + + .progress-details-meta-earned { + + .card-status { + position: relative; + top: -5px; + display: flex; + align-items: center; + margin: 0px 10px; + + .card-status-icon { + display: inline-block; + vertical-align: middle; + width: 15%; + margin: 16px 16px 16px 0px; + //position: relative; + //top: 5px; + + &.complete { + color: rgb(0, 129, 0); + } + + &.incomplete { + color: rgb(165, 165, 165); + } + + } + + /* + h3 { + max-width: 280px; + display: inline-block; + @include line-clamp(2,2); + } + */ + .card-status-title { + position: relative; + top: -8px; + font-size: 14px; + text-align: left; + display: inline-block; + vertical-align: middle; + text-transform: uppercase; + // @include multiLineEllipsis($lineCount: 3, $padding-right: 1rem) + } + + .card-status-earned { + display: block; + position: relative; + top: 12px; + left: -90px; + font-size: 14px; + font-weight: normal; + font-style: italic; + } + + } + + } + + .progress-details-meta-issuer { + + ul { + + li { + list-style: none; + + img { + width: 40px; + height: 40px; + } + } + + } + + } + + } + + } + + .progress-details-share { + + background-color: #f2f2f2; + + a { + font-weight: bold; + } + + .collapsible-trigger { + background-color: bisque; + } + + .progress-details-share-instructions { + + padding-bottom: 0px !important; + + .share-introduction { + + hr { + border-color: rgba(0,0,0,0.30); + } + + } + + } + + .progress-details-share-badgr-instructions { + + padding: 0px 10px 10px 10px !important; + + .badgr-instructions { + + ol { + padding-left: 20px; + list-style: none; + list-style-position: outside; + display: table; + margin: 1em 0; + margin-block-start: 1em; + margin-block-end: 1em; + margin-inline-start: 0px; + margin-inline-end: 0px; + padding-inline-start: 40px; + /*counter-reset: badgr-instr-counter;*/ + + li { + //list-style: none; + list-style-type: decimal; + //counter-increment: badgr-instr-counter; + + /* + &:before { + content: counter(badgr-instr-counter) ". "; + color: #444444; + font-weight: bold; + }*/ + } + + } + + } + + .badgr-image { + + display: inline-block; + background-color: #525dc7; + padding: 10px; + max-height: 120px; + + img { + width: 100%; + height: 100%; + position: relative; + left: 18px; + } + + } + + } + + } + + } + + } + + } + + } + + } + + } \ No newline at end of file diff --git a/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseList.jsx b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseList.jsx index 2b2094aed6..149e8fce16 100644 --- a/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseList.jsx +++ b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseList.jsx @@ -46,7 +46,7 @@ const BadgeProgressCourseList = (props) => { columns={headings} data={getProgressCourseListData()} rowHeaderColumnKey="username" - className={['badge-progress-course-list table-responsive thead-overflow-hidden']} + className="badge-progress-course-list table-responsive thead-overflow-hidden" />
@@ -71,6 +71,7 @@ BadgeProgressCourseList.propTypes = { }), assertion: PropTypes.shape({ issuedOn: PropTypes.string, + entityId: PropTypes.string, expires: PropTypes.string, revoked: PropTypes.bool, imageUrl: PropTypes.string, diff --git a/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListItem.jsx b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListItem.jsx index 90d27c22ee..aa62c4e302 100644 --- a/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListItem.jsx +++ b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListItem.jsx @@ -24,6 +24,7 @@ BadgeProgressCourseListItem.propTypes = { }), assertion: PropTypes.shape({ issuedOn: PropTypes.string, + entityId: PropTypes.string, expires: PropTypes.string, revoked: PropTypes.bool, imageUrl: PropTypes.string, diff --git a/src/course-home/badges-tab/logos.jsx b/src/course-home/badges-tab/logos.jsx new file mode 100644 index 0000000000..0d350fa583 --- /dev/null +++ b/src/course-home/badges-tab/logos.jsx @@ -0,0 +1,24 @@ + +import React from 'react'; +import PropTypes from 'prop-types'; + +function LinkedLogo({ + href, + src, + alt, + ...attributes +}) { + return ( + + {alt} + + ); +} + +LinkedLogo.propTypes = { + href: PropTypes.string.isRequired, + src: PropTypes.string.isRequired, + alt: PropTypes.string.isRequired, +}; + +export default LinkedLogo; diff --git a/src/course-home/badges-tab/messages.js b/src/course-home/badges-tab/messages.js new file mode 100644 index 0000000000..25bdbcdc81 --- /dev/null +++ b/src/course-home/badges-tab/messages.js @@ -0,0 +1,16 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messagesBadgeProgress = defineMessages({ + failure: { + id: 'badge.progress.loading.failure', + defaultMessage: 'There was an error loading the course badge progress.', + description: 'Message when course badge progress page fails to load', + }, + loading: { + id: 'badge.progress.loading', + defaultMessage: 'Loading course badge progress page...', + description: 'Message when course badge progress page is being loaded', + }, +}); + +export default messagesBadgeProgress; diff --git a/src/tab-page/TabPage.jsx b/src/tab-page/TabPage.jsx index 93d4708f77..a58a9275be 100644 --- a/src/tab-page/TabPage.jsx +++ b/src/tab-page/TabPage.jsx @@ -13,6 +13,7 @@ import { useModel } from '../generic/model-store'; import genericMessages from '../generic/messages'; import messages from './messages'; +import messagesBadgeProgress from '../course-home/badges-tab/messages'; import LoadedTabPage from './LoadedTabPage'; import { setCallToActionToast } from '../course-home/data/slice'; @@ -39,11 +40,20 @@ function TabPage({ intl, ...props }) { } = useModel(metadataModel, courseId); if (courseStatus === 'loading') { + let notificationMessage; + switch (activeTabSlug) { + case 'badge_progress': + notificationMessage = messagesBadgeProgress.loading; + break; + default: + notificationMessage = messages.loading; + } + return ( <>
@@ -84,12 +94,21 @@ function TabPage({ intl, ...props }) { ); } + let notificationMessage; + switch (activeTabSlug) { + case 'badge_progress': + notificationMessage = messagesBadgeProgress.failure; + break; + default: + notificationMessage = messages.failure; + } + // courseStatus 'failed' and any other unexpected course status. return ( <>

- {intl.formatMessage(messages.failure)} + {intl.formatMessage(notificationMessage)}

From 084fc0dd8d7cca73f123ceedcc1189d2eb000ed6 Mon Sep 17 00:00:00 2001 From: Zachary Trabookis Date: Fri, 11 Mar 2022 11:57:34 -0500 Subject: [PATCH 06/17] feat(badges): Upgraded code to use latest Paragon classes. Fixed issues with upgrading Paragon. Todo: Need to build out EducateWorkforce theme from https://github.com/edx/brand-openedx instead of using https://github.com/edx/brand-edx.org going forward. --- .../badges-tab/BadgeProgressTab.jsx | 7 +- .../card/BadgeProgressCardDetailsModal.jsx | 214 +++++++++--------- src/course-home/badges-tab/logos.jsx | 1 - src/course-home/data/thunks.js | 2 +- src/tab-page/TabPage.jsx | 1 + 5 files changed, 110 insertions(+), 115 deletions(-) diff --git a/src/course-home/badges-tab/BadgeProgressTab.jsx b/src/course-home/badges-tab/BadgeProgressTab.jsx index 002cd81ecc..f9ba1c8711 100644 --- a/src/course-home/badges-tab/BadgeProgressTab.jsx +++ b/src/course-home/badges-tab/BadgeProgressTab.jsx @@ -41,17 +41,18 @@ function BadgeProgressTab({ intl }) { const hasBadgeProgress = () => progress && progress.length > 0; useEffect(() => { let classProgressExists = 0; + let badgeProgress = Object.values(badgeProgressState); if (hasInstructorStaffRights()) { - badgeProgressState.value.forEach(student => { + badgeProgress.forEach(student => { if (student.progress.length) { classProgressExists += 1; } }); if (classProgressExists) { - setProgress(badgeProgressState.value); + setProgress(badgeProgress); } } else { - setProgress(badgeProgressState.value); + setProgress(badgeProgress); } }, [courseId, administrator]); diff --git a/src/course-home/badges-tab/badge-progress/card/BadgeProgressCardDetailsModal.jsx b/src/course-home/badges-tab/badge-progress/card/BadgeProgressCardDetailsModal.jsx index 4746a9d4df..b41af514ba 100644 --- a/src/course-home/badges-tab/badge-progress/card/BadgeProgressCardDetailsModal.jsx +++ b/src/course-home/badges-tab/badge-progress/card/BadgeProgressCardDetailsModal.jsx @@ -45,127 +45,121 @@ const BadgeProgressCardDetailsModal = (props) => { const redirectBackpack = () => window.open('https://badgr.com/recipient/badges', '_blank'); - const renderModal = () => { + const renderModal = () => ( /* Todo: May consider going back to `src = progress.assertion.imageUrl` to reflect actual image earned. I was not able to render "http://example.com/image.png" because it produces a 404 error. */ - const childElements = ( - {progress.badgeClass.displayName} - ); - - return ( - <> -
-
- -
- -
-
-
-

{progress.badgeClass.displayName}

-
-
-
- {progress.badgeClass.description} -
-
+ <> +
+
+ + {progress.badgeClass.displayName} + +
+ +
+
+
+

{progress.badgeClass.displayName}

-
-
- {progress.badgeClass.displayName} - -
-
- {progress.assertion.issuedOn && progress.blockDisplayName && ( -
- {badgeProgressCardStatus} -
- )} - {progress.assertion.recipient.plaintextIdentity && ( -
-

Recipient

-

{progress.assertion.recipient.plaintextIdentity}

-
- )} - {progress.badgeClass.criteria && ( -
-

Criteria

- {progress.badgeClass.criteria} -
- )} - {progress.assertion.issuer && ( -
-

Issuer

-
    -
  • - {progress.assertion.issuer.name} - {progress.assertion.issuer.email} -
  • -
-
- )} +
+
+ {progress.badgeClass.description}
-
- -
-
-

Share your Open Badge with Badgr

-

- Your achievement has been recognized with an , a digital image file with information - embedded in it that uniquely identifies your accomplishments. -

-

- Badgr is a service that creates and stores Open Badges and lets you share them with others. - To share your badge using Badgr, you can send a link to a web page about your badge to others. - You can also send the badge image file directly to others, and they can use a from Badgr to confirm your accomplishment. - For more options, you must first have a Badgr account. - You should have received an email the first time you received a badge with - instructions about creating a Badgr account. Once you have a Badgr account, you can organize - your badges in a Backpack and access tools to help share your badges on social media, embed - them in web pages, and more. -

-
-
+
+
+
+ {progress.badgeClass.displayName} + +
+
+ {progress.assertion.issuedOn && progress.blockDisplayName && ( +
+ {badgeProgressCardStatus} +
+ )} + {progress.assertion.recipient.plaintextIdentity && ( +
+

Recipient

+

{progress.assertion.recipient.plaintextIdentity}

+
+ )} + {progress.badgeClass.criteria && ( +
+

Criteria

+ {progress.badgeClass.criteria}
-
-
-
    -
  1. Create a account, or to your existing account;
  2. -
  3. ; or
  4. -
  5. - and share it - directly with others. They can verify it's really yours at . -
  6. -
-
-
- {getBadgrLogo()} -
+ )} + {progress.assertion.issuer && ( +
+

Issuer

+
    +
  • + {progress.assertion.issuer.name} + {progress.assertion.issuer.email} +
  • +
- + )}
- - )} - parentSelector={parentSelector} - buttons={[]} - onClose={resetModalWrapperState} - /> - - ); - }; +
+ +
+
+

Share your Open Badge with Badgr

+

+ Your achievement has been recognized with an Open Badge, a digital image file with information + embedded in it that uniquely identifies your accomplishments. +

+

+ Badgr is a service that creates and stores Open Badges and lets you share them with others. + To share your badge using Badgr, you can send a link to a web page about your badge to others. + You can also send the badge image file directly to others, and they can use a from Badgr to confirm your accomplishment. + For more options, you must first have a Badgr account. + You should have received an email the first time you received a badge with + instructions about creating a Badgr account. Once you have a Badgr account, you can organize + your badges in a Backpack and access tools to help share your badges on social media, embed + them in web pages, and more. +

+
+
+
+
+
+
    +
  1. Create a Badgr account, or log in to your existing account;
  2. +
  3. Share this public URL to your badge; or
  4. +
  5. + Download your badge (right-click or option-click, save as) and share it + directly with others. They can verify it's really yours at badgecheck.io. +
  6. +
+
+
+ {getBadgrLogo()} +
+
+
+
+
+ + )} + parentSelector={parentSelector} + buttons={[]} + onClose={resetModalWrapperState} + /> + + ); return renderModal(); }; diff --git a/src/course-home/badges-tab/logos.jsx b/src/course-home/badges-tab/logos.jsx index 0d350fa583..9605f70270 100644 --- a/src/course-home/badges-tab/logos.jsx +++ b/src/course-home/badges-tab/logos.jsx @@ -1,4 +1,3 @@ - import React from 'react'; import PropTypes from 'prop-types'; diff --git a/src/course-home/data/thunks.js b/src/course-home/data/thunks.js index 55f84c63ab..665308ec3a 100644 --- a/src/course-home/data/thunks.js +++ b/src/course-home/data/thunks.js @@ -58,7 +58,7 @@ export function fetchTab(courseId, tab, getTabData, targetUserId) { modelType: tab, model: { id: courseId, - ...tabDataResult, + ...tabDataResult.value, }, })); } else { diff --git a/src/tab-page/TabPage.jsx b/src/tab-page/TabPage.jsx index a58a9275be..9bfde3cda9 100644 --- a/src/tab-page/TabPage.jsx +++ b/src/tab-page/TabPage.jsx @@ -104,6 +104,7 @@ function TabPage({ intl, ...props }) { } // courseStatus 'failed' and any other unexpected course status. + //
return ( <>
From 808ac9599ca85fd3d45f3eb546f6e5d56a784316 Mon Sep 17 00:00:00 2001 From: Zachary Trabookis Date: Fri, 11 Mar 2022 12:04:59 -0500 Subject: [PATCH 07/17] feat(badges): Updated badge progress for the instructor view. - Spoke with edX (@djoy) about upgrading Paragon to use latest version to support component and he mentioned that there was minimal breaking changes. https://openedx.slack.com/archives/C0EUBSV7D/p1619106947014800 - With Paragon upgrade it appears edX went to theming the frontend with what they call `brand` repo. Details here: https://open-edx-proposals.readthedocs.io/en/latest/oep-0048-brand-customization.html - Needed to update the to since it was being deprecated. Also there was a weird issue of when using a filter that the modal wouldn't load until I replaced this component out. - Instructor is able to search for learner and filter by `Awarded`, `Not Awarded` badge assertions. --- .../badge-progress/card/BadgeProgressCard.jsx | 5 +- .../card/BadgeProgressCardDetailsModal.jsx | 233 +++++++++--------- .../badges-tab/badge-progress/card/index.scss | 23 +- .../course-list/BadgeProgressCourseList.jsx | 52 ++-- .../BadgeProgressCourseListItem.jsx | 49 ---- .../BadgeProgressCourseListTable.jsx | 61 +++++ .../BadgeProgressCourseListTableCell.jsx | 44 ++++ .../BadgeProgressCourseListTableHeaderRow.jsx | 28 +++ .../BadgeProgressCourseListTableRow.jsx | 38 +++ .../badge-progress/course-list/index.scss | 2 +- .../badges-tab/badge-progress/index.js | 2 +- src/course-home/badges-tab/utils.js | 46 +++- src/tab-page/TabPage.jsx | 1 - 13 files changed, 386 insertions(+), 198 deletions(-) delete mode 100644 src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListItem.jsx create mode 100644 src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListTable.jsx create mode 100644 src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListTableCell.jsx create mode 100644 src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListTableHeaderRow.jsx create mode 100644 src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListTableRow.jsx diff --git a/src/course-home/badges-tab/badge-progress/card/BadgeProgressCard.jsx b/src/course-home/badges-tab/badge-progress/card/BadgeProgressCard.jsx index 85a2c0f63f..a7f2460a00 100644 --- a/src/course-home/badges-tab/badge-progress/card/BadgeProgressCard.jsx +++ b/src/course-home/badges-tab/badge-progress/card/BadgeProgressCard.jsx @@ -17,7 +17,7 @@ const BadgeProgressCard = (props) => { const getBadgeProgressCardDetails = (earned) => ( <> - + ); @@ -28,8 +28,7 @@ const BadgeProgressCard = (props) => { <> {assertionUrl && ( { const { - parentSelector, progress, minimal, badgeProgressCardStatus, } = props; - const [modalOpen, setModalOpen] = useState(false); - // const [modalModel, setModalModel] = useState([{}]); + // const [modalOpen, setModalOpen] = useState(false); + + const [isOpen, open, close] = useToggle(false); + const [modalSize] = useState('xl'); + const [modalVariant] = useState('dark'); const getBadgrLogo = () => ( { /> ); - const openModal = () => { - setModalOpen(true); - // setModalModel([{}]); - }; - - const resetModalWrapperState = () => { - setModalOpen(false); - // setModalModel([{}]); - // this.button.focus(); - }; - const redirectBackpack = () => window.open('https://badgr.com/recipient/badges', '_blank'); const renderModal = () => ( @@ -53,111 +46,129 @@ const BadgeProgressCardDetailsModal = (props) => { <>
- + {progress.badgeClass.displayName}
- -
-
-
-

{progress.badgeClass.displayName}

-
-
-
- {progress.badgeClass.description} + +
+
+ + + + +
+

{progress.badgeClass.displayName}

-
-
-
-
- {progress.badgeClass.displayName} - -
-
- {progress.assertion.issuedOn && progress.blockDisplayName && ( -
- {badgeProgressCardStatus} +
+
+ {progress.badgeClass.description}
- )} - {progress.assertion.recipient.plaintextIdentity && ( -
-

Recipient

-

{progress.assertion.recipient.plaintextIdentity}

-
- )} - {progress.badgeClass.criteria && ( -
-

Criteria

- {progress.badgeClass.criteria} -
- )} - {progress.assertion.issuer && ( -
-

Issuer

-
    -
  • - {progress.assertion.issuer.name} - {progress.assertion.issuer.email} -
  • -
-
- )} -
+
+ + + +
+ + +
+
+ {progress.badgeClass.displayName} +
-
- -
-
-

Share your Open Badge with Badgr

-

- Your achievement has been recognized with an Open Badge, a digital image file with information - embedded in it that uniquely identifies your accomplishments. -

-

- Badgr is a service that creates and stores Open Badges and lets you share them with others. - To share your badge using Badgr, you can send a link to a web page about your badge to others. - You can also send the badge image file directly to others, and they can use a from Badgr to confirm your accomplishment. - For more options, you must first have a Badgr account. - You should have received an email the first time you received a badge with - instructions about creating a Badgr account. Once you have a Badgr account, you can organize - your badges in a Backpack and access tools to help share your badges on social media, embed - them in web pages, and more. -

-
-
+
+ {progress.assertion.issuedOn && progress.blockDisplayName && ( +
+ {badgeProgressCardStatus}
-
-
-
    -
  1. Create a Badgr account, or log in to your existing account;
  2. -
  3. Share this public URL to your badge; or
  4. -
  5. - Download your badge (right-click or option-click, save as) and share it - directly with others. They can verify it's really yours at badgecheck.io. -
  6. -
-
-
- {getBadgrLogo()} -
+ )} + {progress.assertion.recipient.plaintextIdentity && ( +
+

Recipient

+

{progress.assertion.recipient.plaintextIdentity}

- + )} + {progress.badgeClass.criteria && ( +
+

Criteria

+ {progress.badgeClass.criteria} +
+ )} + {progress.assertion.issuer && ( +
+

Issuer

+
    +
  • + {progress.assertion.issuer.name} + {progress.assertion.issuer.email} +
  • +
+
+ )}
- - )} - parentSelector={parentSelector} - buttons={[]} - onClose={resetModalWrapperState} - /> +
+ +
+
+

Share your Open Badge with Badgr

+

+ Your achievement has been recognized with an Open Badge, a digital image file with information + embedded in it that uniquely identifies your accomplishments. +

+

+ Badgr is a service that creates and stores Open Badges and lets you share them with others. + To share your badge using Badgr, you can send a link to a web page about your badge to others. + You can also send the badge image file directly to others, and they can use a badge verification service from Badgr to confirm your accomplishment. + For more options, you must first have a Badgr account. + You should have received an email the first time you received a badge with + instructions about creating a Badgr account. Once you have a Badgr account, you can organize + your badges in a Backpack and access tools to help share your badges on social media, embed + them in web pages, and more. +

+
+
+
+
+
+
    +
  1. Create a Badgr account, or log in to your existing account;
  2. +
  3. Share this public URL to your badge; or
  4. +
  5. + Download your badge (right-click or option-click, save as) and share it + directly with others. They can verify it's really yours at badgecheck.io. +
  6. +
+
+
+ {getBadgrLogo()} +
+
+
+
+ + + + + + Close + + + +
+ ); @@ -165,7 +176,6 @@ const BadgeProgressCardDetailsModal = (props) => { }; BadgeProgressCardDetailsModal.propTypes = { - parentSelector: PropTypes.string, progress: PropTypes.shape({ courseId: PropTypes.string, blockId: PropTypes.string, @@ -208,7 +218,6 @@ BadgeProgressCardDetailsModal.propTypes = { }; BadgeProgressCardDetailsModal.defaultProps = { - parentSelector: 'body', minimal: '', }; diff --git a/src/course-home/badges-tab/badge-progress/card/index.scss b/src/course-home/badges-tab/badge-progress/card/index.scss index ea23b55ef5..023f3d6736 100644 --- a/src/course-home/badges-tab/badge-progress/card/index.scss +++ b/src/course-home/badges-tab/badge-progress/card/index.scss @@ -173,6 +173,7 @@ /* BadgeProgressCardDetailsModal --------------------------------------- */ + /* .modal-progress-details { .modal { @@ -186,26 +187,36 @@ padding: 15px; .modal-body { + */ .progress-details { text-align: left; .progress-details-header { + + background: #48555d !important; + color: white !important; + + .pgn__modal-hero { + background: inherit !important; + color: inherit !important; + } .progress-details-title { h2 { - font-size: 30px; - font-weight: normal; + font-size: 30px !important; + font-weight: bold !important; } } .progress-details-description { - p { - font-size: 16px; + div, p { + font-size: 16px !important; + font-weight: normal !important; } } @@ -411,7 +422,7 @@ } } - +/* } } @@ -421,4 +432,4 @@ } } - \ No newline at end of file +*/ \ No newline at end of file diff --git a/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseList.jsx b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseList.jsx index 149e8fce16..4f3a70053b 100644 --- a/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseList.jsx +++ b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseList.jsx @@ -1,8 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Table } from '@edx/paragon'; +import snakeCase from 'lodash.snakecase'; +import { DataTable, TextFilter } from '@edx/paragon'; -import BadgeProgressCourseListItem from './BadgeProgressCourseListItem'; +import BadgeProgressCard from '../card/BadgeProgressCard'; +import BadgeProgressCourseListTable from './BadgeProgressCourseListTable'; const BadgeProgressCourseList = (props) => { const { data, headings } = props; @@ -11,13 +13,15 @@ const BadgeProgressCourseList = (props) => { const results = []; data.forEach((item) => { + const itemUserName = item.userName; const learnerData = { username: `'${item.userName}' (${item.email})`, }; item.progress.forEach((i) => { - learnerData[i.blockId] = ( - + const itemKey = snakeCase(`card ${i.blockDisplayName} ${itemUserName}`); + learnerData[snakeCase(i.blockDisplayName)] = ( + ); }); @@ -27,28 +31,32 @@ const BadgeProgressCourseList = (props) => { return results; }; - // eslint-disable-next-line no-unused-vars - const sortProgressByCourseBlockOrder = (progress) => { - if (progress) { - return progress.sort((a, b) => { - if (a.block_order < b.block_order) { return -1; } - if (a.block_order > b.block_order) { return 1; } - return 0; - }); - } - return 0; + const getLearnerCount = () => { + const results = []; + data.forEach((item) => results.push(item.userName)); + return results.length; }; return ( <> -
-
- + console.log(`This function will be called with the value: ${JSON.stringify(currentState)}}`)} + data={getProgressCourseListData()} + columns={headings} + > + + + + ); }; diff --git a/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListItem.jsx b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListItem.jsx deleted file mode 100644 index aa62c4e302..0000000000 --- a/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListItem.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import BadgeProgressCard from '../card/BadgeProgressCard'; - -const BadgeProgressCourseListItem = ({ badge }) => ( - -); - -BadgeProgressCourseListItem.propTypes = { - badge: PropTypes.shape({ - courseId: PropTypes.string, - blockId: PropTypes.string, - blockDisplayName: PropTypes.string, - blockOrder: PropTypes.number, - eventType: PropTypes.string, - badgeClass: PropTypes.shape({ - slug: PropTypes.string, - issuingComponent: PropTypes.string, - displayName: PropTypes.string, - courseId: PropTypes.string, - description: PropTypes.string, - criteria: PropTypes.string, - image: PropTypes.string, - }), - assertion: PropTypes.shape({ - issuedOn: PropTypes.string, - entityId: PropTypes.string, - expires: PropTypes.string, - revoked: PropTypes.bool, - imageUrl: PropTypes.string, - assertionUrl: PropTypes.string, - recipient: PropTypes.shape({ - plaintextIdentity: PropTypes.string, - }), - issuer: PropTypes.shape({ - entityType: PropTypes.string, - entityId: PropTypes.string, - openBadgeId: PropTypes.string, - name: PropTypes.string, - image: PropTypes.string, - email: PropTypes.string, - description: PropTypes.string, - url: PropTypes.string, - }), - }), - }).isRequired, -}; - -export default BadgeProgressCourseListItem; diff --git a/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListTable.jsx b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListTable.jsx new file mode 100644 index 0000000000..3999c8dec5 --- /dev/null +++ b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListTable.jsx @@ -0,0 +1,61 @@ +import React, { useContext } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { DataTableContext } from '@edx/paragon'; + +import BadgeProgressCourseListTableHeaderRow from './BadgeProgressCourseListTableHeaderRow'; +import BadgeProgressCourseListTableRow from './BadgeProgressCourseListTableRow'; + +const BadgeProgressCourseListTable = ({ isStriped }) => { + const useRows = () => { + const { + getTableProps, prepareRow, page, rows, headerGroups, getTableBodyProps, + } = useContext(DataTableContext); + + const displayRows = page || rows; + + return { + getTableProps, prepareRow, displayRows, headerGroups, getTableBodyProps, + }; + }; + + const { + getTableProps, prepareRow, displayRows, headerGroups, getTableBodyProps, + } = useRows(); + + const renderRows = () => displayRows.map((row) => { + prepareRow(row); + return ( + + ); + }); + + if (!getTableProps) { + return null; + } + + return ( +
+
+ + + {renderRows()} + +
+
+ ); +}; + +BadgeProgressCourseListTable.defaultProps = { + isStriped: true, +}; + +BadgeProgressCourseListTable.propTypes = { + /** should table rows be striped */ + isStriped: PropTypes.bool, +}; + +export default BadgeProgressCourseListTable; diff --git a/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListTableCell.jsx b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListTableCell.jsx new file mode 100644 index 0000000000..37b7c6c294 --- /dev/null +++ b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListTableCell.jsx @@ -0,0 +1,44 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import BadgeProgressCard from '../card/BadgeProgressCard'; + +const BadgeProgressCourseListTableCell = ( + { + getCellProps, render, column, value, + }, +) => ( + + + {column.id === 'username' && ( + render('Cell') + )} + {column.id !== 'username' && ( + + )} + + +); + +BadgeProgressCourseListTableCell.defaultProps = { + value: '', +}; + +BadgeProgressCourseListTableCell.propTypes = { + /** Props for the td element */ + getCellProps: PropTypes.func.isRequired, + /** Function that renders the cell contents. Will be called with the string 'Cell' */ + render: PropTypes.func.isRequired, + /** Table column */ + column: PropTypes.shape({ + cellClassName: PropTypes.string, + id: PropTypes.string, + }).isRequired, + value: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.shape(), + ]), +}; + +export default BadgeProgressCourseListTableCell; diff --git a/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListTableHeaderRow.jsx b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListTableHeaderRow.jsx new file mode 100644 index 0000000000..edced33f02 --- /dev/null +++ b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListTableHeaderRow.jsx @@ -0,0 +1,28 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { TableHeaderCell } from '@edx/paragon'; + +const BadgeProgressCourseListTableHeaderRow = ({ headerGroups }) => ( + + {headerGroups.map(headerGroup => ( + + {headerGroup.headers.map(column => ( + + ))} + + ))} + +); + +BadgeProgressCourseListTableHeaderRow.propTypes = { + headerGroups: PropTypes.arrayOf(PropTypes.shape({ + headers: PropTypes.arrayOf(PropTypes.shape({ + /** Props for the TableHeaderCell component. Must include a key */ + getHeaderProps: PropTypes.func.isRequired, + })).isRequired, + /** Returns props for the header tr element */ + getHeaderGroupProps: PropTypes.func.isRequired, + })).isRequired, +}; + +export default BadgeProgressCourseListTableHeaderRow; diff --git a/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListTableRow.jsx b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListTableRow.jsx new file mode 100644 index 0000000000..ee0a6107df --- /dev/null +++ b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListTableRow.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import snakeCase from 'lodash.snakecase'; + +import BadgeProgressCourseListTableCell from './BadgeProgressCourseListTableCell'; + +/* key={`${cell.column.Header}${id}`} */ +const BadgeProgressCourseListTableRow = ({ + getRowProps, cells, id, isSelected, +}) => ( + + {cells.map(cell => )} + +); + +BadgeProgressCourseListTableRow.defaultProps = { + isSelected: false, +}; + +BadgeProgressCourseListTableRow.propTypes = { + /** props to include on the tr tag (must include id) */ + getRowProps: PropTypes.func.isRequired, + /** cells in the row */ + cells: PropTypes.arrayOf(PropTypes.shape({})).isRequired, + /** row id */ + id: PropTypes.string.isRequired, + /** indicates if row has been selected */ + isSelected: PropTypes.bool, +}; + +export default BadgeProgressCourseListTableRow; diff --git a/src/course-home/badges-tab/badge-progress/course-list/index.scss b/src/course-home/badges-tab/badge-progress/course-list/index.scss index 3bfce50379..3d2c6a0810 100644 --- a/src/course-home/badges-tab/badge-progress/course-list/index.scss +++ b/src/course-home/badges-tab/badge-progress/course-list/index.scss @@ -48,7 +48,7 @@ } -/* BadgeProgressCourseListItem +/* BadgeProgressCourseListTable --------------------------------------- */ .progress-list-item { diff --git a/src/course-home/badges-tab/badge-progress/index.js b/src/course-home/badges-tab/badge-progress/index.js index 60bd7d3ca6..6ddbba0298 100644 --- a/src/course-home/badges-tab/badge-progress/index.js +++ b/src/course-home/badges-tab/badge-progress/index.js @@ -2,4 +2,4 @@ export { default as BadgeProgressBanner } from './banner/BadgeProgressBanner'; export { default as BadgeProgressCard } from './card/BadgeProgressCard'; export { default as BadgeProgressCourseList } from './course-list/BadgeProgressCourseList'; -export { default as BadgeProgressCourseListItem } from './course-list/BadgeProgressCourseListItem'; +export { default as BadgeProgressCourseListTable } from './course-list/BadgeProgressCourseListTable'; diff --git a/src/course-home/badges-tab/utils.js b/src/course-home/badges-tab/utils.js index 11d43491a8..fb37713a89 100644 --- a/src/course-home/badges-tab/utils.js +++ b/src/course-home/badges-tab/utils.js @@ -1,5 +1,32 @@ /* eslint-disable import/prefer-default-export */ +import snakeCase from 'lodash.snakecase'; +import { DropdownFilter } from '@edx/paragon'; +import { isEmptyObject } from '../../utils/empty'; + +// Defines a custom filter filter function for finding badge assertion +const filterContainsBadgeAssertion = (rows, id, filterValue) => { + const isBadgeProgressComplete = (data) => { + if (isEmptyObject(data.assertion)) { + return false; + } + return data.assertion.imageUrl.length > 0; + }; + + return rows.filter(row => { + const rowValue = row.values[id]; + const badgeAsserted = isBadgeProgressComplete(rowValue.props.data); + + return badgeAsserted ? snakeCase(`${rowValue.props.data.blockDisplayName} awarded`) === filterValue : snakeCase(`${rowValue.props.data.blockDisplayName} not awarded`) === filterValue; + }); +}; + +// This is an autoRemove method on the filter function that +// when given the new filter value and returns true, the filter +// will be automatically removed. Normally this is just an undefined +// check, but here, we want to remove the filter if it's not a string or string is empty +filterContainsBadgeAssertion.autoRemove = val => typeof val !== 'string' || !val; + const headingMapper = (filterKey, data) => { // eslint-disable-next-line no-unused-vars const dataSortable = data.slice(); @@ -7,17 +34,30 @@ const headingMapper = (filterKey, data) => { function all(entry) { if (entry) { const results = [{ + Header: 'Student', label: 'Student', key: 'username', + accessor: 'username', width: 'col-2', }]; const progressHeadings = entry.progress .filter(blocks => blocks.blockDisplayName) .map(b => ({ - label: b.blockDisplayName.replace(/[0-9]+\./g, ''), - key: b.blockId, - width: 'col-1', + Header: b.blockDisplayName.replace(/[0-9]+\./g, ''), + accessor: snakeCase(b.blockDisplayName), + Filter: DropdownFilter, + filter: filterContainsBadgeAssertion, + filterChoices: [ + { + name: 'Awarded', + value: snakeCase(`${b.blockDisplayName} awarded`), + }, + { + name: 'Not Awarded', + value: snakeCase(`${b.blockDisplayName} not awarded`), + }, + ], })); return results.concat(progressHeadings); diff --git a/src/tab-page/TabPage.jsx b/src/tab-page/TabPage.jsx index 9bfde3cda9..a58a9275be 100644 --- a/src/tab-page/TabPage.jsx +++ b/src/tab-page/TabPage.jsx @@ -104,7 +104,6 @@ function TabPage({ intl, ...props }) { } // courseStatus 'failed' and any other unexpected course status. - //
return ( <>
From 43808807b2218a466b5a7bf17f2ab2731b5645c0 Mon Sep 17 00:00:00 2001 From: Zachary Trabookis Date: Fri, 25 Mar 2022 11:18:26 -0400 Subject: [PATCH 08/17] feat(badges): Render `Badge Progress` on outline page when badge earned. The following changes were made: - Add unique key for BadgeProgressCard - Return `badge_progress` from LMS API outline call and use it to display `Badge Progress` on outline page. --- package-lock.json | 5 +++++ package.json | 2 ++ .../badges-tab/BadgeProgressTab.jsx | 11 ++++++++--- .../badge-header/BadgeTabsNavigation.jsx | 4 ++-- .../banner/BadgeProgressBanner.jsx | 2 +- src/course-home/data/api.js | 1 + src/course-home/outline-tab/OutlineTab.jsx | 1 + src/course-home/outline-tab/Section.jsx | 19 ++++++++++++++++++- 8 files changed, 38 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5dcedfbf68..b9025cc2e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12434,6 +12434,11 @@ "dev": true, "optional": true }, + "iframe-resizer": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/iframe-resizer/-/iframe-resizer-4.3.2.tgz", + "integrity": "sha512-gOWo2hmdPjMQsQ+zTKbses08mDfDEMh4NneGQNP4qwePYujY1lguqP6gnbeJkf154gojWlBhIltlgnMfYjGHWA==" + }, "ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", diff --git a/package.json b/package.json index b77029645b..4ce15c17b2 100644 --- a/package.json +++ b/package.json @@ -47,8 +47,10 @@ "@reduxjs/toolkit": "1.6.2", "classnames": "2.3.1", "core-js": "3.18.3", + "iframe-resizer": "^4.3.2", "js-cookie": "3.0.1", "lodash.camelcase": "4.3.0", + "lodash.snakecase": "^4.1.1", "mime-types": "^2.1.34", "moment": "^2.29.1", "moment-timezone": "^0.5.34", diff --git a/src/course-home/badges-tab/BadgeProgressTab.jsx b/src/course-home/badges-tab/BadgeProgressTab.jsx index f9ba1c8711..0992459954 100644 --- a/src/course-home/badges-tab/BadgeProgressTab.jsx +++ b/src/course-home/badges-tab/BadgeProgressTab.jsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react'; // import PropTypes from 'prop-types'; +import snakeCase from 'lodash.snakecase'; import { useSelector } from 'react-redux'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { StatusAlert } from '@edx/paragon'; @@ -94,9 +95,13 @@ const renderBadgeProgress = () => {
{progress && (
- {progress.map(learnerProgress => ( - - ))} + {progress.map(learnerProgress => { + const itemKey = snakeCase(`card ${learnerProgress.blockDisplayName} ${username}`); + return ( + + ) + } + )}
)}
diff --git a/src/course-home/badges-tab/badge-header/BadgeTabsNavigation.jsx b/src/course-home/badges-tab/badge-header/BadgeTabsNavigation.jsx index 143ccb86d3..66a44787a0 100644 --- a/src/course-home/badges-tab/badge-header/BadgeTabsNavigation.jsx +++ b/src/course-home/badges-tab/badge-header/BadgeTabsNavigation.jsx @@ -38,13 +38,13 @@ function BadgeTabsNavigation({ className="badge-nav-tabs" aria-label={intl.formatMessage(messages['learn.navigation.badge.tabs.label'])} > - + /> */} {tabs.map(({ url, title, slug, disabled, }) => ( diff --git a/src/course-home/badges-tab/badge-progress/banner/BadgeProgressBanner.jsx b/src/course-home/badges-tab/badge-progress/banner/BadgeProgressBanner.jsx index df470dca7c..ceb0154f76 100644 --- a/src/course-home/badges-tab/badge-progress/banner/BadgeProgressBanner.jsx +++ b/src/course-home/badges-tab/badge-progress/banner/BadgeProgressBanner.jsx @@ -15,7 +15,7 @@ const BadgeProgressBanner = ({ hasProgress, hasRights }) => { // d-flex justify-content-left return ( -
+
{ hasProgress && ( <>
diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js index 670c4fa45f..35d6a5f9ad 100644 --- a/src/course-home/data/api.js +++ b/src/course-home/data/api.js @@ -121,6 +121,7 @@ export function normalizeOutlineBlocks(courseId, blocks) { case 'chapter': models.sections[block.id] = { + badgeProgress: block.badge_progress, complete: block.complete, id: block.id, title: block.display_name, diff --git a/src/course-home/outline-tab/OutlineTab.jsx b/src/course-home/outline-tab/OutlineTab.jsx index 0b47b55896..8533f5c5dd 100644 --- a/src/course-home/outline-tab/OutlineTab.jsx +++ b/src/course-home/outline-tab/OutlineTab.jsx @@ -195,6 +195,7 @@ function OutlineTab({ intl }) { {courses[rootCourseId].sectionIds.map((sectionId) => (
@@ -66,6 +70,19 @@ function Section({ , {intl.formatMessage(complete ? messages.completedSection : messages.incompleteSection)} + {badgeProgress ? ( + <> + {badgeProgressUrl && ( + + + + )} + + ) : ( + <> + )}
); From 8ca2b689da4a88f6e9eb816b45161e912b505809 Mon Sep 17 00:00:00 2001 From: Zachary Trabookis Date: Wed, 20 Apr 2022 12:34:23 -0400 Subject: [PATCH 09/17] test(badges): Fix `eslint` tests. --- .../badges-tab/BadgeProgressTab.jsx | 19 +- .../badges-tab/BadgeProgressTab.test.jsx | 309 +----------------- .../badge-header/BadgeTabsNavigation.jsx | 4 +- .../course-list/BadgeProgressCourseList.jsx | 1 + .../__factories__/badgeProgress.factory.js | 137 ++++---- .../badgeProgressTabData.factory.js | 92 +++--- .../__factories__/outlineTabData.factory.js | 63 +++- src/course-home/data/thunks.js | 3 - src/course-home/outline-tab/OutlineTab.jsx | 1 - src/course-home/outline-tab/Section.jsx | 13 +- src/index.jsx | 4 +- 11 files changed, 198 insertions(+), 448 deletions(-) diff --git a/src/course-home/badges-tab/BadgeProgressTab.jsx b/src/course-home/badges-tab/BadgeProgressTab.jsx index 0992459954..ebba28bacb 100644 --- a/src/course-home/badges-tab/BadgeProgressTab.jsx +++ b/src/course-home/badges-tab/BadgeProgressTab.jsx @@ -9,25 +9,23 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons'; import { useModel } from '../../generic/model-store'; -import { debug } from 'util'; import { BadgeTabsNavigation } from './badge-header'; import { BadgeProgressBanner, BadgeProgressCard, BadgeProgressCourseList } from './badge-progress'; import { headingMapper } from './utils'; - -function BadgeProgressTab({ intl }) { +function BadgeProgressTab({ intl }) { // eslint-disable-line no-unused-vars const activeTabSlug = 'progress'; - + const { courseId, } = useSelector(state => state.courseHome); - const { + const { administrator, username, - roles + roles, // eslint-disable-line no-unused-vars } = getAuthenticatedUser(); const hasInstructorStaffRights = () => administrator; @@ -42,7 +40,7 @@ function BadgeProgressTab({ intl }) { const hasBadgeProgress = () => progress && progress.length > 0; useEffect(() => { let classProgressExists = 0; - let badgeProgress = Object.values(badgeProgressState); + const badgeProgress = Object.values(badgeProgressState); if (hasInstructorStaffRights()) { badgeProgress.forEach(student => { if (student.progress.length) { @@ -57,7 +55,7 @@ function BadgeProgressTab({ intl }) { } }, [courseId, administrator]); -const renderBadgeProgress = () => { + const renderBadgeProgress = () => { const defaultAssignmentFilter = 'All'; if (hasInstructorStaffRights()) { @@ -99,9 +97,8 @@ const renderBadgeProgress = () => { const itemKey = snakeCase(`card ${learnerProgress.blockDisplayName} ${username}`); return ( - ) - } - )} + ); + })}
)}
diff --git a/src/course-home/badges-tab/BadgeProgressTab.test.jsx b/src/course-home/badges-tab/BadgeProgressTab.test.jsx index c09b64805d..e5e0949610 100644 --- a/src/course-home/badges-tab/BadgeProgressTab.test.jsx +++ b/src/course-home/badges-tab/BadgeProgressTab.test.jsx @@ -1,18 +1,20 @@ +// TODO Need to complete these tests. + import React from 'react'; import { Route } from 'react-router'; import MockAdapter from 'axios-mock-adapter'; import { Factory } from 'rosie'; import { getConfig, history } from '@edx/frontend-platform'; -import { sendTrackEvent } from '@edx/frontend-platform/analytics'; +// import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { AppProvider } from '@edx/frontend-platform/react'; -import { waitForElementToBeRemoved } from '@testing-library/dom'; -import { render, screen, within } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +// import { waitForElementToBeRemoved } from '@testing-library/dom'; +import { render, screen, within } from '@testing-library/react'; // eslint-disable-line no-unused-vars +// import userEvent from '@testing-library/user-event'; import BadgeProgressTab from './BadgeProgressTab'; import { fetchBadgeProgressTab } from '../data'; -import { fireEvent, initializeMockApp, waitFor } from '../../setupTest'; +import { fireEvent, initializeMockApp, waitFor } from '../../setupTest'; // eslint-disable-line no-unused-vars import initializeStore from '../../store'; import { TabContainer } from '../../tab-page'; import { appendBrowserTimezoneToUrl } from '../../utils'; @@ -50,33 +52,11 @@ describe('BadgeProgressTab', () => { let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/course_home/course_metadata/${courseId}`; courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl); - function setMetadata(attributes, options) { + function setMetadata(attributes, options) { // eslint-disable-line no-unused-vars courseMetadata = Factory.build('courseHomeMetadata', { id: courseId, ...attributes }, options); axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata); } - // // The dates tab is largely repetitive non-interactive static data. Thus it's a little tough to follow - // // testing-library's advice around testing the way your user uses the site (i.e. can't find form elements by label or - // // anything). Instead, we find elements by printed date (which is what the user sees) and data-testid. Which is - // // better than assuming anything about how the surrounding elements are organized by div and span or whatever. And - // // better than adding non-style class names. - // // Hence the following getDay query helper. - // async function getDay(date) { - // const dateNode = await screen.findByText(date); - // let parent = dateNode.parentElement; - // while (parent) { - // if (parent.dataset && parent.dataset.testid === 'dates-day') { - // return { - // day: parent, - // header: within(parent).getByTestId('dates-header'), - // items: within(parent).queryAllByTestId('dates-item'), - // }; - // } - // parent = parent.parentElement; - // } - // throw new Error('Did not find day container'); - // } - describe('when receiving a full set of dates data', () => { beforeEach(() => { axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata); @@ -94,277 +74,4 @@ describe('BadgeProgressTab', () => { // expect(badges[1]).toHaveTextContent('Not yet released'); }); }); - - // describe('when receiving a full set of dates data', () => { - // beforeEach(() => { - // 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 - - // render(component); - // }); - - // it('handles unreleased & complete', async () => { - // const { header } = await getDay('Sun, May 3, 2020'); - // const badges = within(header).getAllByTestId('dates-badge'); - // expect(badges).toHaveLength(2); - // expect(badges[0]).toHaveTextContent('Completed'); - // expect(badges[1]).toHaveTextContent('Not yet released'); - // }); - - // it('handles unreleased & past due', async () => { - // const { header } = await getDay('Mon, May 4, 2020'); - // const badges = within(header).getAllByTestId('dates-badge'); - // expect(badges).toHaveLength(2); - // expect(badges[0]).toHaveTextContent('Past due'); - // expect(badges[1]).toHaveTextContent('Not yet released'); - // }); - - // it('handles verified only', async () => { - // const { day } = await getDay('Sun, Aug 18, 2030'); - // const badge = within(day).getByTestId('dates-badge'); - // expect(badge).toHaveTextContent('Verified only'); - // }); - - // it('verified only has no link', async () => { - // const { day } = await getDay('Sun, Aug 18, 2030'); - // expect(within(day).queryByRole('link')).toBeNull(); - // }); - - // it('same status items have header badge', async () => { - // const { day, header } = await getDay('Tue, May 26, 2020'); - // const badge = within(header).getByTestId('dates-badge'); - // expect(badge).toHaveTextContent('Past due'); // one header badge - // expect(within(day).getAllByTestId('dates-badge')).toHaveLength(1); // no other badges - // }); - - // it('different status items have individual badges', async () => { - // const { header, items } = await getDay('Thu, May 28, 2020'); - // const headerBadges = within(header).queryAllByTestId('dates-badge'); - // expect(headerBadges).toHaveLength(0); // no header badges - // expect(items).toHaveLength(2); - // expect(within(items[0]).getByTestId('dates-badge')).toHaveTextContent('Completed'); - // expect(within(items[1]).getByTestId('dates-badge')).toHaveTextContent('Past due'); - // }); - - // it('shows extra info', async () => { - // const { items } = await getDay('Sat, Aug 17, 2030'); - // expect(items).toHaveLength(3); - - // const tipIcon = within(items[2]).getByTestId('dates-extra-info'); - // const tipText = "ORA Dates are set by the instructor, and can't be changed"; - - // expect(screen.queryByText(tipText)).toBeNull(); // tooltip does not start in DOM - // userEvent.hover(tipIcon); - // const tooltip = screen.getByText(tipText); // now it's there - // userEvent.unhover(tipIcon); - // waitForElementToBeRemoved(tooltip); // and it's gone again - // }); - // }); - - // describe('Suggested schedule messaging', () => { - // beforeEach(() => { - // setMetadata({ is_self_paced: true, is_enrolled: true }); - // history.push(`/course/${courseId}/dates`); - // }); - - // it('renders SuggestedScheduleHeader', async () => { - // datesTabData.datesBannerInfo = { - // contentTypeGatingEnabled: false, - // missedDeadlines: false, - // missedGatedContent: false, - // }; - - // axiosMock.onGet(datesUrl).reply(200, datesTabData); - // render(component); - - // await waitFor(() => expect(screen.getByText('We’ve built a suggested schedule to help you stay on track. But don’t worry—it’s flexible so you can learn at your own pace.')).toBeInTheDocument()); - // }); - - // it('renders UpgradeToCompleteAlert', async () => { - // datesTabData.datesBannerInfo = { - // contentTypeGatingEnabled: true, - // missedDeadlines: false, - // missedGatedContent: false, - // verifiedUpgradeLink: 'http://localhost:18130/basket/add/?sku=8CF08E5', - // }; - - // axiosMock.onGet(datesUrl).reply(200, datesTabData); - // render(component); - - // await waitFor(() => expect(screen.getByText('You are auditing this course, which means that you are unable to participate in graded assignments. To complete graded assignments as part of this course, you can upgrade today.')).toBeInTheDocument()); - // expect(screen.getByRole('button', { name: 'Upgrade now' })).toBeInTheDocument(); - // }); - - // it('renders UpgradeToShiftDatesAlert', async () => { - // datesTabData.datesBannerInfo = { - // contentTypeGatingEnabled: true, - // missedDeadlines: true, - // missedGatedContent: true, - // verifiedUpgradeLink: 'http://localhost:18130/basket/add/?sku=8CF08E5', - // }; - - // axiosMock.onGet(datesUrl).reply(200, datesTabData); - // render(component); - - // await waitFor(() => expect(screen.getByText('It looks like you missed some important deadlines based on our suggested schedule.')).toBeInTheDocument()); - // expect(screen.getByText('To keep yourself on track, you can update this schedule and shift the past due assignments into the future. Don’t worry—you won’t lose any of the progress you’ve made when you shift your due dates.')).toBeInTheDocument(); - // expect(screen.getByRole('button', { name: 'Upgrade to shift due dates' })).toBeInTheDocument(); - // }); - - // it('renders ShiftDatesAlert', async () => { - // datesTabData.datesBannerInfo = { - // contentTypeGatingEnabled: true, - // missedDeadlines: true, - // missedGatedContent: false, - // verifiedUpgradeLink: 'http://localhost:18130/basket/add/?sku=8CF08E5', - // }; - - // axiosMock.onGet(datesUrl).reply(200, datesTabData); - // render(component); - - // await waitFor(() => expect(screen.getByText('It looks like you missed some important deadlines based on our suggested schedule.')).toBeInTheDocument()); - // expect(screen.getByText('To keep yourself on track, you can update this schedule and shift the past due assignments into the future. Don’t worry—you won’t lose any of the progress you’ve made when you shift your due dates.')).toBeInTheDocument(); - // expect(screen.getByRole('button', { name: 'Shift due dates' })).toBeInTheDocument(); - // }); - - // it('handles shift due dates click', async () => { - // datesTabData.datesBannerInfo = { - // contentTypeGatingEnabled: true, - // missedDeadlines: true, - // missedGatedContent: false, - // verifiedUpgradeLink: 'http://localhost:18130/basket/add/?sku=8CF08E5', - // }; - - // axiosMock.onGet(datesUrl).reply(200, datesTabData); - // render(component); - - // // confirm "Shift due dates" button has rendered - // await waitFor(() => expect(screen.getByRole('button', { name: 'Shift due dates' })).toBeInTheDocument()); - - // // update response to reflect shifted dates - // datesTabData.datesBannerInfo = { - // contentTypeGatingEnabled: true, - // missedDeadlines: false, - // missedGatedContent: false, - // verifiedUpgradeLink: 'http://localhost:18130/basket/add/?sku=8CF08E5', - // }; - - // const resetDeadlinesData = { - // header: "You've successfully shifted your dates!", - // }; - // axiosMock.onPost(`${getConfig().LMS_BASE_URL}/api/course_experience/v1/reset_course_deadlines`).reply(200, resetDeadlinesData); - - // // click "Shift due dates" - // fireEvent.click(screen.getByRole('button', { name: 'Shift due dates' })); - - // // wait for page to reload & Toast to render - // await waitFor(() => expect(screen.getByText("You've successfully shifted your dates!")).toBeInTheDocument()); - // // confirm "Shift due dates" button has not rendered - // expect(screen.queryByRole('button', { name: 'Shift due dates' })).not.toBeInTheDocument(); - // }); - - // it('sends analytics event onClick of upgrade button in UpgradeToCompleteAlert', async () => { - // sendTrackEvent.mockClear(); - // datesTabData.datesBannerInfo = { - // contentTypeGatingEnabled: true, - // missedDeadlines: false, - // missedGatedContent: false, - // verifiedUpgradeLink: 'http://localhost:18130/basket/add/?sku=8CF08E5', - // }; - - // axiosMock.onGet(datesUrl).reply(200, datesTabData); - // render(component); - - // const upgradeButton = await waitFor(() => screen.getByRole('button', { name: 'Upgrade now' })); - // fireEvent.click(upgradeButton); - - // expect(sendTrackEvent).toHaveBeenCalledTimes(1); - // expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.ecommerce.upsell_links_clicked', { - // org_key: 'edX', - // courserun_key: courseId, - // linkCategory: 'personalized_learner_schedules', - // linkName: 'dates_upgrade', - // linkType: 'button', - // pageName: 'dates_tab', - // }); - // }); - - // it('sends analytics event onClick of upgrade button in UpgradeToShiftDatesAlert', async () => { - // sendTrackEvent.mockClear(); - // datesTabData.datesBannerInfo = { - // contentTypeGatingEnabled: true, - // missedDeadlines: true, - // missedGatedContent: true, - // verifiedUpgradeLink: 'http://localhost:18130/basket/add/?sku=8CF08E5', - // }; - - // axiosMock.onGet(datesUrl).reply(200, datesTabData); - // render(component); - - // const upgradeButton = await waitFor(() => screen.getByRole('button', { name: 'Upgrade to shift due dates' })); - // fireEvent.click(upgradeButton); - - // expect(sendTrackEvent).toHaveBeenCalledTimes(1); - // expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.ecommerce.upsell_links_clicked', { - // org_key: 'edX', - // courserun_key: courseId, - // linkCategory: 'personalized_learner_schedules', - // linkName: 'dates_upgrade', - // linkType: 'button', - // pageName: 'dates_tab', - // }); - // }); - // }); - - // describe('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 - // }); - - // it('redirects to the home page when unauthenticated', async () => { - // await renderDenied('authentication_required'); - // expect(global.location.href).toEqual(`http://localhost/redirect/course-home/${courseMetadata.id}`); - // }); - - // it('redirects to the home page when unenrolled', async () => { - // await renderDenied('enrollment_required'); - // expect(global.location.href).toEqual(`http://localhost/redirect/course-home/${courseMetadata.id}`); - // }); - // }); }); diff --git a/src/course-home/badges-tab/badge-header/BadgeTabsNavigation.jsx b/src/course-home/badges-tab/badge-header/BadgeTabsNavigation.jsx index 66a44787a0..8a861a9867 100644 --- a/src/course-home/badges-tab/badge-header/BadgeTabsNavigation.jsx +++ b/src/course-home/badges-tab/badge-header/BadgeTabsNavigation.jsx @@ -6,8 +6,8 @@ import classNames from 'classnames'; import messages from './messages'; import Tabs from '../../../generic/tabs/Tabs'; -import LinkedLogo from '../logos'; -import logo from '../assets/logo-badgr-black.svg'; +import LinkedLogo from '../logos'; // eslint-disable-line no-unused-vars +import logo from '../assets/logo-badgr-black.svg'; // eslint-disable-line no-unused-vars function BadgeTabsNavigation({ activeTabSlug, className, intl, diff --git a/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseList.jsx b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseList.jsx index 4f3a70053b..3259fd3590 100644 --- a/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseList.jsx +++ b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseList.jsx @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import React from 'react'; import PropTypes from 'prop-types'; import snakeCase from 'lodash.snakecase'; diff --git a/src/course-home/data/__factories__/badgeProgress.factory.js b/src/course-home/data/__factories__/badgeProgress.factory.js index e3cb5b9d49..2eda245627 100644 --- a/src/course-home/data/__factories__/badgeProgress.factory.js +++ b/src/course-home/data/__factories__/badgeProgress.factory.js @@ -8,85 +8,88 @@ Factory.define('badge-progress') event_type: 'chapter_complete', }); - // Default to one badge_class. If badge_class was given, fill in - // whatever attributes might be missing. - // .attr('badge_class', ['badge_class'], (badge_class) => { - // if (!badge_class) { - // badge_class = [{}]; - // } - // return badge_class.map((data) => Factory.attributes('badge_class', data)); - // }) +// Default to one badge_class. If badge_class was given, fill in +// whatever attributes might be missing. +// .attr('badge_class', ['badge_class'], (badge_class) => { +// if (!badge_class) { +// badge_class = [{}]; +// } +// return badge_class.map((data) => Factory.attributes('badge_class', data)); +// }) - // Default to one assertion. If assertion was given, fill in - // whatever attributes might be missing. - // .attr('assertion', ['assertion'], (assertion) => { - // if (!assertion) { - // assertion = [{}]; - // } - // return assertion.map((data) => Factory.attributes('assertion', data)); - // }) +// Default to one assertion. If assertion was given, fill in +// whatever attributes might be missing. +// .attr('assertion', ['assertion'], (assertion) => { +// if (!assertion) { +// assertion = [{}]; +// } +// return assertion.map((data) => Factory.attributes('assertion', data)); +// }) // { -// course_id: "course-v1:edX+DemoX+Demo_Course", -// block_id: "block-v1:edX+DemoX+Demo_Course+type@chapter+block@dc1e160e5dc348a48a98fa0f4a6e8675", -// block_display_name: "Example Week 1: Getting Started", -// event_type: "chapter_complete", +// course_id: 'course-v1:edX+DemoX+Demo_Course', +// block_id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@dc1e160e5dc348a48a98fa0f4a6e8675', +// block_display_name: 'Example Week 1: Getting Started', +// event_type: 'chapter_complete', // badge_class: { -// slug: "special_award", -// issuing_component: "openedx__course", -// display_name: "Very Special Award", -// course_id: "course-v1:edX+DemoX+Demo_Course", -// description: "Awarded for people who did something incredibly special", -// criteria: "Do something incredibly special.", -// image: "http://example.com/media/badge_classes/badges/special_xdpqpBv_9FYOZwN.png" +// slug: 'special_award', +// issuing_component: 'openedx__course', +// display_name: 'Very Special Award', +// course_id: 'course-v1:edX+DemoX+Demo_Course', +// description: 'Awarded for people who did something incredibly special', +// criteria: 'Do something incredibly special.', +// image: 'http://example.com/media/badge_classes/badges/special_xdpqpBv_9FYOZwN.png' // }, // assertion: { -// issuedOn: "2019-04-20T02:43:06.566955Z", -// expires: "2019-04-30T00:00:00.000000Z", +// issuedOn: '2019-04-20T02:43:06.566955Z', +// expires: '2019-04-30T00:00:00.000000Z', // revoked: false, -// image_url: "http://badges.example.com/media/issued/cd75b69fc1c979fcc1697c8403da2bdf.png", -// assertion_url: "http://badges.example.com/public/assertions/07020647-e772-44dd-98b7-d13d34335ca6", +// image_url: 'http://badges.example.com/media/issued/cd75b69fc1c979fcc1697c8403da2bdf.png', +// assertion_url: 'http://badges.example.com/public/assertions/07020647-e772-44dd-98b7-d13d34335ca6', // recipient: { -// plaintextIdentity: "john.doe@example.com" +// plaintextIdentity: 'john.doe@example.com' // }, // issuer: { -// entityType: "Issuer", -// entityId: "npqlh0acRFG5pKKbnb4SRg", -// openBadgeId: "https://api.badgr.io/public/issuers/npqlh0acRFG5pKKbnb4SRg", -// name: "EducateWorkforce", -// image: "https://media.us.badgr.io/uploads/issuers/issuer_logo_77bb4498-838b-45b7-8722-22878fedb5e8.svg", -// email: "cucwd.developer@gmail.com", -// description: "An online learning solution offered with partnering 2-year colleges to help integrate web and digital solutions into their existing courses. The platform was designed by multiple instructional design, usability, and computing experts to include research-based learning features.", -// url: "https://ew-localhost.com" +// entityType: 'Issuer', +// entityId: 'npqlh0acRFG5pKKbnb4SRg', +// openBadgeId: 'https://api.badgr.io/public/issuers/npqlh0acRFG5pKKbnb4SRg', +// name: 'EducateWorkforce', +// image: 'https://media.us.badgr.io/uploads/issuers/issuer_logo_77bb4498-838b-45b7-8722-22878fedb5e8.svg', +// email: 'cucwd.developer@gmail.com', +// description: 'An online learning solution offered with partnering 2-year colleges to help integrate' +// 'web and digital solutions into their existing courses. The platform was designed by' +// 'multiple instructional design, usability, and computing experts to include research-based' +// 'learning features.', +// url: 'https://ew-localhost.com' // } // } // }, Factory.define('badge_class') .attrs({ - slug: "special_award", - issuing_component: "openedx__course", - display_name: "Very Special Award", - course_id: "course-v1:edX+DemoX+Demo_Course", - description: "Awarded for people who did something incredibly special", - criteria: "Do something incredibly special.", - image: "http://example.com/media/badge_classes/badges/special_xdpqpBv_9FYOZwN.png" + slug: 'special_award', + issuing_component: 'openedx__course', + display_name: 'Very Special Award', + course_id: 'course-v1:edX+DemoX+Demo_Course', + description: 'Awarded for people who did something incredibly special', + criteria: 'Do something incredibly special.', + image: 'http://example.com/media/badge_classes/badges/special_xdpqpBv_9FYOZwN.png', }); Factory.define('assertion') .attrs({ - issuedOn: "2019-04-20T02:43:06.566955Z", - expires: "2019-04-30T00:00:00.000000Z", + issuedOn: '2019-04-20T02:43:06.566955Z', + expires: '2019-04-30T00:00:00.000000Z', revoked: false, - image_url: "http://badges.example.com/media/issued/cd75b69fc1c979fcc1697c8403da2bdf.png", - assertion_url: "http://badges.example.com/public/assertions/07020647-e772-44dd-98b7-d13d34335ca6", + image_url: 'http://badges.example.com/media/issued/cd75b69fc1c979fcc1697c8403da2bdf.png', + assertion_url: 'http://badges.example.com/public/assertions/07020647-e772-44dd-98b7-d13d34335ca6', }) - + // Default to one recipient. If recipient was given, fill in // whatever attributes might be missing. .attr('recipient', ['recipient'], (recipient) => { if (!recipient) { - recipient = [{}]; + return {}; } return recipient.map((data) => Factory.attributes('recipient', data)); }) @@ -95,26 +98,24 @@ Factory.define('assertion') // whatever attributes might be missing. .attr('issuer', ['issuer'], (issuer) => { if (!issuer) { - issuer = [{}]; + return {}; } return issuer.map((data) => Factory.attributes('issuer', data)); - }) - + }); Factory.define('recipient') .attrs({ - plaintextIdentity: "john.doe@example.com" - }) + plaintextIdentity: 'john.doe@example.com', + }); Factory.define('issuer') .attrs({ - entityType: "Issuer", - entityId: "npqlh0acRFG5pKKbnb4SRg", - openBadgeId: "https://api.badgr.io/public/issuers/npqlh0acRFG5pKKbnb4SRg", - name: "EducateWorkforce", - image: "https://media.us.badgr.io/uploads/issuers/issuer_logo_77bb4498-838b-45b7-8722-22878fedb5e8.svg", - email: "cucwd.developer@gmail.com", - description: "An online learning solution offered with partnering 2-year colleges to help integrate web and digital solutions into their existing courses. The platform was designed by multiple instructional design, usability, and computing experts to include research-based learning features.", - url: "https://ew-localhost.com" - }) - + entityType: 'Issuer', + entityId: 'npqlh0acRFG5pKKbnb4SRg', + openBadgeId: 'https://api.badgr.io/public/issuers/npqlh0acRFG5pKKbnb4SRg', + name: 'EducateWorkforce', + image: 'https://media.us.badgr.io/uploads/issuers/issuer_logo_77bb4498-838b-45b7-8722-22878fedb5e8.svg', + email: 'cucwd.developer@gmail.com', + description: 'An online learning solution offered with partnering 2-year colleges to help integrate web and digital solutions into their existing courses. The platform was designed by multiple instructional design, usability, and computing experts to include research-based learning features.', + url: 'https://ew-localhost.com', + }); diff --git a/src/course-home/data/__factories__/badgeProgressTabData.factory.js b/src/course-home/data/__factories__/badgeProgressTabData.factory.js index c57a5ad0b3..6086b31124 100644 --- a/src/course-home/data/__factories__/badgeProgressTabData.factory.js +++ b/src/course-home/data/__factories__/badgeProgressTabData.factory.js @@ -9,53 +9,53 @@ Factory.define('badgeProgressTabData') .sequence('id', (i) => `course-v1:edX+DemoX+Demo_Course_${i}`) .sequence('user_id') .attrs({ - user_name: 'TestUser', - name: 'Test Username', - email: 'test@edx.org', + user_name: 'TestUser', + name: 'Test Username', + email: 'test@edx.org', }) .attrs( - 'progress', ['id'], (id) => { - const progress = [ - Factory.build( - 'badge-progress', - { - course_id: 'course-v1:edX+DemoX+Demo_Course', - block_id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@dc1e160e5dc348a48a98fa0f4a6e8675', - block_display_name: 'Example Week 1: Getting Started', - event_type: 'chapter_complete', - badge_class: { - slug: 'special_award', - issuing_component: 'openedx__course', - display_name: 'Very Special Award', - course_id: 'course-v1:edX+DemoX+Demo_Course', - description: 'Awarded for people who did something incredibly special', - criteria: 'Do something incredibly special.', - image: 'http://example.com/media/badge_classes/badges/special_xdpqpBv_9FYOZwN.png' - }, - assertion: { - issuedOn: '2019-04-20T02:43:06.566955Z', - expires: '2019-04-30T00:00:00.000000Z', - revoked: false, - image_url: 'http://badges.example.com/media/issued/cd75b69fc1c979fcc1697c8403da2bdf.png', - assertion_url: 'http://badges.example.com/public/assertions/07020647-e772-44dd-98b7-d13d34335ca6', - recipient: { - plaintextIdentity: 'john.doe@example.com' - }, - issuer: { - entityType: 'Issuer', - entityId: 'npqlh0acRFG5pKKbnb4SRg', - openBadgeId: 'https://api.badgr.io/public/issuers/npqlh0acRFG5pKKbnb4SRg', - name: 'EducateWorkforce', - image: 'https://media.us.badgr.io/uploads/issuers/issuer_logo_77bb4498-838b-45b7-8722-22878fedb5e8.svg', - email: 'cucwd.developer@gmail.com', - description: 'An online learning solution offered with partnering 2-year colleges to help integrate web and digital solutions into their existing courses. The platform was designed by multiple instructional design, usability, and computing experts to include research-based learning features.', - url: 'https://ew-localhost.com' - } - } - } - ), - ]; + 'progress', ['id'], (id) => { // eslint-disable-line no-unused-vars + const progress = [ + Factory.build( + 'badge-progress', + { + course_id: 'course-v1:edX+DemoX+Demo_Course', + block_id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@dc1e160e5dc348a48a98fa0f4a6e8675', + block_display_name: 'Example Week 1: Getting Started', + event_type: 'chapter_complete', + badge_class: { + slug: 'special_award', + issuing_component: 'openedx__course', + display_name: 'Very Special Award', + course_id: 'course-v1:edX+DemoX+Demo_Course', + description: 'Awarded for people who did something incredibly special', + criteria: 'Do something incredibly special.', + image: 'http://example.com/media/badge_classes/badges/special_xdpqpBv_9FYOZwN.png', + }, + assertion: { + issuedOn: '2019-04-20T02:43:06.566955Z', + expires: '2019-04-30T00:00:00.000000Z', + revoked: false, + image_url: 'http://badges.example.com/media/issued/cd75b69fc1c979fcc1697c8403da2bdf.png', + assertion_url: 'http://badges.example.com/public/assertions/07020647-e772-44dd-98b7-d13d34335ca6', + recipient: { + plaintextIdentity: 'john.doe@example.com', + }, + issuer: { + entityType: 'Issuer', + entityId: 'npqlh0acRFG5pKKbnb4SRg', + openBadgeId: 'https://api.badgr.io/public/issuers/npqlh0acRFG5pKKbnb4SRg', + name: 'EducateWorkforce', + image: 'https://media.us.badgr.io/uploads/issuers/issuer_logo_77bb4498-838b-45b7-8722-22878fedb5e8.svg', + email: 'cucwd.developer@gmail.com', + description: 'An online learning solution offered with partnering 2-year colleges to help integrate web and digital solutions into their existing courses. The platform was designed by multiple instructional design, usability, and computing experts to include research-based learning features.', + url: 'https://ew-localhost.com', + }, + }, + }, + ), + ]; - return progress; - }, + return progress; + }, ); diff --git a/src/course-home/data/__factories__/outlineTabData.factory.js b/src/course-home/data/__factories__/outlineTabData.factory.js index c9f5d39be7..ee501162f2 100644 --- a/src/course-home/data/__factories__/outlineTabData.factory.js +++ b/src/course-home/data/__factories__/outlineTabData.factory.js @@ -1,16 +1,63 @@ import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies -import '../../../courseware/data/__factories__/courseBlocks.factory'; +import { buildMinimalCourseBlocks } from '../../../shared/data/__factories__/courseBlocks.factory'; Factory.define('outlineTabData') .option('courseId', 'course-v1:edX+DemoX+Demo_Course') .option('host', 'http://localhost:18000') - .attr('course_tools', ['host', 'courseId'], (host, courseId) => ({ - analytics_id: 'edx.bookmarks', - title: 'Bookmarks', - url: `${host}/courses/${courseId}/bookmarks/`, + .option('date_blocks', []) + .attr('course_blocks', ['courseId'], courseId => { + const { courseBlocks } = buildMinimalCourseBlocks(courseId); + return { + blocks: courseBlocks.blocks, + }; + }) + .attr('dates_widget', ['date_blocks'], (dateBlocks) => ({ + course_date_blocks: dateBlocks, })) - .attr('course_blocks', ['courseId'], courseId => ({ - blocks: Factory.build('courseBlocks', { courseId }).blocks, + .attr('resume_course', ['host', 'courseId'], (host, courseId) => ({ + has_visited_course: false, + url: `${host}/courses/${courseId}/jump_to/block-v1:edX+Test+Block@12345abcde`, })) - .attr('handouts_html', [], () => '
  • Handout 1
'); + .attr('verified_mode', ['host'], (host) => ({ + access_expiration_date: '2050-01-01T12:00:00', + currency: 'USD', + currency_symbol: '$', + price: 149, + sku: 'ABCD1234', + upgrade_url: `${host}/dashboard`, + })) + .attrs({ + has_scheduled_content: null, + access_expiration: null, + can_show_upgrade_sock: false, + cert_data: { + cert_status: null, + cert_web_view_url: null, + certificate_available_date: null, + download_url: null, + }, + course_goals: { + goal_options: [], + selected_goal: null, + }, + course_tools: [ + { + analytics_id: 'edx.bookmarks', + title: 'Bookmarks', + url: 'https://example.com/bookmarks', + }, + ], + dates_banner_info: { + content_type_gating_enabled: false, + missed_gated_content: false, + missed_deadlines: false, + }, + enroll_alert: { + can_enroll: true, + extra_text: 'Contact the administrator.', + }, + handouts_html: '
  • Handout 1
', + offer: null, + welcome_message_html: '

Welcome to this course!

', + }); diff --git a/src/course-home/data/thunks.js b/src/course-home/data/thunks.js index 665308ec3a..be5a9cbc41 100644 --- a/src/course-home/data/thunks.js +++ b/src/course-home/data/thunks.js @@ -25,7 +25,6 @@ import { fetchTabSuccess, setCallToActionToast, } from './slice'; -import { debug } from 'util'; const eventTypes = { POST_EVENT: 'post_event', @@ -74,8 +73,6 @@ export function fetchTab(courseId, tab, getTabData, targetUserId) { dispatch(fetchTabFailure({ courseId })); } }); - - }; } diff --git a/src/course-home/outline-tab/OutlineTab.jsx b/src/course-home/outline-tab/OutlineTab.jsx index 8533f5c5dd..0b47b55896 100644 --- a/src/course-home/outline-tab/OutlineTab.jsx +++ b/src/course-home/outline-tab/OutlineTab.jsx @@ -195,7 +195,6 @@ function OutlineTab({ intl }) { {courses[rootCourseId].sectionIds.map((sectionId) => (
{badgeProgressUrl && ( - - - - )} + + + + )} ) : ( <> diff --git a/src/index.jsx b/src/index.jsx index 63cb5b128e..c668c13c9d 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -26,7 +26,9 @@ import GoalUnsubscribe from './course-home/goal-unsubscribe'; import ProgressTab from './course-home/progress-tab/ProgressTab'; import { TabContainer } from './tab-page'; -import { fetchBadgeProgressTab, fetchBadgeLeaderboardTab, fetchDatesTab, fetchOutlineTab, fetchProgressTab } from './course-home/data'; +import { + fetchBadgeProgressTab, fetchBadgeLeaderboardTab, fetchDatesTab, fetchOutlineTab, fetchProgressTab, +} from './course-home/data'; import { fetchCourse } from './courseware/data'; import initializeStore from './store'; import NoticesProvider from './generic/notices'; From 27ca5fb80a21184f0bc07063346bc1879d92b635 Mon Sep 17 00:00:00 2001 From: Zachary Trabookis Date: Wed, 20 Apr 2022 16:01:43 -0400 Subject: [PATCH 10/17] test(badges): Reset testing back to `maple.1` for some files. With changes merged in from Badges feature change some `maple.1` files were updated so we're reverting them back to the original baseline. Also for now we're not testing the Badges feature with this commit. --- .../badges-tab/BadgeProgressTab.test.jsx | 114 ++-- .../courseHomeMetadata.factory.js | 32 +- .../__factories__/datesTabData.factory.js | 208 +++++- src/course-home/data/__factories__/index.js | 2 + .../data/__snapshots__/redux.test.js.snap | 595 ++++++++++++++++-- src/course-home/data/redux.test.js | 110 +++- src/index.scss | 2 +- 7 files changed, 899 insertions(+), 164 deletions(-) diff --git a/src/course-home/badges-tab/BadgeProgressTab.test.jsx b/src/course-home/badges-tab/BadgeProgressTab.test.jsx index e5e0949610..540a685bfc 100644 --- a/src/course-home/badges-tab/BadgeProgressTab.test.jsx +++ b/src/course-home/badges-tab/BadgeProgressTab.test.jsx @@ -1,77 +1,83 @@ +/* eslint-disable */ // TODO Need to complete these tests. -import React from 'react'; -import { Route } from 'react-router'; -import MockAdapter from 'axios-mock-adapter'; -import { Factory } from 'rosie'; -import { getConfig, history } from '@edx/frontend-platform'; +// import React from 'react'; +// import { Route } from 'react-router'; +// import MockAdapter from 'axios-mock-adapter'; +// import { Factory } from 'rosie'; +// import { getConfig, history } from '@edx/frontend-platform'; // import { sendTrackEvent } from '@edx/frontend-platform/analytics'; -import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { AppProvider } from '@edx/frontend-platform/react'; +// import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +// import { AppProvider } from '@edx/frontend-platform/react'; // import { waitForElementToBeRemoved } from '@testing-library/dom'; -import { render, screen, within } from '@testing-library/react'; // eslint-disable-line no-unused-vars +// import { render, screen, within } from '@testing-library/react'; // eslint-disable-line no-unused-vars // import userEvent from '@testing-library/user-event'; -import BadgeProgressTab from './BadgeProgressTab'; -import { fetchBadgeProgressTab } from '../data'; +// import BadgeProgressTab from './BadgeProgressTab'; +// import { fetchBadgeProgressTab } from '../data'; import { fireEvent, initializeMockApp, waitFor } from '../../setupTest'; // eslint-disable-line no-unused-vars import initializeStore from '../../store'; -import { TabContainer } from '../../tab-page'; -import { appendBrowserTimezoneToUrl } from '../../utils'; -import { UserMessagesProvider } from '../../generic/user-messages'; +// import { TabContainer } from '../../tab-page'; +// import { appendBrowserTimezoneToUrl } from '../../utils'; +// import { UserMessagesProvider } from '../../generic/user-messages'; initializeMockApp(); jest.mock('@edx/frontend-platform/analytics'); describe('BadgeProgressTab', () => { let axiosMock; - let store; let component; - beforeEach(() => { - axiosMock = new MockAdapter(getAuthenticatedHttpClient()); - store = initializeStore(); - component = ( - - - - - - - - - - ); - }); - - const badgeProgressTabData = Factory.build('badgeProgressTabData'); - let courseMetadata = Factory.build('courseHomeMetadata', { user_timezone: 'America/New_York' }); - const { id: courseId } = courseMetadata; + const store = initializeStore(); - const badgeProgressUrl = `${getConfig().LMS_BASE_URL}/api/badges/v1/progress/${courseId}`; - let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/course_home/course_metadata/${courseId}`; - courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl); + // beforeEach(() => { + // axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + // store = initializeStore(); + // // component = ( + // // + // // + // // + // // + // // + // // + // // + // // + // // + // // ); + // }); - function setMetadata(attributes, options) { // eslint-disable-line no-unused-vars - courseMetadata = Factory.build('courseHomeMetadata', { id: courseId, ...attributes }, options); - axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata); - } + // const badgeProgressTabData = Factory.build('badgeProgressTabData'); + // let courseMetadata = Factory.build('courseHomeMetadata', { user_timezone: 'America/New_York' }); + // const { id: courseId } = courseMetadata; - describe('when receiving a full set of dates data', () => { - beforeEach(() => { - axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata); - axiosMock.onGet(badgeProgressUrl).reply(200, badgeProgressTabData); - history.push(`/course/${courseId}/badges/progress`); // so tab can pull course id from url + // const badgeProgressUrl = `${getConfig().LMS_BASE_URL}/api/badges/v1/progress/${courseId}`; + // let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/course_home/course_metadata/${courseId}`; + // courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl); - render(component); - }); + // function setMetadata(attributes, options) { // eslint-disable-line no-unused-vars + // courseMetadata = Factory.build('courseHomeMetadata', { id: courseId, ...attributes }, options); + // axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata); + // } - it('handles unreleased & complete', async () => { - // const { header } = await getDay('Sun, May 3, 2020'); - // const badges = within(header).getAllByTestId('dates-badge'); - // expect(badges).toHaveLength(2); - // expect(badges[0]).toHaveTextContent('Completed'); - // expect(badges[1]).toHaveTextContent('Not yet released'); - }); + it('Todo: Need Test', () => { + }); + + // describe('when receiving a full set of dates data', () => { + // beforeEach(() => { + // axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata); + // axiosMock.onGet(badgeProgressUrl).reply(200, badgeProgressTabData); + // history.push(`/course/${courseId}/badges/progress`); // so tab can pull course id from url + + // render(component); + // }); + + // it('handles unreleased & complete', async () => { + // // const { header } = await getDay('Sun, May 3, 2020'); + // // const badges = within(header).getAllByTestId('dates-badge'); + // // expect(badges).toHaveLength(2); + // // expect(badges[0]).toHaveTextContent('Completed'); + // // expect(badges[1]).toHaveTextContent('Not yet released'); + // }); + // }); }); diff --git a/src/course-home/data/__factories__/courseHomeMetadata.factory.js b/src/course-home/data/__factories__/courseHomeMetadata.factory.js index 89911633cc..f6fe95b01b 100644 --- a/src/course-home/data/__factories__/courseHomeMetadata.factory.js +++ b/src/course-home/data/__factories__/courseHomeMetadata.factory.js @@ -1,23 +1,23 @@ import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies +import courseMetadataBase from '../../../shared/data/__factories__/courseMetadataBase.factory'; + Factory.define('courseHomeMetadata') - .sequence( - 'course_id', - (courseId) => `course-v1:edX+DemoX+Demo_Course_${courseId}`, - ) - .option('courseTabs', []) + .extend(courseMetadataBase) .option('host', 'http://localhost:18000') .attrs({ - is_staff: false, - number: 'DemoX', - org: 'edX', title: 'Demonstration Course', is_self_paced: false, - }) - .attr('tabs', ['courseTabs', 'host'], (courseTabs, host) => courseTabs.map( - tab => ({ - tab_id: tab.slug, - title: tab.title, - url: `${host}${tab.url}`, - }), - )); + 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', + user_timezone: 'UTC', + }); diff --git a/src/course-home/data/__factories__/datesTabData.factory.js b/src/course-home/data/__factories__/datesTabData.factory.js index ee3b7c352e..9ece9a2d07 100644 --- a/src/course-home/data/__factories__/datesTabData.factory.js +++ b/src/course-home/data/__factories__/datesTabData.factory.js @@ -1,26 +1,222 @@ import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies +// Sample data helpful when developing & testing, to see a variety of configurations. +// This set of data is not realistic (mix of having access and not), but it +// is intended to demonstrate many UI results. Factory.define('datesTabData') .attrs({ dates_banner_info: { content_type_gating_enabled: false, missed_gated_content: false, missed_deadlines: false, + verified_upgrade_link: 'http://localhost:18130/basket/add/?sku=8CF08E5', }, course_date_blocks: [ { - date: '2013-02-05T05:00:00Z', + date: '2020-05-01T17:59:41Z', date_type: 'course-start-date', description: '', learner_has_access: true, link: '', title: 'Course Starts', - extraInfo: '', + extra_info: null, + }, + { + assignment_type: 'Homework', + complete: true, + date: '2020-05-04T02:59:40.942669Z', + date_type: 'assignment-due-date', + description: '', + learner_has_access: true, + title: 'Multi Badges Completed', + extra_info: null, + }, + { + assignment_type: 'Homework', + date: '2020-05-05T02:59:40.942669Z', + date_type: 'assignment-due-date', + description: '', + learner_has_access: true, + title: 'Multi Badges Past Due', + extra_info: null, + }, + { + assignment_type: 'Homework', + date: '2020-05-27T02:59:40.942669Z', + date_type: 'assignment-due-date', + description: '', + learner_has_access: true, + link: 'https://example.com/', + title: 'Both Past Due 1', + extra_info: null, + }, + { + assignment_type: 'Homework', + date: '2020-05-27T02:59:40.942669Z', + date_type: 'assignment-due-date', + description: '', + learner_has_access: true, + link: 'https://example.com/', + title: 'Both Past Due 2', + extra_info: null, + }, + { + assignment_type: 'Homework', + complete: true, + date: '2020-05-28T08:59:40.942669Z', + date_type: 'assignment-due-date', + description: '', + learner_has_access: true, + link: 'https://example.com/', + title: 'One Completed/Due 1', + extra_info: null, + }, + { + assignment_type: 'Homework', + date: '2020-05-28T08:59:40.942669Z', + date_type: 'assignment-due-date', + description: '', + learner_has_access: true, + link: 'https://example.com/', + title: 'One Completed/Due 2', + extra_info: null, + }, + { + assignment_type: 'Homework', + complete: true, + date: '2020-05-29T08:59:40.942669Z', + date_type: 'assignment-due-date', + description: '', + learner_has_access: true, + link: 'https://example.com/', + title: 'Both Completed 1', + extra_info: null, + }, + { + assignment_type: 'Homework', + complete: true, + date: '2020-05-29T08:59:40.942669Z', + date_type: 'assignment-due-date', + description: '', + learner_has_access: true, + link: 'https://example.com/', + title: 'Both Completed 2', + extra_info: null, + }, + { + date: '2020-06-16T17:59:40.942669Z', + date_type: 'verified-upgrade-deadline', + description: "Don't miss the opportunity to highlight your new knowledge and skills by earning a verified certificate.", + learner_has_access: true, + link: 'https://example.com/', + title: 'Upgrade to Verified Certificate', + extra_info: null, + }, + { + assignment_type: 'Homework', + date: '2030-08-17T05:59:40.942669Z', + date_type: 'assignment-due-date', + description: '', + learner_has_access: false, + link: 'https://example.com/', + title: 'One Verified 1', + extra_info: null, + }, + { + assignment_type: 'Homework', + date: '2030-08-17T05:59:40.942669Z', + date_type: 'assignment-due-date', + description: '', + learner_has_access: true, + link: 'https://example.com/', + title: 'One Verified 2', + extra_info: null, + }, + { + assignment_type: 'Homework', + date: '2030-08-17T05:59:40.942669Z', + date_type: 'assignment-due-date', + description: '', + learner_has_access: true, + link: 'https://example.com/', + title: 'ORA Verified 2', + extra_info: "ORA Dates are set by the instructor, and can't be changed", + }, + { + assignment_type: 'Homework', + date: '2030-08-18T05:59:40.942669Z', + date_type: 'assignment-due-date', + description: '', + learner_has_access: false, + link: 'https://example.com/', + title: 'Both Verified 1', + extra_info: null, + }, + { + assignment_type: 'Homework', + date: '2030-08-18T05:59:40.942669Z', + date_type: 'assignment-due-date', + description: '', + learner_has_access: false, + link: 'https://example.com/', + title: 'Both Verified 2', + extra_info: null, + }, + { + assignment_type: 'Homework', + date: '2030-08-19T05:59:40.942669Z', + date_type: 'assignment-due-date', + description: '', + learner_has_access: true, + title: 'One Unreleased 1', + }, + { + assignment_type: 'Homework', + date: '2030-08-19T05:59:40.942669Z', + date_type: 'assignment-due-date', + description: '', + learner_has_access: true, + link: 'https://example.com/', + title: 'One Unreleased 2', + extra_info: null, + }, + { + assignment_type: 'Homework', + date: '2030-08-20T05:59:40.942669Z', + date_type: 'assignment-due-date', + description: '', + learner_has_access: true, + title: 'Both Unreleased 1', + extra_info: null, + }, + { + assignment_type: 'Homework', + date: '2030-08-20T05:59:40.942669Z', + date_type: 'assignment-due-date', + description: '', + learner_has_access: true, + title: 'Both Unreleased 2', + extra_info: null, + }, + { + date: '2030-08-23T00:00:00Z', + date_type: 'course-end-date', + description: '', + learner_has_access: true, + link: '', + title: 'Course Ends', + extra_info: null, + }, + { + date: '2030-09-01T00:00:00Z', + date_type: 'verification-deadline-date', + description: 'You must successfully complete verification before this date to qualify for a Verified Certificate.', + learner_has_access: false, + link: 'https://example.com/', + title: 'Verification Deadline', + extra_info: null, }, ], - missed_deadlines: false, - missed_gated_content: false, + has_ended: false, learner_is_full_access: true, - user_timezone: null, - verified_upgrade_link: 'http://localhost:18130/basket/add/?sku=8CF08E5', }); diff --git a/src/course-home/data/__factories__/index.js b/src/course-home/data/__factories__/index.js index a2680575c5..e421d00c7c 100644 --- a/src/course-home/data/__factories__/index.js +++ b/src/course-home/data/__factories__/index.js @@ -1,3 +1,5 @@ import './courseHomeMetadata.factory'; import './datesTabData.factory'; import './outlineTabData.factory'; +import './progressTabData.factory'; +import './upgradeNotificationData.factory'; diff --git a/src/course-home/data/__snapshots__/redux.test.js.snap b/src/course-home/data/__snapshots__/redux.test.js.snap index 9dad7db432..700aa35448 100644 --- a/src/course-home/data/__snapshots__/redux.test.js.snap +++ b/src/course-home/data/__snapshots__/redux.test.js.snap @@ -1,26 +1,14 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Data layer integration tests Should initialize store 1`] = ` -Object { - "courseHome": Object { - "courseId": null, - "courseStatus": "loading", - }, - "courseware": Object { - "courseId": null, - "courseStatus": "loading", - "sequenceId": null, - "sequenceStatus": "loading", - }, - "models": Object {}, -} -`; - exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize, and save metadata 1`] = ` Object { "courseHome": Object { "courseId": "course-v1:edX+DemoX+Demo_Course_1", "courseStatus": "loaded", + "targetUserId": undefined, + "toastBodyLink": null, + "toastBodyText": null, + "toastHeader": "", }, "courseware": Object { "courseId": null, @@ -29,17 +17,29 @@ Object { "sequenceStatus": "loading", }, "models": Object { - "courses": Object { + "courseHomeMeta": Object { "course-v1:edX+DemoX+Demo_Course_1": Object { - "courseId": "course-v1:edX+DemoX+Demo_Course_1", + "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, + "isMasquerading": false, "isSelfPaced": false, "isStaff": false, "number": "DemoX", "org": "edX", + "originalUserIsStaff": false, + "start": "2013-02-05T05:00:00Z", "tabs": Array [ Object { - "slug": "courseware", + "slug": "outline", "title": "Course", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/course/", }, @@ -63,37 +63,249 @@ Object { "title": "Instructor", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/instructor", }, + Object { + "slug": "dates", + "title": "Dates", + "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/dates", + }, + Object { + "slug": "badges-progress", + "title": "Badges", + "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/badges-progress", + }, ], "title": "Demonstration Course", + "userTimezone": "UTC", + "verifiedMode": Object { + "currencySymbol": "$", + "price": 10, + "upgradeUrl": "test", + }, }, }, "dates": Object { "course-v1:edX+DemoX+Demo_Course_1": Object { "courseDateBlocks": Array [ Object { - "date": "2013-02-05T05:00:00Z", + "date": "2020-05-01T17:59:41Z", "dateType": "course-start-date", "description": "", - "extraInfo": "", + "extraInfo": null, "learnerHasAccess": true, "link": "", "title": "Course Starts", }, + Object { + "assignmentType": "Homework", + "complete": true, + "date": "2020-05-04T02:59:40.942669Z", + "dateType": "assignment-due-date", + "description": "", + "extraInfo": null, + "learnerHasAccess": true, + "title": "Multi Badges Completed", + }, + Object { + "assignmentType": "Homework", + "date": "2020-05-05T02:59:40.942669Z", + "dateType": "assignment-due-date", + "description": "", + "extraInfo": null, + "learnerHasAccess": true, + "title": "Multi Badges Past Due", + }, + Object { + "assignmentType": "Homework", + "date": "2020-05-27T02:59:40.942669Z", + "dateType": "assignment-due-date", + "description": "", + "extraInfo": null, + "learnerHasAccess": true, + "link": "https://example.com/", + "title": "Both Past Due 1", + }, + Object { + "assignmentType": "Homework", + "date": "2020-05-27T02:59:40.942669Z", + "dateType": "assignment-due-date", + "description": "", + "extraInfo": null, + "learnerHasAccess": true, + "link": "https://example.com/", + "title": "Both Past Due 2", + }, + Object { + "assignmentType": "Homework", + "complete": true, + "date": "2020-05-28T08:59:40.942669Z", + "dateType": "assignment-due-date", + "description": "", + "extraInfo": null, + "learnerHasAccess": true, + "link": "https://example.com/", + "title": "One Completed/Due 1", + }, + Object { + "assignmentType": "Homework", + "date": "2020-05-28T08:59:40.942669Z", + "dateType": "assignment-due-date", + "description": "", + "extraInfo": null, + "learnerHasAccess": true, + "link": "https://example.com/", + "title": "One Completed/Due 2", + }, + Object { + "assignmentType": "Homework", + "complete": true, + "date": "2020-05-29T08:59:40.942669Z", + "dateType": "assignment-due-date", + "description": "", + "extraInfo": null, + "learnerHasAccess": true, + "link": "https://example.com/", + "title": "Both Completed 1", + }, + Object { + "assignmentType": "Homework", + "complete": true, + "date": "2020-05-29T08:59:40.942669Z", + "dateType": "assignment-due-date", + "description": "", + "extraInfo": null, + "learnerHasAccess": true, + "link": "https://example.com/", + "title": "Both Completed 2", + }, + Object { + "date": "2020-06-16T17:59:40.942669Z", + "dateType": "verified-upgrade-deadline", + "description": "Don't miss the opportunity to highlight your new knowledge and skills by earning a verified certificate.", + "extraInfo": null, + "learnerHasAccess": true, + "link": "https://example.com/", + "title": "Upgrade to Verified Certificate", + }, + Object { + "assignmentType": "Homework", + "date": "2030-08-17T05:59:40.942669Z", + "dateType": "assignment-due-date", + "description": "", + "extraInfo": null, + "learnerHasAccess": false, + "link": "https://example.com/", + "title": "One Verified 1", + }, + Object { + "assignmentType": "Homework", + "date": "2030-08-17T05:59:40.942669Z", + "dateType": "assignment-due-date", + "description": "", + "extraInfo": null, + "learnerHasAccess": true, + "link": "https://example.com/", + "title": "One Verified 2", + }, + Object { + "assignmentType": "Homework", + "date": "2030-08-17T05:59:40.942669Z", + "dateType": "assignment-due-date", + "description": "", + "extraInfo": "ORA Dates are set by the instructor, and can't be changed", + "learnerHasAccess": true, + "link": "https://example.com/", + "title": "ORA Verified 2", + }, + Object { + "assignmentType": "Homework", + "date": "2030-08-18T05:59:40.942669Z", + "dateType": "assignment-due-date", + "description": "", + "extraInfo": null, + "learnerHasAccess": false, + "link": "https://example.com/", + "title": "Both Verified 1", + }, + Object { + "assignmentType": "Homework", + "date": "2030-08-18T05:59:40.942669Z", + "dateType": "assignment-due-date", + "description": "", + "extraInfo": null, + "learnerHasAccess": false, + "link": "https://example.com/", + "title": "Both Verified 2", + }, + Object { + "assignmentType": "Homework", + "date": "2030-08-19T05:59:40.942669Z", + "dateType": "assignment-due-date", + "description": "", + "learnerHasAccess": true, + "title": "One Unreleased 1", + }, + Object { + "assignmentType": "Homework", + "date": "2030-08-19T05:59:40.942669Z", + "dateType": "assignment-due-date", + "description": "", + "extraInfo": null, + "learnerHasAccess": true, + "link": "https://example.com/", + "title": "One Unreleased 2", + }, + Object { + "assignmentType": "Homework", + "date": "2030-08-20T05:59:40.942669Z", + "dateType": "assignment-due-date", + "description": "", + "extraInfo": null, + "learnerHasAccess": true, + "title": "Both Unreleased 1", + }, + Object { + "assignmentType": "Homework", + "date": "2030-08-20T05:59:40.942669Z", + "dateType": "assignment-due-date", + "description": "", + "extraInfo": null, + "learnerHasAccess": true, + "title": "Both Unreleased 2", + }, + Object { + "date": "2030-08-23T00:00:00Z", + "dateType": "course-end-date", + "description": "", + "extraInfo": null, + "learnerHasAccess": true, + "link": "", + "title": "Course Ends", + }, + Object { + "date": "2030-09-01T00:00:00Z", + "dateType": "verification-deadline-date", + "description": "You must successfully complete verification before this date to qualify for a Verified Certificate.", + "extraInfo": null, + "learnerHasAccess": false, + "link": "https://example.com/", + "title": "Verification Deadline", + }, ], "datesBannerInfo": Object { "contentTypeGatingEnabled": false, "missedDeadlines": false, "missedGatedContent": false, + "verifiedUpgradeLink": "http://localhost:18130/basket/add/?sku=8CF08E5", }, + "hasEnded": false, "id": "course-v1:edX+DemoX+Demo_Course_1", "learnerIsFullAccess": true, - "missedDeadlines": false, - "missedGatedContent": false, - "userTimezone": null, - "verifiedUpgradeLink": "http://localhost:18130/basket/add/?sku=8CF08E5", }, }, }, + "recommendations": Object { + "recommendationsStatus": "loading", + }, } `; @@ -102,6 +314,10 @@ Object { "courseHome": Object { "courseId": "course-v1:edX+DemoX+Demo_Course_1", "courseStatus": "loaded", + "targetUserId": undefined, + "toastBodyLink": null, + "toastBodyText": null, + "toastHeader": "", }, "courseware": Object { "courseId": null, @@ -110,17 +326,29 @@ Object { "sequenceStatus": "loading", }, "models": Object { - "courses": Object { + "courseHomeMeta": Object { "course-v1:edX+DemoX+Demo_Course_1": Object { - "courseId": "course-v1:edX+DemoX+Demo_Course_1", + "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, + "isMasquerading": false, "isSelfPaced": false, "isStaff": false, "number": "DemoX", "org": "edX", + "originalUserIsStaff": false, + "start": "2013-02-05T05:00:00Z", "tabs": Array [ Object { - "slug": "courseware", + "slug": "outline", "title": "Course", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/course/", }, @@ -144,63 +372,318 @@ Object { "title": "Instructor", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/instructor", }, + Object { + "slug": "dates", + "title": "Dates", + "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/dates", + }, + Object { + "slug": "badges-progress", + "title": "Badges", + "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/badges-progress", + }, ], "title": "Demonstration Course", + "userTimezone": "UTC", + "verifiedMode": Object { + "currencySymbol": "$", + "price": 10, + "upgradeUrl": "test", + }, }, }, "outline": Object { "course-v1:edX+DemoX+Demo_Course_1": Object { + "accessExpiration": null, + "canShowUpgradeSock": false, + "certData": Object { + "certStatus": null, + "certWebViewUrl": null, + "certificateAvailableDate": null, + "downloadUrl": null, + }, "courseBlocks": Object { "courses": Object { - "block-v1:edX+DemoX+Demo_Course+type@course+block@bcdabcdabcdabcdabcdabcdabcdabcd4": Object { + "block-v1:edX+DemoX+Demo_Course+type@course+block@bcdabcdabcdabcdabcdabcdabcdabcd3": Object { + "hasScheduledContent": false, "id": "course-v1:edX+DemoX+Demo_Course_1", "sectionIds": Array [ - "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd3", + "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2", ], - "title": "bcdabcdabcdabcdabcdabcdabcdabcd4", + "title": "bcdabcdabcdabcdabcdabcdabcdabcd3", }, }, "sections": Object { - "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd3": Object { + "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2": Object { + "badgeProgress": undefined, + "complete": false, "courseId": "course-v1:edX+DemoX+Demo_Course_1", - "id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd3", + "id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2", + "resumeBlock": false, "sequenceIds": Array [ - "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd2", + "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1", ], - "title": "bcdabcdabcdabcdabcdabcdabcdabcd3", + "title": "Title of Section", }, }, "sequences": Object { - "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd2": Object { - "id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd2", - "lmsWebUrl": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd2", - "sectionId": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd3", - "title": "bcdabcdabcdabcdabcdabcdabcdabcd2", - "unitIds": Array [ - "block-v1:edX+DemoX+Demo_Course+type@vertical+block@bcdabcdabcdabcdabcdabcdabcdabcd1", - ], + "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1": Object { + "complete": false, + "description": null, + "due": null, + "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", + "sectionId": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2", + "showLink": true, + "title": "Title of Sequence", }, }, - "units": Object { - "block-v1:edX+DemoX+Demo_Course+type@vertical+block@bcdabcdabcdabcdabcdabcdabcdabcd1": Object { - "graded": false, - "id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@bcdabcdabcdabcdabcdabcdabcdabcd1", - "lmsWebUrl": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@vertical+block@bcdabcdabcdabcdabcdabcdabcdabcd1", - "sequenceId": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd2", - "title": "bcdabcdabcdabcdabcdabcdabcdabcd1", - }, + }, + "courseGoals": Object { + "goalOptions": Array [], + "selectedGoal": null, + }, + "courseTools": Array [ + Object { + "analyticsId": "edx.bookmarks", + "title": "Bookmarks", + "url": "https://example.com/bookmarks", }, + ], + "datesBannerInfo": Object { + "contentTypeGatingEnabled": false, + "missedDeadlines": false, + "missedGatedContent": false, }, - "courseTools": Object { - "analyticsId": "edx.bookmarks", - "title": "Bookmarks", - "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/bookmarks/", + "datesWidget": Object { + "courseDateBlocks": Array [], }, - "datesWidget": undefined, + "enrollAlert": Object { + "canEnroll": true, + "extraText": "Contact the administrator.", + }, + "enrollmentMode": undefined, "handoutsHtml": "
  • Handout 1
", + "hasEnded": undefined, + "hasScheduledContent": null, "id": "course-v1:edX+DemoX+Demo_Course_1", + "offer": null, + "resumeCourse": Object { + "hasVisitedCourse": false, + "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+Test+Block@12345abcde", + }, + "timeOffsetMillis": 0, + "userHasPassingGrade": undefined, + "verifiedMode": Object { + "accessExpirationDate": "2050-01-01T12:00:00", + "currency": "USD", + "currencySymbol": "$", + "price": 149, + "sku": "ABCD1234", + "upgradeUrl": "http://localhost:18000/dashboard", + }, + "welcomeMessageHtml": "

Welcome to this course!

", }, }, }, + "recommendations": Object { + "recommendationsStatus": "loading", + }, +} +`; + +exports[`Data layer integration tests Test fetchProgressTab Should fetch, normalize, and save metadata 1`] = ` +Object { + "courseHome": Object { + "courseId": "course-v1:edX+DemoX+Demo_Course_1", + "courseStatus": "loaded", + "targetUserId": undefined, + "toastBodyLink": null, + "toastBodyText": null, + "toastHeader": "", + }, + "courseware": Object { + "courseId": null, + "courseStatus": "loading", + "sequenceId": null, + "sequenceStatus": "loading", + }, + "models": 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, + "isMasquerading": false, + "isSelfPaced": false, + "isStaff": false, + "number": "DemoX", + "org": "edX", + "originalUserIsStaff": false, + "start": "2013-02-05T05:00:00Z", + "tabs": Array [ + Object { + "slug": "outline", + "title": "Course", + "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/course/", + }, + Object { + "slug": "discussion", + "title": "Discussion", + "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/discussion/forum/", + }, + Object { + "slug": "wiki", + "title": "Wiki", + "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/course_wiki", + }, + Object { + "slug": "progress", + "title": "Progress", + "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/progress", + }, + Object { + "slug": "instructor", + "title": "Instructor", + "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/instructor", + }, + Object { + "slug": "dates", + "title": "Dates", + "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/dates", + }, + Object { + "slug": "badges-progress", + "title": "Badges", + "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/badges-progress", + }, + ], + "title": "Demonstration Course", + "userTimezone": "UTC", + "verifiedMode": Object { + "currencySymbol": "$", + "price": 10, + "upgradeUrl": "test", + }, + }, + }, + "progress": Object { + "course-v1:edX+DemoX+Demo_Course_1": Object { + "accessExpiration": null, + "certificateData": Object {}, + "completionSummary": Object { + "completeCount": 1, + "incompleteCount": 1, + "lockedCount": 0, + }, + "courseGrade": Object { + "isPassing": true, + "letterGrade": "pass", + "percent": 1, + "visiblePercent": 1, + }, + "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 { + "averageGrade": 1, + "numDroppable": 1, + "shortLabel": "HW", + "type": "Homework", + "weight": 1, + "weightedGrade": 1, + }, + ], + "gradeRange": Object { + "pass": 0.75, + }, + }, + "hasScheduledContent": false, + "id": "course-v1:edX+DemoX+Demo_Course_1", + "sectionScores": Array [ + Object { + "displayName": "First section", + "subsections": Array [ + Object { + "assignmentType": "Homework", + "blockKey": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345", + "displayName": "First subsection", + "hasGradedAssignment": true, + "learnerHasAccess": true, + "numPointsEarned": 0, + "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", + }, + ], + }, + Object { + "displayName": "Second section", + "subsections": Array [ + Object { + "assignmentType": "Homework", + "displayName": "Second subsection", + "hasGradedAssignment": true, + "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", + }, + ], + }, + ], + "studioUrl": "http://studio.edx.org/settings/grading/course-v1:edX+Test+run", + "userHasPassingGrade": false, + "verificationData": Object { + "link": null, + "status": "none", + "statusDate": null, + }, + "verifiedMode": null, + }, + }, + }, + "recommendations": Object { + "recommendationsStatus": "loading", + }, } `; diff --git a/src/course-home/data/redux.test.js b/src/course-home/data/redux.test.js index db87abf66e..5ea0b858ae 100644 --- a/src/course-home/data/redux.test.js +++ b/src/course-home/data/redux.test.js @@ -6,11 +6,9 @@ import { getConfig } from '@edx/frontend-platform'; import * as thunks from './thunks'; -import executeThunk from '../../utils'; +import { appendBrowserTimezoneToUrl, executeThunk } from '../../utils'; -import './__factories__'; -import '../../courseware/data/__factories__/courseMetadata.factory'; -import initializeMockApp from '../../setupTest'; +import { initializeMockApp } from '../../setupTest'; import initializeStore from '../../store'; const { loggingService } = initializeMockApp(); @@ -18,20 +16,10 @@ const { loggingService } = initializeMockApp(); const axiosMock = new MockAdapter(getAuthenticatedHttpClient()); describe('Data layer integration tests', () => { - const courseMetadata = Factory.build('courseMetadata'); - const courseHomeMetadata = Factory.build( - 'courseHomeMetadata', { - course_id: courseMetadata.id, - }, - { courseTabs: courseMetadata.tabs }, - ); - - const courseId = courseMetadata.id; - const courseBaseUrl = `${getConfig().LMS_BASE_URL}/api/courseware/course`; - const courseMetadataBaseUrl = `${getConfig().LMS_BASE_URL}/api/course_home/v1/course_metadata`; - - const courseUrl = `${courseBaseUrl}/${courseId}`; - const courseMetadataUrl = `${courseMetadataBaseUrl}/${courseId}`; + const courseHomeMetadata = Factory.build('courseHomeMetadata'); + const { id: courseId } = courseHomeMetadata; + let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/course_home/course_metadata/${courseId}`; + courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl); let store; @@ -42,15 +30,10 @@ describe('Data layer integration tests', () => { store = initializeStore(); }); - it('Should initialize store', () => { - expect(store.getState()).toMatchSnapshot(); - }); - describe('Test fetchDatesTab', () => { - const datesBaseUrl = `${getConfig().LMS_BASE_URL}/api/course_home/v1/dates`; + const datesBaseUrl = `${getConfig().LMS_BASE_URL}/api/course_home/dates`; it('Should fail to fetch if error occurs', async () => { - axiosMock.onGet(courseUrl).networkError(); axiosMock.onGet(courseMetadataUrl).networkError(); axiosMock.onGet(`${datesBaseUrl}/${courseId}`).networkError(); @@ -65,7 +48,6 @@ describe('Data layer integration tests', () => { const datesUrl = `${datesBaseUrl}/${courseId}`; - axiosMock.onGet(courseUrl).reply(200, courseMetadata); axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeMetadata); axiosMock.onGet(datesUrl).reply(200, datesTabData); @@ -78,10 +60,9 @@ describe('Data layer integration tests', () => { }); describe('Test fetchOutlineTab', () => { - const outlineBaseUrl = `${getConfig().LMS_BASE_URL}/api/course_home/v1/outline`; + const outlineBaseUrl = `${getConfig().LMS_BASE_URL}/api/course_home/outline`; it('Should result in fetch failure if error occurs', async () => { - axiosMock.onGet(courseUrl).networkError(); axiosMock.onGet(courseMetadataUrl).networkError(); axiosMock.onGet(`${outlineBaseUrl}/${courseId}`).networkError(); @@ -96,7 +77,6 @@ describe('Data layer integration tests', () => { const outlineUrl = `${outlineBaseUrl}/${courseId}`; - axiosMock.onGet(courseUrl).reply(200, courseMetadata); axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeMetadata); axiosMock.onGet(outlineUrl).reply(200, outlineTabData); @@ -108,21 +88,89 @@ describe('Data layer integration tests', () => { }); }); + describe('Test fetchProgressTab', () => { + const progressBaseUrl = `${getConfig().LMS_BASE_URL}/api/course_home/progress`; + + it('Should result in fetch failure if error occurs', async () => { + axiosMock.onGet(courseMetadataUrl).networkError(); + axiosMock.onGet(`${progressBaseUrl}/${courseId}`).networkError(); + + await executeThunk(thunks.fetchProgressTab(courseId), store.dispatch); + + expect(loggingService.logError).toHaveBeenCalled(); + expect(store.getState().courseHome.courseStatus).toEqual('failed'); + }); + + it('Should fetch, normalize, and save metadata', async () => { + const progressTabData = Factory.build('progressTabData', { courseId }); + + const progressUrl = `${progressBaseUrl}/${courseId}`; + + axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeMetadata); + axiosMock.onGet(progressUrl).reply(200, progressTabData); + + await executeThunk(thunks.fetchProgressTab(courseId), store.dispatch); + + const state = store.getState(); + expect(state.courseHome.courseStatus).toEqual('loaded'); + expect(state).toMatchSnapshot(); + }); + + it('Should handle the url including a targetUserId', async () => { + const progressTabData = Factory.build('progressTabData', { courseId }); + const targetUserId = 2; + const progressUrl = `${progressBaseUrl}/${courseId}/${targetUserId}/`; + + axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeMetadata); + axiosMock.onGet(progressUrl).reply(200, progressTabData); + + await executeThunk(thunks.fetchProgressTab(courseId, 2), store.dispatch); + + const state = store.getState(); + expect(state.courseHome.targetUserId).toEqual(2); + }); + }); + + describe('Test saveCourseGoal', () => { + it('Should save course goal', async () => { + const goalUrl = `${getConfig().LMS_BASE_URL}/api/course_home/save_course_goal`; + axiosMock.onPost(goalUrl).reply(200, {}); + + await thunks.saveCourseGoal(courseId, 'unsure'); + + expect(axiosMock.history.post[0].url).toEqual(goalUrl); + expect(axiosMock.history.post[0].data).toEqual(`{"course_id":"${courseId}","goal_key":"unsure"}`); + }); + }); + describe('Test resetDeadlines', () => { it('Should reset course deadlines', async () => { const resetUrl = `${getConfig().LMS_BASE_URL}/api/course_experience/v1/reset_course_deadlines`; - axiosMock.onPost(resetUrl).reply(201); + const model = 'dates'; + axiosMock.onPost(resetUrl).reply(201, {}); const getTabDataMock = jest.fn(() => ({ type: 'MOCK_ACTION', })); - await executeThunk(thunks.resetDeadlines(courseId, getTabDataMock), store.dispatch); + await executeThunk(thunks.resetDeadlines(courseId, model, getTabDataMock), store.dispatch); expect(axiosMock.history.post[0].url).toEqual(resetUrl); - expect(axiosMock.history.post[0].data).toEqual(`{"course_key":"${courseId}"}`); + expect(axiosMock.history.post[0].data).toEqual(`{"course_key":"${courseId}","research_event_data":{"location":"dates-tab"}}`); expect(getTabDataMock).toHaveBeenCalledWith(courseId); }); }); + + describe('Test dismissWelcomeMessage', () => { + it('Should dismiss welcome message', async () => { + const dismissUrl = `${getConfig().LMS_BASE_URL}/api/course_home/dismiss_welcome_message`; + axiosMock.onPost(dismissUrl).reply(201); + + await executeThunk(thunks.dismissWelcomeMessage(courseId), store.dispatch); + + expect(axiosMock.history.post[0].url).toEqual(dismissUrl); + expect(axiosMock.history.post[0].data).toEqual(`{"course_id":"${courseId}"}`); + }); + }); }); diff --git a/src/index.scss b/src/index.scss index f0d2fcfef0..e4f3b31be3 100755 --- a/src/index.scss +++ b/src/index.scss @@ -112,7 +112,7 @@ border-radius: 0; border: solid 1px #eaeaea; border-left-width: 0; - position: relative; + position: relative; font-weight: 400; padding: 0 0.375rem; height: 3rem; From dedff4c05b724f1f4d48e32ee516bc6d8ef17a14 Mon Sep 17 00:00:00 2001 From: Chris Gaber Date: Fri, 21 Jan 2022 17:15:39 -0500 Subject: [PATCH 11/17] feat(key-terms): Imported from koa to maple --- src/course-home/data/api.js | 20 ++ src/course-home/data/index.js | 1 + src/course-home/data/thunks.js | 5 + src/course-home/glossary-tab/GlossaryTab.jsx | 334 ++++++++++++++++++ src/course-home/glossary-tab/GlossaryTab.scss | 138 ++++++++ .../glossary-tab/GlossaryTab.test.jsx | 10 + src/course-home/glossary-tab/index.jsx | 3 + src/course-home/glossary-tab/messages.js | 14 + src/index.jsx | 8 +- 9 files changed, 532 insertions(+), 1 deletion(-) create mode 100644 src/course-home/glossary-tab/GlossaryTab.jsx create mode 100644 src/course-home/glossary-tab/GlossaryTab.scss create mode 100644 src/course-home/glossary-tab/GlossaryTab.test.jsx create mode 100644 src/course-home/glossary-tab/index.jsx create mode 100644 src/course-home/glossary-tab/messages.js diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js index 35d6a5f9ad..e81316c13b 100644 --- a/src/course-home/data/api.js +++ b/src/course-home/data/api.js @@ -325,6 +325,26 @@ export async function getProgressTabData(courseId, targetUserId) { } } +export async function getGlossaryTabData(courseId) { + const url = `${getConfig().LMS_BASE_URL}/api/course_home/glossary/${courseId}`; + try { + const { data } = await getAuthenticatedHttpClient().get(url); + return camelCaseObject(data); + } catch (error) { + const { httpErrorStatus } = error && error.customAttributes; + if (httpErrorStatus === 404) { + global.location.replace(`${getConfig().LMS_BASE_URL}/courses/${courseId}/glossary`); + return {}; + } + if (httpErrorStatus === 401) { + // The backend sends this for unenrolled and unauthenticated learners, but we handle those cases by examining + // courseAccess in the metadata call, so just ignore this status for now. + return {}; + } + throw error; + } +} + export async function getProctoringInfoData(courseId, username) { let url = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/user_onboarding/status?is_learning_mfe=true&course_id=${encodeURIComponent(courseId)}`; if (username) { diff --git a/src/course-home/data/index.js b/src/course-home/data/index.js index b3eaab85bc..8541ce730d 100644 --- a/src/course-home/data/index.js +++ b/src/course-home/data/index.js @@ -4,6 +4,7 @@ export { fetchDatesTab, fetchOutlineTab, fetchProgressTab, + fetchGlossaryTab, resetDeadlines, saveCourseGoal, } from './thunks'; diff --git a/src/course-home/data/thunks.js b/src/course-home/data/thunks.js index be5a9cbc41..e1a0424a62 100644 --- a/src/course-home/data/thunks.js +++ b/src/course-home/data/thunks.js @@ -8,6 +8,7 @@ import { getDatesTabData, getOutlineTabData, getProgressTabData, + getGlossaryTabData, postCourseDeadlines, postCourseGoals, postDismissWelcomeMessage, @@ -92,6 +93,10 @@ export function fetchProgressTab(courseId, targetUserId) { return fetchTab(courseId, 'progress', getProgressTabData, parseInt(targetUserId, 10) || targetUserId); } +export function fetchGlossaryTab(courseId) { + return fetchTab(courseId, 'glossary', getGlossaryTabData); +} + export function fetchOutlineTab(courseId) { return fetchTab(courseId, 'outline', getOutlineTabData); } diff --git a/src/course-home/glossary-tab/GlossaryTab.jsx b/src/course-home/glossary-tab/GlossaryTab.jsx new file mode 100644 index 0000000000..7f3a20e84c --- /dev/null +++ b/src/course-home/glossary-tab/GlossaryTab.jsx @@ -0,0 +1,334 @@ +import React, { createContext } from 'react'; +import { useState, useEffect, useContext } from 'react'; +import { useSelector } from 'react-redux'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import './GlossaryTab.scss'; + +import messages from './messages'; + +import { + DropdownButton, + Collapsible, + Button, + Icon, + ActionRow, + SearchField, + Pagination, + Form +} from '@edx/paragon'; + +import { ExpandLess, ExpandMore } from '@edx/paragon/icons'; + +// Getting all necessary contexts and variables +export const CourseContext = createContext(); +export const KeyTermContext = createContext(); +const ListViewContext = createContext(); +const queryParams = new URLSearchParams(window.location.search); +const scrolltoParam = queryParams.get('scrollTo'); +const paginationLength = 15; + +// Lists all resources +function ResourceList() { + const { resources } = useContext(KeyTermContext); + resources.sort((a, b) => a.friendly_name > b.friendly_name ? 1 : -1) + if (resources.length > 0) return ( +
+ References: + {resources.map(function (resource) { + return ( +

+ {resource.friendly_name} +

+ ); + })} +
+ ); + return null; +} + +// Lists all lessons +function Lessons() { + const { lessons } = useContext(KeyTermContext); + + // Sorting list by module name then by lesson name + lessons.sort((a, b) => a.module_name === b.module_name ? (a.lesson_name > b.lesson_name ? 1: -1) : (a.module_name > b.module_name ? 1: -1)) + if (lessons.length > 0) return ( +
+ Lessons + { + lessons.map(function (lesson) { + return ( + + ); + }) + } +
+ ); + + return null; +} + +// Gets a specific textbook +function Lesson({ lesson }) { + const { courseId } = useContext(CourseContext); + const encodedCourse = courseId.replace(" ", "+"); + return ( +

+ {lesson.module_name}>{lesson.lesson_name}>{lesson.unit_name}     +

+ ); +} + +// Gets a specific textbook +function Textbook({ textbook }) { + const [variant, setVariant] = useState('primary'); + + const { courseId } = useContext(CourseContext); + const assetId = courseId.replace('course', 'asset'); + + const lmsTextbookLink = `http://localhost:18000/${assetId}+type@asset+block@${textbook.textbook_link}#page=${textbook.page_num}`; + + return ( +

+ {textbook.chapter}, pg. {textbook.page_num} +

+ ); +} + +// Lists all textbooks +function TextbookList() { + const { textbooks } = useContext(KeyTermContext); + if (textbooks.length > 0) return ( +
+ Textbooks + {textbooks.map(function (textbook) { + return ( + + ); + })} +
+ ); + + return null; +} + +// Lists all definitions +function DefinitionsList() { + const { definitions } = useContext(KeyTermContext); + if (definitions.length > 0) return ( +
+ Definitions + {definitions.map(function (descr) { + return ( +
+

{descr.description}

+
+ ); + })} +
+ ); + + return null; +} + +// Refers to one key term. +function KeyTerm({index}) { + const { key_name } = useContext(KeyTermContext); + + return ( +
+ {key_name}} + styling='card-lg' + iconWhenOpen={} + iconWhenClosed={} + > + + +
+ ); +} + +// All the data needed for a keyterm. +function KeyTermData() { + return ( +
+ + + + +
+ ); +} + +// Filter modules button +function ModuleDropdown(termData) { + const { filterModules, setFilterModules } = useContext(ListViewContext); + var lessons = [] + var newSet = new Set() + + termData["value"]["termData"].filter(function (keyTerm) { + keyTerm.lessons.forEach(lesson => { + if (lessons.find(function(object) {return object.module_name === lesson.module_name}) === undefined) lessons.push(lesson) + }); + }) + + lessons.sort((a, b) => a.module_name > b.module_name ? 1 : -1) + + const handleChange = e => { + filterModules.forEach(item => {newSet.add(item)}); + e.target.checked ? newSet.add(e.target.value) : newSet.delete(e.target.value); + setFilterModules(newSet); + } + + var buttontitle = filterModules.size > 0 ? `Filter Modules (${filterModules.size})` : "Filter Modules"; + return ( + + + + {lessons.map(lesson => {lesson.module_name})} + + + + ) +} + +// Lists all keyterms +function KeyTermList() { + const { filterModules, searchQuery, selectedPage, setPagination } = useContext(ListViewContext); + const { termData } = useContext(CourseContext); + + function paginate(termList, page_size, page_number) { + return termList.slice((page_number - 1) * page_size, page_number * page_size); + } + + const displayTerms = termData + .filter(function (keyTerm) { + // Displaying filtered keyterms + if (filterModules.size == 0 || keyTerm.lessons.find(function(object) {return filterModules.has(object.module_name)}) !== undefined) + // Returns keyterms with names or definitions matching search query + return keyTerm.key_name.toString().toLowerCase().includes(searchQuery.toLowerCase()) || + keyTerm.definitions.find(function(object) {return object.description.toLowerCase().includes(searchQuery.toLowerCase())}) !== undefined; + }) + .sort(function compare(a, b) { + if (a.key_name < b.key_name) return -1; + if (a.key_name > b.key_name) return 1; + return 0; + }); + + setPagination(displayTerms.length / paginationLength); + if (displayTerms.length === 0) setPagination(0); + + return ( +
+ {displayTerms.length === 0 ? (

No Terms to Display...

) : null} + {paginate(displayTerms, paginationLength, selectedPage).map((keyTerm, index)=> { + return ( + + + + ); + })} +
+ ); +} + +// Refers to the whole glossary page +function GlossaryTab({ intl }) { + const { courseId } = useSelector(state => state.courseHome); + const [searchQuery, setSearchQuery] = useState(''); + const [filterModules, setFilterModules] = useState(new Set()); + const [termData, setTermData] = useState([]); + const [selectedPage, setSelectedPage] = useState(1); + const [pagination, setPagination] = useState(); + const [expandAll, setExpandAll] = useState(false); + + // Fetch data from edx_keyterms_api + const getTerms=()=> { + const encodedCourse = courseId.replace(" ", "+"); + const restUrl = `http://localhost:18500/api/v1/course_terms?course_id=${encodedCourse}`; + fetch(restUrl, { + method: "GET" + }) + .then((response) => response.json()) + .then((jsonData) => setTermData(jsonData)) + .catch((error) => { + console.error(error); + }); + } + + useEffect(()=>{ + getTerms(); + },[]); + + return ( + <> + {/* Header */} +
+ {intl.formatMessage(messages.glossaryHeader)} +
+ + {/* Search Functions */} + + { +

+ Displaying {pagination > 0 ? 1 + paginationLength * (selectedPage - 1) : 0} + - + {pagination * paginationLength < paginationLength + ? parseInt(pagination * paginationLength) + : paginationLength * selectedPage}{' '} + of {parseInt(pagination * paginationLength)} items +

+ } + + + { + setSearchQuery(value); + }} + onClear={() => setSearchQuery("") + } + placeholder='Search' + /> + + + +
+ + {/* List of Key Terms */} + + + + + + { +
+ {pagination === 0 ? null : ( + parseInt(pagination) + ? parseInt(pagination) + 1 + : pagination + } + onPageSelect={(value) => setSelectedPage(value)} + /> + )} +
+ } +
+ + + ); +} + +GlossaryTab.propTypes = { + intl: intlShape.isRequired, +}; + +export default injectIntl(GlossaryTab); \ No newline at end of file diff --git a/src/course-home/glossary-tab/GlossaryTab.scss b/src/course-home/glossary-tab/GlossaryTab.scss new file mode 100644 index 0000000000..693a740681 --- /dev/null +++ b/src/course-home/glossary-tab/GlossaryTab.scss @@ -0,0 +1,138 @@ + + .dashboard-container { + min-height: 80vh; + } + + .key-term_list { + display: flex; + flex-direction: column; + padding:10px; + } + + #dropdown-basic-button { + margin-left: 10px; + } + + .key-term_search { + padding-right: 10px; + width: 50%; + } + + .dropdown-menu { + max-height: 250px; + overflow-y: scroll; + padding: 10px; + } + + .pgn__form-label { + width: 200px; + } + // .key-term-container { + // display: flex; + // flex-direction: column; + // } + + .flex-col { + display: flex; + flex-direction: column; + padding-right: 15px; + padding-left: 15px; + width: 25%; + // border-right: 2px solid lightgray; + } + + .flex-row { + display: flex; + flex: 1; + } + + .key-term-info { + display: flex; + align-content: flex-start; + flex: 1; + } + + .row { + width:100%; + } + + p { + font-size: 0.75em; + line-height: 1.5em; + /* max-width: 35%; */ + } + + .sidebar > p { + text-align: center; + } + + .textbook-container { + flex: 1; + } + + .menu-bar { + padding-bottom: 10px; + } + + .bulk-insert-container { + text-align: center; + } + + .filter-container { + text-align: center; + } + + .create-key-term { + text-align: center; + margin-top: 1em; + } + + .drag-n-drop { + padding-left: 1em; + padding-right: 1em; + margin-bottom: 10px; + } + + p.drag-n-drop { + padding-left: 1em; + padding-right: 1em; + margin-bottom: 25px; + margin-top: 10px; + } + + br.drag-n-drop { + margin: 10px; + } + + .footer-container { + padding: 20px; + display: flex; + justify-content: center; + } + + .modal-col-left { + width: 45%; + margin: 10px; + } + + .modal-col-right { + width: 45%; + margin: 10px; + } + + .action-row { + margin-left: 1.5em; + margin-right: 0.5em; + } + + .float-left { + float: left; + } + + .float-right { + float: right; + } + + p.error-message { + color: red; + } \ No newline at end of file diff --git a/src/course-home/glossary-tab/GlossaryTab.test.jsx b/src/course-home/glossary-tab/GlossaryTab.test.jsx new file mode 100644 index 0000000000..da80876ccf --- /dev/null +++ b/src/course-home/glossary-tab/GlossaryTab.test.jsx @@ -0,0 +1,10 @@ +import { + initializeMockApp, +} from '../../setupTest'; + +initializeMockApp(); +jest.mock('@edx/frontend-platform/analytics'); + +describe('Glossary Tab', () => { + +}); \ No newline at end of file diff --git a/src/course-home/glossary-tab/index.jsx b/src/course-home/glossary-tab/index.jsx new file mode 100644 index 0000000000..d3e1127a42 --- /dev/null +++ b/src/course-home/glossary-tab/index.jsx @@ -0,0 +1,3 @@ +import GlossaryTab from './GlossaryTab'; + +export default GlossaryTab; \ No newline at end of file diff --git a/src/course-home/glossary-tab/messages.js b/src/course-home/glossary-tab/messages.js new file mode 100644 index 0000000000..74d0e3c17c --- /dev/null +++ b/src/course-home/glossary-tab/messages.js @@ -0,0 +1,14 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + glossaryHeader: { + id: 'glossary.header', + defaultMessage: 'Glossary', + }, + studioLink: { + id: 'glossary.link.studio', + defaultMessage: 'View grading in Studio', + }, +}); + +export default messages; \ No newline at end of file diff --git a/src/index.jsx b/src/index.jsx index c668c13c9d..7a3682ee73 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -24,10 +24,11 @@ import { BadgeProgressTab, BadgeLeaderboardTab } from './course-home/badges-tab' import DatesTab from './course-home/dates-tab'; import GoalUnsubscribe from './course-home/goal-unsubscribe'; import ProgressTab from './course-home/progress-tab/ProgressTab'; +import GlossaryTab from './course-home/glossary-tab'; import { TabContainer } from './tab-page'; import { - fetchBadgeProgressTab, fetchBadgeLeaderboardTab, fetchDatesTab, fetchOutlineTab, fetchProgressTab, + fetchBadgeProgressTab, fetchBadgeLeaderboardTab, fetchDatesTab, fetchOutlineTab, fetchProgressTab, fetchGlossaryTab } from './course-home/data'; import { fetchCourse } from './courseware/data'; import initializeStore from './store'; @@ -61,6 +62,11 @@ subscribe(APP_READY, () => { + + + + + Date: Fri, 18 Feb 2022 09:53:53 -0500 Subject: [PATCH 12/17] chore(key-terms): Update linting --- src/course-home/glossary-tab/GlossaryTab.jsx | 101 +++++++++++------- .../glossary-tab/GlossaryTab.test.jsx | 4 +- src/course-home/glossary-tab/messages.js | 2 +- 3 files changed, 63 insertions(+), 44 deletions(-) diff --git a/src/course-home/glossary-tab/GlossaryTab.jsx b/src/course-home/glossary-tab/GlossaryTab.jsx index 7f3a20e84c..7a7ef4fe1b 100644 --- a/src/course-home/glossary-tab/GlossaryTab.jsx +++ b/src/course-home/glossary-tab/GlossaryTab.jsx @@ -1,22 +1,20 @@ -import React, { createContext } from 'react'; -import { useState, useEffect, useContext } from 'react'; +import React, { useState, useEffect, useContext, createContext } from 'react'; import { useSelector } from 'react-redux'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import './GlossaryTab.scss'; -import messages from './messages'; - import { DropdownButton, Collapsible, - Button, Icon, ActionRow, SearchField, Pagination, - Form + Form, } from '@edx/paragon'; +import messages from './messages'; + import { ExpandLess, ExpandMore } from '@edx/paragon/icons'; // Getting all necessary contexts and variables @@ -30,19 +28,21 @@ const paginationLength = 15; // Lists all resources function ResourceList() { const { resources } = useContext(KeyTermContext); - resources.sort((a, b) => a.friendly_name > b.friendly_name ? 1 : -1) - if (resources.length > 0) return ( -
- References: - {resources.map(function (resource) { - return ( -

- {resource.friendly_name} -

- ); - })} -
- ); + resources.sort((a, b) => { a.friendly_name > b.friendly_name ? 1 : -1 } ); + if (resources.length > 0) { + return ( +
+ References: + {resources.map(resource => { + return ( +

+ {resource.friendly_name} +

+ ); + })} +
+ ); + } return null; } @@ -51,20 +51,37 @@ function Lessons() { const { lessons } = useContext(KeyTermContext); // Sorting list by module name then by lesson name - lessons.sort((a, b) => a.module_name === b.module_name ? (a.lesson_name > b.lesson_name ? 1: -1) : (a.module_name > b.module_name ? 1: -1)) - if (lessons.length > 0) return ( -
- Lessons - { - lessons.map(function (lesson) { - return ( - - ); - }) + lessons.sort((a, b) => { + if (a.module_name === b.module_name) { + if (a.lesson_name > b.lesson_name) { + return 1; + } else { + return -1; } -
- ); + } else { + if (a.module_name > b.module_name) { + return 1; + } + else { + return -1; + } + } + }); + if (lessons.length > 0) { + return ( +
+ Lessons + { + lessons.map(lesson => { + return ( + + ); + }) + } +
+ ); + } return null; } @@ -74,7 +91,9 @@ function Lesson({ lesson }) { const encodedCourse = courseId.replace(" ", "+"); return (

- {lesson.module_name}>{lesson.lesson_name}>{lesson.unit_name}     + + {lesson.module_name}>{lesson.lesson_name}>{lesson.unit_name} +    

); } @@ -101,7 +120,7 @@ function TextbookList() { if (textbooks.length > 0) return (
Textbooks - {textbooks.map(function (textbook) { + {textbooks.map(textbook => { return ( ); @@ -118,7 +137,7 @@ function DefinitionsList() { if (definitions.length > 0) return (
Definitions - {definitions.map(function (descr) { + {definitions.map(descr => { return (

{descr.description}

@@ -172,13 +191,13 @@ function ModuleDropdown(termData) { var lessons = [] var newSet = new Set() - termData["value"]["termData"].filter(function (keyTerm) { + termData["value"]["termData"].filter(keyTerm => { keyTerm.lessons.forEach(lesson => { - if (lessons.find(function(object) {return object.module_name === lesson.module_name}) === undefined) lessons.push(lesson) + if (lessons.find(object => {return object.module_name === lesson.module_name}) === undefined) lessons.push(lesson); }); }) - lessons.sort((a, b) => a.module_name > b.module_name ? 1 : -1) + lessons.sort((a, b) => {a.module_name > b.module_name ? 1 : -1}); const handleChange = e => { filterModules.forEach(item => {newSet.add(item)}); @@ -208,12 +227,12 @@ function KeyTermList() { } const displayTerms = termData - .filter(function (keyTerm) { + .filter(keyTerm => { // Displaying filtered keyterms - if (filterModules.size == 0 || keyTerm.lessons.find(function(object) {return filterModules.has(object.module_name)}) !== undefined) + if (filterModules.size == 0 || keyTerm.lessons.find(object => { return filterModules.has(object.module_name) }) !== undefined) // Returns keyterms with names or definitions matching search query return keyTerm.key_name.toString().toLowerCase().includes(searchQuery.toLowerCase()) || - keyTerm.definitions.find(function(object) {return object.description.toLowerCase().includes(searchQuery.toLowerCase())}) !== undefined; + keyTerm.definitions.find(object => { return object.description.toLowerCase().includes(searchQuery.toLowerCase()) }) !== undefined; }) .sort(function compare(a, b) { if (a.key_name < b.key_name) return -1; @@ -331,4 +350,4 @@ GlossaryTab.propTypes = { intl: intlShape.isRequired, }; -export default injectIntl(GlossaryTab); \ No newline at end of file +export default injectIntl(GlossaryTab); diff --git a/src/course-home/glossary-tab/GlossaryTab.test.jsx b/src/course-home/glossary-tab/GlossaryTab.test.jsx index da80876ccf..60652b98d1 100644 --- a/src/course-home/glossary-tab/GlossaryTab.test.jsx +++ b/src/course-home/glossary-tab/GlossaryTab.test.jsx @@ -6,5 +6,5 @@ initializeMockApp(); jest.mock('@edx/frontend-platform/analytics'); describe('Glossary Tab', () => { - -}); \ No newline at end of file + +}); diff --git a/src/course-home/glossary-tab/messages.js b/src/course-home/glossary-tab/messages.js index 74d0e3c17c..7011568bf9 100644 --- a/src/course-home/glossary-tab/messages.js +++ b/src/course-home/glossary-tab/messages.js @@ -11,4 +11,4 @@ const messages = defineMessages({ }, }); -export default messages; \ No newline at end of file +export default messages; From b51b863fb75ed3a3aaa29246eb791ddbc8931fbf Mon Sep 17 00:00:00 2001 From: Chris Gaber Date: Sun, 27 Feb 2022 16:56:45 -0500 Subject: [PATCH 13/17] fix(key-terms): Moved API call to api.js and fixed linting --- package-lock.json | 137 ++++++--- package.json | 1 + src/course-home/data/api.js | 15 + src/course-home/glossary-tab/GlossaryTab.jsx | 297 ++++++++++--------- src/course-home/glossary-tab/index.jsx | 2 +- 5 files changed, 273 insertions(+), 179 deletions(-) diff --git a/package-lock.json b/package-lock.json index b9025cc2e1..fb89942fb5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3343,11 +3343,6 @@ "integrity": "sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA==", "dev": true }, - "@edx/brand": { - "version": "npm:@edx/brand-openedx@1.1.0", - "resolved": "https://registry.npmjs.org/@edx/brand-openedx/-/brand-openedx-1.1.0.tgz", - "integrity": "sha512-ne2ZKF1r0akkt0rEzCAQAk4cTDTI2GiWCpc+T7ldQpw9X57OnUB16dKsFNe40C9uEjL5h3Ps/ZsFM5dm4cIkEQ==" - }, "@edx/eslint-config": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@edx/eslint-config/-/eslint-config-1.2.0.tgz", @@ -3601,6 +3596,16 @@ "color-convert": "^2.0.1" } }, + "aria-query": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz", + "integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=", + "dev": true, + "requires": { + "ast-types-flow": "0.0.7", + "commander": "^2.11.0" + } + }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -3633,6 +3638,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, "debug": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", @@ -3642,6 +3653,29 @@ "ms": "2.1.2" } }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "eslint-plugin-jsx-a11y": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz", + "integrity": "sha512-CawzfGt9w83tyuVekn0GDPU9ytYtxyxyFZ3aSWROmnRRFQFT2BiPJd7jvRdzNDi6oLWaS2asMeYSNMjWTV4eNg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.4.5", + "aria-query": "^3.0.0", + "array-includes": "^3.0.3", + "ast-types-flow": "^0.0.7", + "axobject-query": "^2.0.2", + "damerau-levenshtein": "^1.0.4", + "emoji-regex": "^7.0.2", + "has": "^1.0.3", + "jsx-ast-utils": "^2.2.1" + } + }, "jest": { "version": "26.6.3", "resolved": "https://registry.npmjs.org/jest/-/jest-26.6.3.tgz", @@ -6748,21 +6782,13 @@ } }, "aria-query": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz", - "integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", + "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", "dev": true, "requires": { - "ast-types-flow": "0.0.7", - "commander": "^2.11.0" - }, - "dependencies": { - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - } + "@babel/runtime": "^7.10.2", + "@babel/runtime-corejs3": "^7.10.2" } }, "arr-diff": { @@ -6940,6 +6966,12 @@ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.4.tgz", "integrity": "sha512-SA5mXJWrId1TaQjfxUYghbqQ/hYioKmLJvPJyDuYRtXXenFNMjj4hSSt1Cf1xsuXSXrtxrVC5Ot4eU6cOtBDdA==" }, + "axe-core": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.4.1.tgz", + "integrity": "sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw==", + "dev": true + }, "axios": { "version": "0.21.4", "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", @@ -9119,9 +9151,9 @@ } }, "damerau-levenshtein": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.7.tgz", - "integrity": "sha512-VvdQIPGdWP0SqFXghj79Wf/5LArmreyMsGLa6FG6iC4t3j7j5s71TrwWmT/4akbDQIqjfACkLZmjXhA7g2oUZw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, "data-urls": { @@ -10412,27 +10444,49 @@ } }, "eslint-plugin-jsx-a11y": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz", - "integrity": "sha512-CawzfGt9w83tyuVekn0GDPU9ytYtxyxyFZ3aSWROmnRRFQFT2BiPJd7jvRdzNDi6oLWaS2asMeYSNMjWTV4eNg==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.5.1.tgz", + "integrity": "sha512-sVCFKX9fllURnXT2JwLN5Qgo24Ug5NF6dxhkmxsMEUZhXRcGg+X3e1JbJ84YePQKBl5E0ZjAH5Q4rkdcGY99+g==", "dev": true, "requires": { - "@babel/runtime": "^7.4.5", - "aria-query": "^3.0.0", - "array-includes": "^3.0.3", + "@babel/runtime": "^7.16.3", + "aria-query": "^4.2.2", + "array-includes": "^3.1.4", "ast-types-flow": "^0.0.7", - "axobject-query": "^2.0.2", - "damerau-levenshtein": "^1.0.4", - "emoji-regex": "^7.0.2", + "axe-core": "^4.3.5", + "axobject-query": "^2.2.0", + "damerau-levenshtein": "^1.0.7", + "emoji-regex": "^9.2.2", "has": "^1.0.3", - "jsx-ast-utils": "^2.2.1" + "jsx-ast-utils": "^3.2.1", + "language-tags": "^1.0.5", + "minimatch": "^3.0.4" }, "dependencies": { + "@babel/runtime": { + "version": "7.17.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.2.tgz", + "integrity": "sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true + }, + "jsx-ast-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz", + "integrity": "sha512-uP5vu8xfy2F9A6LGC22KO7e2/vGTS1MhP+18f++ZNlf0Ohaxbc9nIEwHAsejlJKyzfZzU5UIhe5ItYkitcZnZA==", + "dev": true, + "requires": { + "array-includes": "^3.1.3", + "object.assign": "^4.1.2" + } } } }, @@ -16784,6 +16838,21 @@ "integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==", "dev": true }, + "language-subtag-registry": { + "version": "0.3.21", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz", + "integrity": "sha512-L0IqwlIXjilBVVYKFT37X9Ih11Um5NEl9cbJIuU/SwP/zEEAbBPOnEeeuxVMf45ydWQRDQN3Nqc96OgbH1K+Pg==", + "dev": true + }, + "language-tags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", + "integrity": "sha1-0yHbxNowuovzAk4ED6XBRmH5GTo=", + "dev": true, + "requires": { + "language-subtag-registry": "~0.3.2" + } + }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", diff --git a/package.json b/package.json index 4ce15c17b2..158ebe98a3 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "axios-mock-adapter": "1.20.0", "codecov": "3.8.3", "es-check": "6.0.0", + "eslint-plugin-jsx-a11y": "^6.5.1", "glob": "7.2.0", "husky": "7.0.2", "jest": "27.2.5", diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js index e81316c13b..7d36d525ee 100644 --- a/src/course-home/data/api.js +++ b/src/course-home/data/api.js @@ -345,6 +345,21 @@ export async function getGlossaryTabData(courseId) { } } +export async function getGlossaryData(courseId) { + const encodedCourse = courseId.replace(' ', '+'); + const url = `http://localhost:18500/api/v1/course_terms?course_id=${encodedCourse}`; + try { + const { data } = await getAuthenticatedHttpClient().get(url); + return data; + } catch (error) { + const { httpErrorStatus } = error && error.customAttributes; + if (httpErrorStatus === 404) { + return {}; + } + throw error; + } +} + export async function getProctoringInfoData(courseId, username) { let url = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/user_onboarding/status?is_learning_mfe=true&course_id=${encodeURIComponent(courseId)}`; if (username) { diff --git a/src/course-home/glossary-tab/GlossaryTab.jsx b/src/course-home/glossary-tab/GlossaryTab.jsx index 7a7ef4fe1b..2226c302e9 100644 --- a/src/course-home/glossary-tab/GlossaryTab.jsx +++ b/src/course-home/glossary-tab/GlossaryTab.jsx @@ -1,4 +1,11 @@ -import React, { useState, useEffect, useContext, createContext } from 'react'; +import React, { + useState, + useEffect, + useContext, + createContext, +} from 'react'; + +import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import './GlossaryTab.scss'; @@ -10,12 +17,12 @@ import { ActionRow, SearchField, Pagination, - Form, + Form, } from '@edx/paragon'; -import messages from './messages'; - import { ExpandLess, ExpandMore } from '@edx/paragon/icons'; +import { getGlossaryData } from '../data/api'; +import messages from './messages'; // Getting all necessary contexts and variables export const CourseContext = createContext(); @@ -28,18 +35,20 @@ const paginationLength = 15; // Lists all resources function ResourceList() { const { resources } = useContext(KeyTermContext); - resources.sort((a, b) => { a.friendly_name > b.friendly_name ? 1 : -1 } ); + + resources.sort((a, b) => (a.friendly_name > b.friendly_name ? 1 : -1)); + if (resources.length > 0) { return (
References: - {resources.map(resource => { - return ( + { + resources.map(resource => (

{resource.friendly_name}

- ); - })} + )) + }
); } @@ -49,36 +58,27 @@ function ResourceList() { // Lists all lessons function Lessons() { const { lessons } = useContext(KeyTermContext); - + // Sorting list by module name then by lesson name - lessons.sort((a, b) => { + lessons.sort((a, b) => { if (a.module_name === b.module_name) { - if (a.lesson_name > b.lesson_name) { - return 1; - } else { - return -1; - } - } else { - if (a.module_name > b.module_name) { - return 1; - } - else { - return -1; - } - } + if (a.lesson_name > b.lesson_name) { return 1; } + return -1; + } + if (a.module_name > b.module_name) { return 1; } + + return -1; }); if (lessons.length > 0) { return ( -
+
Lessons { - lessons.map(lesson => { - return ( - - ); - }) - } + lessons.map(lesson => ( + + )) + }
); } @@ -88,10 +88,10 @@ function Lessons() { // Gets a specific textbook function Lesson({ lesson }) { const { courseId } = useContext(CourseContext); - const encodedCourse = courseId.replace(" ", "+"); + const encodedCourse = courseId.replace(' ', '+'); return (

- + {lesson.module_name}>{lesson.lesson_name}>{lesson.unit_name}    

@@ -100,8 +100,6 @@ function Lesson({ lesson }) { // Gets a specific textbook function Textbook({ textbook }) { - const [variant, setVariant] = useState('primary'); - const { courseId } = useContext(CourseContext); const assetId = courseId.replace('course', 'asset'); @@ -117,53 +115,57 @@ function Textbook({ textbook }) { // Lists all textbooks function TextbookList() { const { textbooks } = useContext(KeyTermContext); - if (textbooks.length > 0) return ( -
- Textbooks - {textbooks.map(textbook => { - return ( + if (textbooks.length > 0) { + return ( +
+ Textbooks + { + textbooks.map(textbook => ( - ); - })} -
- ); - + )) + } +
+ ); + } return null; } // Lists all definitions function DefinitionsList() { const { definitions } = useContext(KeyTermContext); - if (definitions.length > 0) return ( -
- Definitions - {definitions.map(descr => { - return ( -
-

{descr.description}

+ if (definitions.length > 0) { + return ( +
+ Definitions + {definitions.map((descr) => ( +
+

{descr.description}

- ); - })} -
- ); - + ))} +
+ ); + } return null; } // Refers to one key term. -function KeyTerm({index}) { +function KeyTerm({ index }) { + /* eslint-disable camelcase */ const { key_name } = useContext(KeyTermContext); return ( -
- + {key_name}} - styling='card-lg' + styling="card-lg" iconWhenOpen={} iconWhenClosed={} > @@ -171,12 +173,13 @@ function KeyTerm({index}) {
); + /* eslint-enable camelcase */ } // All the data needed for a keyterm. function KeyTermData() { return ( -
+
@@ -188,71 +191,70 @@ function KeyTermData() { // Filter modules button function ModuleDropdown(termData) { const { filterModules, setFilterModules } = useContext(ListViewContext); - var lessons = [] - var newSet = new Set() + const lessons = []; + const newSet = new Set(); - termData["value"]["termData"].filter(keyTerm => { - keyTerm.lessons.forEach(lesson => { - if (lessons.find(object => {return object.module_name === lesson.module_name}) === undefined) lessons.push(lesson); - }); - }) + termData.value.termData.filter(keyTerm => keyTerm.lessons.forEach(lesson => { + if (lessons.find(object => object.module_name === lesson.module_name) === undefined) { lessons.push(lesson); } + })); - lessons.sort((a, b) => {a.module_name > b.module_name ? 1 : -1}); + lessons.sort((a, b) => (a.module_name > b.module_name ? 1 : -1)); const handleChange = e => { - filterModules.forEach(item => {newSet.add(item)}); - e.target.checked ? newSet.add(e.target.value) : newSet.delete(e.target.value); - setFilterModules(newSet); - } + filterModules.forEach(item => { newSet.add(item); }); + if (e.target.checked) { newSet.add(e.target.value); } else { newSet.delete(e.target.value); } + return setFilterModules(newSet); + }; - var buttontitle = filterModules.size > 0 ? `Filter Modules (${filterModules.size})` : "Filter Modules"; + const buttontitle = filterModules.size > 0 ? `Filter Modules (${filterModules.size})` : 'Filter Modules'; return ( - {lessons.map(lesson => {lesson.module_name})} + {lessons.map(lesson => {lesson.module_name})} - ) + ); } // Lists all keyterms function KeyTermList() { - const { filterModules, searchQuery, selectedPage, setPagination } = useContext(ListViewContext); + const { + filterModules, searchQuery, selectedPage, setPagination, + } = useContext(ListViewContext); const { termData } = useContext(CourseContext); - function paginate(termList, page_size, page_number) { - return termList.slice((page_number - 1) * page_size, page_number * page_size); + function paginate(termList, pageSize, pageNumber) { + return termList.slice((pageNumber - 1) * pageSize, pageNumber * pageSize); } - + const displayTerms = termData - .filter(keyTerm => { - // Displaying filtered keyterms - if (filterModules.size == 0 || keyTerm.lessons.find(object => { return filterModules.has(object.module_name) }) !== undefined) - // Returns keyterms with names or definitions matching search query - return keyTerm.key_name.toString().toLowerCase().includes(searchQuery.toLowerCase()) || - keyTerm.definitions.find(object => { return object.description.toLowerCase().includes(searchQuery.toLowerCase()) }) !== undefined; - }) - .sort(function compare(a, b) { - if (a.key_name < b.key_name) return -1; - if (a.key_name > b.key_name) return 1; + .filter(keyTerm => ( + // First finds all keyterms that have been filtered for + filterModules.size === 0 + || keyTerm.lessons.find(object => filterModules.has(object.module_name)) !== undefined) + // Returns keyterms with names or definitions matching search query + && (keyTerm.key_name.toString().toLowerCase().includes(searchQuery.toLowerCase()) + || keyTerm.definitions.find(object => object.description.toLowerCase() + .includes(searchQuery.toLowerCase())) !== undefined)) + .sort((a, b) => { + if (a.key_name < b.key_name) { return -1; } + if (a.key_name > b.key_name) { return 1; } return 0; }); - + setPagination(displayTerms.length / paginationLength); - if (displayTerms.length === 0) setPagination(0); + if (displayTerms.length === 0) { setPagination(0); } return ( -
- {displayTerms.length === 0 ? (

No Terms to Display...

) : null} - {paginate(displayTerms, paginationLength, selectedPage).map((keyTerm, index)=> { - return ( +
+ {displayTerms.length === 0 ? (

No Terms to Display...

) : null} + {paginate(displayTerms, paginationLength, selectedPage).map((keyTerm, index) => ( - ); - })} + ))}
); } @@ -267,23 +269,10 @@ function GlossaryTab({ intl }) { const [pagination, setPagination] = useState(); const [expandAll, setExpandAll] = useState(false); - // Fetch data from edx_keyterms_api - const getTerms=()=> { - const encodedCourse = courseId.replace(" ", "+"); - const restUrl = `http://localhost:18500/api/v1/course_terms?course_id=${encodedCourse}`; - fetch(restUrl, { - method: "GET" - }) - .then((response) => response.json()) - .then((jsonData) => setTermData(jsonData)) - .catch((error) => { - console.error(error); - }); - } - - useEffect(()=>{ - getTerms(); - },[]); + useEffect(() => { + getGlossaryData(courseId) + .then((keytermData) => setTermData(keytermData)); + }, []); return ( <> @@ -294,54 +283,52 @@ function GlossaryTab({ intl }) { {/* Search Functions */} - {

Displaying {pagination > 0 ? 1 + paginationLength * (selectedPage - 1) : 0} - - - {pagination * paginationLength < paginationLength - ? parseInt(pagination * paginationLength) - : paginationLength * selectedPage}{' '} - of {parseInt(pagination * paginationLength)} items + - + {pagination * paginationLength < paginationLength + ? parseInt(pagination * paginationLength, 10) + : paginationLength * selectedPage}{' '} + of {parseInt(pagination * paginationLength, 10)} items

- } - + { - setSearchQuery(value); - }} - onClear={() => setSearchQuery("") - } - placeholder='Search' + onSubmit={(value) => { + setSearchQuery(value); + }} + onClear={() => setSearchQuery('')} + placeholder="Search" /> - - + +
- + {/* List of Key Terms */} - - + + - - { -
+ +
{pagination === 0 ? null : ( parseInt(pagination) - ? parseInt(pagination) + 1 + pagination > parseInt(pagination, 10) + ? parseInt(pagination, 10) + 1 : pagination } onPageSelect={(value) => setSelectedPage(value)} /> )}
- } - + ); } @@ -350,4 +337,26 @@ GlossaryTab.propTypes = { intl: intlShape.isRequired, }; +Textbook.propTypes = { + textbook: PropTypes.shape({ + textbook_link: PropTypes.string, + chapter: PropTypes.string, + page_num: PropTypes.number, + }).isRequired, +}; + +Lesson.propTypes = { + lesson: PropTypes.shape({ + id: PropTypes.number, + lesson_link: PropTypes.string, + module_name: PropTypes.string, + lesson_name: PropTypes.string, + unit_name: PropTypes.string, + }).isRequired, +}; + +KeyTerm.propTypes = { + index: PropTypes.number.isRequired, +}; + export default injectIntl(GlossaryTab); diff --git a/src/course-home/glossary-tab/index.jsx b/src/course-home/glossary-tab/index.jsx index d3e1127a42..d684d52048 100644 --- a/src/course-home/glossary-tab/index.jsx +++ b/src/course-home/glossary-tab/index.jsx @@ -1,3 +1,3 @@ import GlossaryTab from './GlossaryTab'; -export default GlossaryTab; \ No newline at end of file +export default GlossaryTab; From 4f4df7af4d20e65d9c8c8c9ac51047cc5e625630 Mon Sep 17 00:00:00 2001 From: Chris Gaber Date: Sun, 27 Feb 2022 17:07:04 -0500 Subject: [PATCH 14/17] chore(key-terms): update package-lock.json --- package-lock.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/package-lock.json b/package-lock.json index fb89942fb5..4e333b398e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3343,6 +3343,11 @@ "integrity": "sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA==", "dev": true }, + "@edx/brand": { + "version": "npm:@edx/brand-openedx@1.1.0", + "resolved": "https://registry.npmjs.org/@edx/brand-openedx/-/brand-openedx-1.1.0.tgz", + "integrity": "sha512-ne2ZKF1r0akkt0rEzCAQAk4cTDTI2GiWCpc+T7ldQpw9X57OnUB16dKsFNe40C9uEjL5h3Ps/ZsFM5dm4cIkEQ==" + }, "@edx/eslint-config": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@edx/eslint-config/-/eslint-config-1.2.0.tgz", From 8b5ccc3d896bb1b8f3d609153f351d0a1db26bf5 Mon Sep 17 00:00:00 2001 From: Chris Gaber Date: Sun, 27 Feb 2022 17:29:47 -0500 Subject: [PATCH 15/17] fix(key-terms): added test case --- src/course-home/glossary-tab/GlossaryTab.test.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/course-home/glossary-tab/GlossaryTab.test.jsx b/src/course-home/glossary-tab/GlossaryTab.test.jsx index 60652b98d1..113843e3cf 100644 --- a/src/course-home/glossary-tab/GlossaryTab.test.jsx +++ b/src/course-home/glossary-tab/GlossaryTab.test.jsx @@ -6,5 +6,7 @@ initializeMockApp(); jest.mock('@edx/frontend-platform/analytics'); describe('Glossary Tab', () => { - + it('has a title', async () => { + expect(screen.getByText('Glossary')).toBeInTheDocument(); + }); }); From 724fb264f9b2aac016c776decf2657ecdec1c33a Mon Sep 17 00:00:00 2001 From: Chris Gaber Date: Sun, 27 Feb 2022 17:39:01 -0500 Subject: [PATCH 16/17] fix(key-terms): added test case --- src/course-home/glossary-tab/GlossaryTab.test.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/course-home/glossary-tab/GlossaryTab.test.jsx b/src/course-home/glossary-tab/GlossaryTab.test.jsx index 113843e3cf..86327f1538 100644 --- a/src/course-home/glossary-tab/GlossaryTab.test.jsx +++ b/src/course-home/glossary-tab/GlossaryTab.test.jsx @@ -1,3 +1,5 @@ +import { screen } from '@testing-library/react'; + import { initializeMockApp, } from '../../setupTest'; From 05f04579a18c7e165630e6e0b702bb23aaa37cfb Mon Sep 17 00:00:00 2001 From: Zachary Trabookis Date: Wed, 27 Apr 2022 16:37:47 -0400 Subject: [PATCH 17/17] fix(key-terms): Resolve lint issues. --- src/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.jsx b/src/index.jsx index 7a3682ee73..b3486b16ba 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -28,7 +28,7 @@ import GlossaryTab from './course-home/glossary-tab'; import { TabContainer } from './tab-page'; import { - fetchBadgeProgressTab, fetchBadgeLeaderboardTab, fetchDatesTab, fetchOutlineTab, fetchProgressTab, fetchGlossaryTab + fetchBadgeProgressTab, fetchBadgeLeaderboardTab, fetchDatesTab, fetchOutlineTab, fetchProgressTab, fetchGlossaryTab, } from './course-home/data'; import { fetchCourse } from './courseware/data'; import initializeStore from './store';