diff --git a/src/courseware/course/sequence/Sequence.jsx b/src/courseware/course/sequence/Sequence.jsx
index 0c0b33e82d..4a3f648e0f 100644
--- a/src/courseware/course/sequence/Sequence.jsx
+++ b/src/courseware/course/sequence/Sequence.jsx
@@ -27,6 +27,7 @@ import messages from './messages';
import HiddenAfterDue from './hidden-after-due';
import { SequenceNavigation, UnitNavigation } from './sequence-navigation';
import SequenceContent from './SequenceContent';
+import FeedbackWidget from './Unit/feedback-widget';
const Sequence = ({
unitId,
@@ -37,7 +38,11 @@ const Sequence = ({
previousSequenceHandler,
}) => {
const intl = useIntl();
- const course = useModel('coursewareMeta', courseId);
+ const {
+ canAccessProctoredExams,
+ license,
+ wholeCourseTranslationEnabled,
+ } = useModel('coursewareMeta', courseId);
const {
isStaff,
originalUserIsStaff,
@@ -49,7 +54,8 @@ const Sequence = ({
const shouldDisplayNotificationTriggerInSequence = useWindowSize().width < breakpoints.small.minWidth;
const enableNewSidebar = getConfig().ENABLE_NEW_SIDEBAR;
- const handleNext = () => {
+ const handleNext = (position) => {
+ logEvent('edx.ui.lms.sequence.next_selected', position);
const nextIndex = sequence.unitIds.indexOf(unitId) + 1;
if (nextIndex < sequence.unitIds.length) {
const newUnitId = sequence.unitIds[nextIndex];
@@ -59,7 +65,8 @@ const Sequence = ({
}
};
- const handlePrevious = () => {
+ const handlePrevious = (position) => {
+ logEvent('edx.ui.lms.sequence.previous_selected', position);
const previousIndex = sequence.unitIds.indexOf(unitId) - 1;
if (previousIndex >= 0) {
const newUnitId = sequence.unitIds[previousIndex];
@@ -144,55 +151,60 @@ const Sequence = ({
const gated = sequence && sequence.gatedContent !== undefined && sequence.gatedContent.gated;
const defaultContent = (
-
-
-
{
- logEvent('edx.ui.lms.sequence.next_selected', 'top');
- handleNext();
- }}
- onNavigate={(destinationUnitId) => {
- logEvent('edx.ui.lms.sequence.tab_selected', 'top', destinationUnitId);
- handleNavigate(destinationUnitId);
- }}
- previousHandler={() => {
- logEvent('edx.ui.lms.sequence.previous_selected', 'top');
- handlePrevious();
- }}
- />
- {shouldDisplayNotificationTriggerInSequence && (
- enableNewSidebar === 'true' ? :
- )}
-
-
-
- {unitHasLoaded && (
-
+
+
+
{
- logEvent('edx.ui.lms.sequence.previous_selected', 'bottom');
- handlePrevious();
+ className="mb-4"
+ nextHandler={() => {
+ handleNext('top');
}}
- onClickNext={() => {
- logEvent('edx.ui.lms.sequence.next_selected', 'bottom');
- handleNext();
+ onNavigate={(destinationUnitId) => {
+ logEvent('edx.ui.lms.sequence.tab_selected', 'top', destinationUnitId);
+ handleNavigate(destinationUnitId);
+ }}
+ previousHandler={() => {
+ handlePrevious('top');
}}
/>
+ {shouldDisplayNotificationTriggerInSequence && (
+ enableNewSidebar === 'true' ? :
)}
+
+
+
+ {unitHasLoaded && (
+ {
+ handlePrevious('bottom');
+ }}
+ onClickNext={() => {
+ handleNext('bottom');
+ }}
+ />
+ )}
+
+ {enableNewSidebar === 'true' ?
:
}
- {enableNewSidebar === 'true' ? : }
-
+ {
+ wholeCourseTranslationEnabled && (
+
+
+
+ )
+ }
+ >
);
if (sequenceStatus === 'loaded') {
@@ -203,11 +215,11 @@ const Sequence = ({
courseId={courseId}
isStaff={isStaff}
originalUserIsStaff={originalUserIsStaff}
- canAccessProctoredExams={course.canAccessProctoredExams}
+ canAccessProctoredExams={canAccessProctoredExams}
>
{defaultContent}
-
+
);
}
diff --git a/src/courseware/course/sequence/Unit/feedback-widget/__snapshots__/index.test.jsx.snap b/src/courseware/course/sequence/Unit/feedback-widget/__snapshots__/index.test.jsx.snap
new file mode 100644
index 0000000000..5ec61637f9
--- /dev/null
+++ b/src/courseware/course/sequence/Unit/feedback-widget/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,52 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`
renders 1`] = `
+
+
+
+ Rate this page translation
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+`;
diff --git a/src/courseware/course/sequence/Unit/feedback-widget/index.jsx b/src/courseware/course/sequence/Unit/feedback-widget/index.jsx
new file mode 100644
index 0000000000..d5172835c3
--- /dev/null
+++ b/src/courseware/course/sequence/Unit/feedback-widget/index.jsx
@@ -0,0 +1,99 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { ActionRow, IconButton, Icon } from '@edx/paragon';
+import { Close, ThumbUpOutline, ThumbDownOffAlt } from '@edx/paragon/icons';
+
+import './index.scss';
+import messages from './messages';
+import useFeedbackWidget from './useFeedbackWidget';
+
+const FeedbackWidget = ({
+ courseId,
+ languageCode,
+ unitId,
+ userId,
+}) => {
+ const { formatMessage } = useIntl();
+ const {
+ closeFeedbackWidget,
+ sendFeedback,
+ showFeedbackWidget,
+ showGratitudeText,
+ } = useFeedbackWidget({
+ courseId,
+ languageCode,
+ unitId,
+ userId,
+ });
+ return (
+ (showFeedbackWidget || showGratitudeText) && (
+
+ {
+ showFeedbackWidget && (
+
+
+ {formatMessage(messages.rateTranslationText)}
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+ )
+ }
+ {
+ showGratitudeText && (
+
+
+ {formatMessage(messages.gratitudeText)}
+
+
+ )
+ }
+
+ )
+ );
+};
+
+FeedbackWidget.propTypes = {
+ courseId: PropTypes.string.isRequired,
+ languageCode: PropTypes.string.isRequired,
+ userId: PropTypes.string.isRequired,
+ unitId: PropTypes.string.isRequired,
+};
+
+FeedbackWidget.defaultProps = {};
+
+export default FeedbackWidget;
diff --git a/src/courseware/course/sequence/Unit/feedback-widget/index.scss b/src/courseware/course/sequence/Unit/feedback-widget/index.scss
new file mode 100644
index 0000000000..9c88934bfa
--- /dev/null
+++ b/src/courseware/course/sequence/Unit/feedback-widget/index.scss
@@ -0,0 +1,4 @@
+.action-row-divider {
+ font-size: 31px;
+ font-weight: 100;
+}
\ No newline at end of file
diff --git a/src/courseware/course/sequence/Unit/feedback-widget/index.test.jsx b/src/courseware/course/sequence/Unit/feedback-widget/index.test.jsx
new file mode 100644
index 0000000000..2fc51fb2af
--- /dev/null
+++ b/src/courseware/course/sequence/Unit/feedback-widget/index.test.jsx
@@ -0,0 +1,51 @@
+import { shallow } from '@edx/react-unit-test-utils';
+
+import FeedbackWidget from './index';
+
+jest.mock('@edx/paragon', () => jest.requireActual('@edx/react-unit-test-utils').mockComponents({
+ ActionRow: {
+ Spacer: 'Spacer',
+ },
+ IconButton: 'IconButton',
+ Icon: 'Icon',
+}));
+jest.mock('@edx/paragon/icons', () => ({
+ Close: 'Close',
+ ThumbUpOutline: 'ThumbUpOutline',
+ ThumbDownOffAlt: 'ThumbDownOffAlt',
+}));
+jest.mock('./useFeedbackWidget', () => () => ({
+ closeFeedbackWidget: jest.fn().mockName('closeFeedbackWidget'),
+ openFeedbackWidget: jest.fn().mockName('openFeedbackWidget'),
+ sendFeedback: jest.fn().mockName('sendFeedback'),
+ showFeedbackWidget: true,
+ showGratitudeText: false,
+}));
+jest.mock('@edx/frontend-platform/i18n', () => {
+ const i18n = jest.requireActual('@edx/frontend-platform/i18n');
+ const { formatMessage } = jest.requireActual('@edx/react-unit-test-utils');
+ // this provide consistent for the test on different platform/timezone
+ const formatDate = jest.fn(date => new Date(date).toISOString()).mockName('useIntl.formatDate');
+ return {
+ ...i18n,
+ useIntl: jest.fn(() => ({
+ formatMessage,
+ formatDate,
+ })),
+ defineMessages: m => m,
+ FormattedMessage: () => 'FormattedMessage',
+ };
+});
+
+describe('
', () => {
+ const props = {
+ courseId: 'course-v1:edX+DemoX+Demo_Course',
+ languageCode: 'es',
+ unitId: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@37b72b3915204b70acb00c55b604b563',
+ userId: '123',
+ };
+ it('renders', () => {
+ const wrapper = shallow(
);
+ expect(wrapper.snapshot).toMatchSnapshot();
+ });
+});
diff --git a/src/courseware/course/sequence/Unit/feedback-widget/messages.js b/src/courseware/course/sequence/Unit/feedback-widget/messages.js
new file mode 100644
index 0000000000..f84f836a86
--- /dev/null
+++ b/src/courseware/course/sequence/Unit/feedback-widget/messages.js
@@ -0,0 +1,16 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ rateTranslationText: {
+ id: 'feedbackWidget.rateTranslationText',
+ defaultMessage: 'Rate this page translation',
+ description: 'Title for the feedback widget action row.',
+ },
+ gratitudeText: {
+ id: 'feedbackWidget.gratitudeText',
+ defaultMessage: 'Thank you! Your feedback matters.',
+ description: 'Title for secondary action row.',
+ },
+});
+
+export default messages;
diff --git a/src/courseware/course/sequence/Unit/feedback-widget/useFeedbackWidget.js b/src/courseware/course/sequence/Unit/feedback-widget/useFeedbackWidget.js
new file mode 100644
index 0000000000..cdce79935f
--- /dev/null
+++ b/src/courseware/course/sequence/Unit/feedback-widget/useFeedbackWidget.js
@@ -0,0 +1,38 @@
+import { useCallback, useState } from 'react';
+
+const useFeedbackWidget = () => {
+ const [showFeedbackWidget, setShowFeedbackWidget] = useState(true);
+ const [showGratitudeText, setShowGratitudeText] = useState(false);
+
+ const closeFeedbackWidget = useCallback(() => {
+ setShowFeedbackWidget(false);
+ }, [setShowFeedbackWidget]);
+
+ const openFeedbackWidget = useCallback(() => {
+ setShowFeedbackWidget(true);
+ }, [setShowFeedbackWidget]);
+
+ const openGratitudeText = useCallback(() => {
+ setShowGratitudeText(true);
+ setTimeout(() => {
+ setShowGratitudeText(false);
+ }, 3000);
+ }, [setShowGratitudeText]);
+
+ const sendFeedback = useCallback(() => {
+ // Create feedback
+ closeFeedbackWidget();
+ openGratitudeText();
+ }, [closeFeedbackWidget, openGratitudeText]);
+
+ return {
+ closeFeedbackWidget,
+ openFeedbackWidget,
+ openGratitudeText,
+ sendFeedback,
+ showFeedbackWidget,
+ showGratitudeText,
+ };
+};
+
+export default useFeedbackWidget;
diff --git a/src/courseware/course/sequence/Unit/feedback-widget/useFeedbackWidget.test.js b/src/courseware/course/sequence/Unit/feedback-widget/useFeedbackWidget.test.js
new file mode 100644
index 0000000000..7990937453
--- /dev/null
+++ b/src/courseware/course/sequence/Unit/feedback-widget/useFeedbackWidget.test.js
@@ -0,0 +1,59 @@
+import { renderHook, act } from '@testing-library/react-hooks';
+
+import useFeedbackWidget from './useFeedbackWidget';
+
+describe('useFeedbackWidget', () => {
+ test('closeFeedbackWidget behavior', () => {
+ const { result } = renderHook(() => useFeedbackWidget());
+
+ expect(result.current.showFeedbackWidget).toBe(true);
+ act(() => {
+ result.current.closeFeedbackWidget();
+ });
+ expect(result.current.showFeedbackWidget).toBe(false);
+ });
+
+ test('openFeedbackWidget behavior', () => {
+ const { result } = renderHook(() => useFeedbackWidget());
+
+ act(() => {
+ result.current.closeFeedbackWidget();
+ });
+ expect(result.current.showFeedbackWidget).toBe(false);
+ act(() => {
+ result.current.openFeedbackWidget();
+ });
+ expect(result.current.showFeedbackWidget).toBe(true);
+ });
+
+ test('openGratitudeText behavior', async () => {
+ const { result, waitFor } = renderHook(() => useFeedbackWidget());
+
+ expect(result.current.showGratitudeText).toBe(false);
+ act(() => {
+ result.current.openGratitudeText();
+ });
+ expect(result.current.showGratitudeText).toBe(true);
+ // Wait for 3 seconds to hide the gratitude text
+ waitFor(() => {
+ expect(result.current.showGratitudeText).toBe(false);
+ }, { timeout: 3000 });
+ });
+
+ test('sendFeedback behavior', () => {
+ const { result, waitFor } = renderHook(() => useFeedbackWidget());
+
+ expect(result.current.showFeedbackWidget).toBe(true);
+ expect(result.current.showGratitudeText).toBe(false);
+ act(() => {
+ result.current.sendFeedback();
+ });
+ expect(result.current.showFeedbackWidget).toBe(false);
+ expect(result.current.showGratitudeText).toBe(true);
+
+ // Wait for 3 seconds to hide the gratitude text
+ waitFor(() => {
+ expect(result.current.showGratitudeText).toBe(false);
+ }, { timeout: 3000 });
+ });
+});