diff --git a/.env b/.env index 76b0857ed4..32adc06e74 100644 --- a/.env +++ b/.env @@ -4,7 +4,6 @@ NODE_ENV='production' ACCESS_TOKEN_COOKIE_NAME='' -AI_TRANSLATIONS_URL='' BASE_URL='' CONTACT_URL='' CREDENTIALS_BASE_URL='' diff --git a/.env.development b/.env.development index e19ff1b8de..aa70982169 100644 --- a/.env.development +++ b/.env.development @@ -4,7 +4,6 @@ NODE_ENV='development' ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload' -AI_TRANSLATIONS_URL='http://localhost:18760' BASE_URL='http://localhost:2000' CONTACT_URL='http://localhost:18000/contact' CREDENTIALS_BASE_URL='http://localhost:18150' diff --git a/.env.test b/.env.test index 6d241af4e2..34745fe206 100644 --- a/.env.test +++ b/.env.test @@ -4,7 +4,6 @@ NODE_ENV='test' ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload' -AI_TRANSLATIONS_URL='http://localhost:18760' BASE_URL='http://localhost:2000' CONTACT_URL='http://localhost:18000/contact' CREDENTIALS_BASE_URL='http://localhost:18150' diff --git a/example.env.config.jsx b/example.env.config.jsx index 9ffb019e26..ecbb4ba95f 100644 --- a/example.env.config.jsx +++ b/example.env.config.jsx @@ -1,4 +1,4 @@ -import UnitTranslationPlugin from '@plugins/UnitTranslationPlugin'; +import UnitTranslationPlugin from '@edx/unit-translation-selector-plugin'; import { PLUGIN_OPERATIONS, DIRECT_PLUGIN } from '@openedx/frontend-plugin-framework'; // Load environment variables from .env file diff --git a/jest.config.js b/jest.config.js index 9fd4138f38..72fe697b39 100644 --- a/jest.config.js +++ b/jest.config.js @@ -15,7 +15,6 @@ const config = createConfig('jest', { // See https://stackoverflow.com/questions/72382316/jest-encountered-an-unexpected-token-react-markdown 'react-markdown': '/node_modules/react-markdown/react-markdown.min.js', '@src/(.*)': '/src/$1', - '@plugins/(.*)': '/plugins/$1', }, testTimeout: 30000, globalSetup: "./global-setup.js", diff --git a/plugins/README.md b/plugins/README.md deleted file mode 100644 index 62fcbf06be..0000000000 --- a/plugins/README.md +++ /dev/null @@ -1,17 +0,0 @@ -## How to develop plugin - -You can define plugin in `env.config.jsx` see `example.env.config.jsx` as example. - -## Current caveat - -- The way for how I deal with override method is still wonky - - The redux still require middleware to ignore the plugin's action from serializing - - I am not sure how it behave with useCallback, useMemo, ...etc - - There are still open question on how to write it properly - -## Current work that should consider core part and extendable for the future plugin framework - -- `usePluingsCallback` is the callback supose to be some level of equality to be using `React.useCallback`. It would try to execute the function, then any plugin that try `registerOverrideMethod`. The order of the it being run isn't the determined. There are a couple things I want to add: - - I might consider testing it with `zustand` library to make sure it is portable and not rely on `redux`. I tried to do this with provider, but it seems to run into infinite loop of trigger changed. - -- `registerOverrideMethod` is working like a way to register callback that behave like a middleware. It ran the default one, then pass the result of the default one to the plugin. Any plugin that register the override can update the value. Alternatively, we can override the function completely instead applying each affect. Or we can support both. But it requires a bit more thought out architecture. diff --git a/plugins/UnitTranslationPlugin/__snapshots__/index.test.jsx.snap b/plugins/UnitTranslationPlugin/__snapshots__/index.test.jsx.snap deleted file mode 100644 index de7768e3a9..0000000000 --- a/plugins/UnitTranslationPlugin/__snapshots__/index.test.jsx.snap +++ /dev/null @@ -1,29 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` render TranslationSelection when translation is enabled and language is available 1`] = ` - -`; - -exports[` render translation when the user is staff 1`] = ` - -`; diff --git a/plugins/UnitTranslationPlugin/data/api.js b/plugins/UnitTranslationPlugin/data/api.js deleted file mode 100644 index 7347906467..0000000000 --- a/plugins/UnitTranslationPlugin/data/api.js +++ /dev/null @@ -1,90 +0,0 @@ -import { getConfig, camelCaseObject } from '@edx/frontend-platform'; -import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { logError } from '@edx/frontend-platform/logging'; -import { stringify } from 'query-string'; - -export const fetchTranslationConfig = async (courseId) => { - const url = `${ - getConfig().LMS_BASE_URL - }/api/translatable_xblocks/config/?course_id=${encodeURIComponent(courseId)}`; - try { - const { data } = await getAuthenticatedHttpClient().get(url); - return { - enabled: data.feature_enabled, - availableLanguages: data.available_translation_languages || [ - { - code: 'en', - label: 'English', - }, - { - code: 'es', - label: 'Spanish', - }, - ], - }; - } catch (error) { - logError(`Translation plugin fail to fetch from ${url}`, error); - return { - enabled: false, - availableLanguages: [], - }; - } -}; - -export async function getTranslationFeedback({ - courseId, - translationLanguage, - unitId, - userId, -}) { - const params = stringify({ - translation_language: translationLanguage, - course_id: encodeURIComponent(courseId), - unit_id: encodeURIComponent(unitId), - user_id: userId, - }); - const fetchFeedbackUrl = `${ - getConfig().AI_TRANSLATIONS_URL - }/api/v1/whole-course-translation-feedback?${params}`; - try { - const { data } = await getAuthenticatedHttpClient().get(fetchFeedbackUrl); - return camelCaseObject(data); - } catch (error) { - logError( - `Translation plugin fail to fetch from ${fetchFeedbackUrl}`, - error, - ); - return {}; - } -} - -export async function createTranslationFeedback({ - courseId, - feedbackValue, - translationLanguage, - unitId, - userId, -}) { - const createFeedbackUrl = `${ - getConfig().AI_TRANSLATIONS_URL - }/api/v1/whole-course-translation-feedback/`; - try { - const { data } = await getAuthenticatedHttpClient().post( - createFeedbackUrl, - { - course_id: courseId, - feedback_value: feedbackValue, - translation_language: translationLanguage, - unit_id: unitId, - user_id: userId, - }, - ); - return camelCaseObject(data); - } catch (error) { - logError( - `Translation plugin fail to create feedback from ${createFeedbackUrl}`, - error, - ); - return {}; - } -} diff --git a/plugins/UnitTranslationPlugin/data/api.test.js b/plugins/UnitTranslationPlugin/data/api.test.js deleted file mode 100644 index 4d3a22d219..0000000000 --- a/plugins/UnitTranslationPlugin/data/api.test.js +++ /dev/null @@ -1,125 +0,0 @@ -import { camelCaseObject } from '@edx/frontend-platform'; -import { logError } from '@edx/frontend-platform/logging'; -import { stringify } from 'query-string'; - -import { - fetchTranslationConfig, - getTranslationFeedback, - createTranslationFeedback, -} from './api'; - -const mockGetMethod = jest.fn(); -const mockPostMethod = jest.fn(); -jest.mock('@edx/frontend-platform/auth', () => ({ - getAuthenticatedHttpClient: () => ({ - get: mockGetMethod, - post: mockPostMethod, - }), -})); -jest.mock('@edx/frontend-platform/logging', () => ({ - logError: jest.fn(), -})); - -describe('UnitTranslation api', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - describe('fetchTranslationConfig', () => { - const courseId = 'course-v1:edX+DemoX+Demo_Course'; - const expectedResponse = { - feature_enabled: true, - available_translation_languages: [ - { - code: 'en', - label: 'English', - }, - { - code: 'es', - label: 'Spanish', - }, - ], - }; - it('should fetch translation config', async () => { - const expectedUrl = `http://localhost:18000/api/translatable_xblocks/config/?course_id=${encodeURIComponent( - courseId, - )}`; - mockGetMethod.mockResolvedValueOnce({ data: expectedResponse }); - const result = await fetchTranslationConfig(courseId); - expect(result).toEqual({ - enabled: true, - availableLanguages: expectedResponse.available_translation_languages, - }); - expect(mockGetMethod).toHaveBeenCalledWith(expectedUrl); - }); - - it('should return disabled and unavailable languages on error', async () => { - mockGetMethod.mockRejectedValueOnce(new Error('error')); - const result = await fetchTranslationConfig(courseId); - expect(result).toEqual({ - enabled: false, - availableLanguages: [], - }); - expect(logError).toHaveBeenCalled(); - }); - }); - - describe('getTranslationFeedback', () => { - const props = { - courseId: 'course-v1:edX+DemoX+Demo_Course', - translationLanguage: 'es', - unitId: 'unit-v1:edX+DemoX+Demo_Course+type@video+block@video', - userId: 'test_user', - }; - const expectedResponse = { - feedback: 'good', - }; - it('should fetch translation feedback', async () => { - const params = stringify({ - translation_language: props.translationLanguage, - course_id: encodeURIComponent(props.courseId), - unit_id: encodeURIComponent(props.unitId), - user_id: props.userId, - }); - const expectedUrl = `http://localhost:18760/api/v1/whole-course-translation-feedback?${params}`; - mockGetMethod.mockResolvedValueOnce({ data: expectedResponse }); - const result = await getTranslationFeedback(props); - expect(result).toEqual(camelCaseObject(expectedResponse)); - expect(mockGetMethod).toHaveBeenCalledWith(expectedUrl); - }); - - it('should return empty object on error', async () => { - mockGetMethod.mockRejectedValueOnce(new Error('error')); - const result = await getTranslationFeedback(props); - expect(result).toEqual({}); - expect(logError).toHaveBeenCalled(); - }); - }); - - describe('createTranslationFeedback', () => { - const props = { - courseId: 'course-v1:edX+DemoX+Demo_Course', - feedbackValue: 'good', - translationLanguage: 'es', - unitId: 'unit-v1:edX+DemoX+Demo_Course+type@video+block@video', - userId: 'test_user', - }; - it('should create translation feedback', async () => { - const expectedUrl = 'http://localhost:18760/api/v1/whole-course-translation-feedback/'; - mockPostMethod.mockResolvedValueOnce({}); - await createTranslationFeedback(props); - expect(mockPostMethod).toHaveBeenCalledWith(expectedUrl, { - course_id: props.courseId, - feedback_value: props.feedbackValue, - translation_language: props.translationLanguage, - unit_id: props.unitId, - user_id: props.userId, - }); - }); - - it('should log error on failure', async () => { - mockPostMethod.mockRejectedValueOnce(new Error('error')); - await createTranslationFeedback(props); - expect(logError).toHaveBeenCalled(); - }); - }); -}); diff --git a/plugins/UnitTranslationPlugin/feedback-widget/__snapshots__/index.test.jsx.snap b/plugins/UnitTranslationPlugin/feedback-widget/__snapshots__/index.test.jsx.snap deleted file mode 100644 index 0fb87db44c..0000000000 --- a/plugins/UnitTranslationPlugin/feedback-widget/__snapshots__/index.test.jsx.snap +++ /dev/null @@ -1,204 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` render feedback widget 1`] = ` -
-
-
- - Rate this page translation - -
- - -
-
- | -
-
- -
-
-
-
-
-`; - -exports[` render gratitude text 1`] = ` -
-
-
- - Thank you! Your feedback matters. - -
-
-
-`; - -exports[` renders hidden by default 1`] = ` -
-
-
- - Rate this page translation - -
- - -
-
- | -
-
- -
-
-
-
- - Thank you! Your feedback matters. - -
-
-
-`; - -exports[` renders show when elemReady is true 1`] = ` -
-
-
- - Rate this page translation - -
- - -
-
- | -
-
- -
-
-
-
- - Thank you! Your feedback matters. - -
-
-
-`; diff --git a/plugins/UnitTranslationPlugin/feedback-widget/index.jsx b/plugins/UnitTranslationPlugin/feedback-widget/index.jsx deleted file mode 100644 index d8f50c4c1b..0000000000 --- a/plugins/UnitTranslationPlugin/feedback-widget/index.jsx +++ /dev/null @@ -1,116 +0,0 @@ -import React, { - useEffect, useRef, useState, -} from 'react'; -import PropTypes from 'prop-types'; - -import { useIntl } from '@edx/frontend-platform/i18n'; -import { ActionRow, IconButton, Icon } from '@openedx/paragon'; -import { Close, ThumbUpOutline, ThumbDownOffAlt } from '@openedx/paragon/icons'; - -import './index.scss'; -import messages from './messages'; -import useFeedbackWidget from './useFeedbackWidget'; - -const FeedbackWidget = ({ - courseId, - translationLanguage, - unitId, - userId, -}) => { - const { formatMessage } = useIntl(); - const ref = useRef(null); - const [elemReady, setElemReady] = useState(false); - const { - closeFeedbackWidget, - showFeedbackWidget, - showGratitudeText, - onThumbsUpClick, - onThumbsDownClick, - } = useFeedbackWidget({ - courseId, - translationLanguage, - unitId, - userId, - }); - - useEffect(() => { - if (ref.current) { - const domNode = document.getElementById('whole-course-translation-feedback-widget'); - domNode.appendChild(ref.current); - setElemReady(true); - } - }, [ref.current]); - - return ( -
- {(showFeedbackWidget || showGratitudeText) ? ( -
- { - showFeedbackWidget && ( -
- - {formatMessage(messages.rateTranslationText)} - -
- - -
-
- | -
-
- -
-
-
- ) - } - { - showGratitudeText && ( -
- - {formatMessage(messages.gratitudeText)} - -
- ) - } -
- ) : null} -
- ); -}; - -FeedbackWidget.propTypes = { - courseId: PropTypes.string.isRequired, - translationLanguage: PropTypes.string.isRequired, - userId: PropTypes.string.isRequired, - unitId: PropTypes.string.isRequired, -}; - -FeedbackWidget.defaultProps = {}; - -export default FeedbackWidget; diff --git a/plugins/UnitTranslationPlugin/feedback-widget/index.scss b/plugins/UnitTranslationPlugin/feedback-widget/index.scss deleted file mode 100644 index 9c88934bfa..0000000000 --- a/plugins/UnitTranslationPlugin/feedback-widget/index.scss +++ /dev/null @@ -1,4 +0,0 @@ -.action-row-divider { - font-size: 31px; - font-weight: 100; -} \ No newline at end of file diff --git a/plugins/UnitTranslationPlugin/feedback-widget/index.test.jsx b/plugins/UnitTranslationPlugin/feedback-widget/index.test.jsx deleted file mode 100644 index e2a492f6b6..0000000000 --- a/plugins/UnitTranslationPlugin/feedback-widget/index.test.jsx +++ /dev/null @@ -1,107 +0,0 @@ -import { useState } from 'react'; -import { shallow } from '@edx/react-unit-test-utils'; - -import FeedbackWidget from './index'; -import useFeedbackWidget from './useFeedbackWidget'; - -jest.mock('react', () => ({ - ...jest.requireActual('react'), - useState: jest.fn((value) => [value, jest.fn()]), -})); -jest.mock('@openedx/paragon', () => jest.requireActual('@edx/react-unit-test-utils').mockComponents({ - ActionRow: { - Spacer: 'Spacer', - }, - IconButton: 'IconButton', - Icon: 'Icon', -})); -jest.mock('@openedx/paragon/icons', () => ({ - Close: 'Close', - ThumbUpOutline: 'ThumbUpOutline', - ThumbDownOffAlt: 'ThumbDownOffAlt', -})); -jest.mock('./useFeedbackWidget'); -jest.mock('@edx/frontend-platform/i18n', () => { - const i18n = jest.requireActual('@edx/frontend-platform/i18n'); - const { formatMessage } = jest.requireActual('@edx/react-unit-test-utils'); - return { - ...i18n, - useIntl: jest.fn(() => ({ - formatMessage, - })), - }; -}); - -describe('', () => { - const props = { - courseId: 'course-v1:edX+DemoX+Demo_Course', - translationLanguage: 'es', - unitId: - 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@37b72b3915204b70acb00c55b604b563', - userId: '123', - }; - - const mockUseFeedbackWidget = ({ showFeedbackWidget, showGratitudeText }) => { - useFeedbackWidget.mockReturnValueOnce({ - closeFeedbackWidget: jest.fn().mockName('closeFeedbackWidget'), - sendFeedback: jest.fn().mockName('sendFeedback'), - onThumbsUpClick: jest.fn().mockName('onThumbsUpClick'), - onThumbsDownClick: jest.fn().mockName('onThumbsDownClick'), - showFeedbackWidget, - showGratitudeText, - }); - }; - - it('renders hidden by default', () => { - mockUseFeedbackWidget({ - showFeedbackWidget: true, - showGratitudeText: true, - }); - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); - expect(wrapper.instance.findByType('div')[0].props.className).toContain( - 'd-none', - ); - }); - - it('renders show when elemReady is true', () => { - mockUseFeedbackWidget({ - showFeedbackWidget: true, - showGratitudeText: true, - }); - useState.mockReturnValueOnce([true, jest.fn()]); - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); - expect(wrapper.instance.findByType('div')[0].props.className).not.toContain( - 'd-none', - ); - }); - - it('render empty when showFeedbackWidget and showGratitudeText are false', () => { - mockUseFeedbackWidget({ - showFeedbackWidget: false, - showGratitudeText: false, - }); - useState.mockReturnValueOnce([true, jest.fn()]); - const wrapper = shallow(); - expect(wrapper.instance.findByType('div')[0].children.length).toBe(0); - }); - - it('render feedback widget', () => { - mockUseFeedbackWidget({ - showFeedbackWidget: true, - showGratitudeText: false, - }); - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); - }); - - it('render gratitude text', () => { - mockUseFeedbackWidget({ - showFeedbackWidget: false, - showGratitudeText: true, - }); - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); - }); -}); diff --git a/plugins/UnitTranslationPlugin/feedback-widget/messages.js b/plugins/UnitTranslationPlugin/feedback-widget/messages.js deleted file mode 100644 index f84f836a86..0000000000 --- a/plugins/UnitTranslationPlugin/feedback-widget/messages.js +++ /dev/null @@ -1,16 +0,0 @@ -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/plugins/UnitTranslationPlugin/feedback-widget/useFeedbackWidget.js b/plugins/UnitTranslationPlugin/feedback-widget/useFeedbackWidget.js deleted file mode 100644 index 32b61f7684..0000000000 --- a/plugins/UnitTranslationPlugin/feedback-widget/useFeedbackWidget.js +++ /dev/null @@ -1,82 +0,0 @@ -import { useCallback, useEffect, useState } from 'react'; - -import { createTranslationFeedback, getTranslationFeedback } from '../data/api'; - -const useFeedbackWidget = ({ - courseId, - translationLanguage, - unitId, - userId, -}) => { - const [showFeedbackWidget, setShowFeedbackWidget] = useState(false); - const [showGratitudeText, setShowGratitudeText] = useState(false); - - const closeFeedbackWidget = useCallback(() => { - setShowFeedbackWidget(false); - }, [setShowFeedbackWidget]); - - const openFeedbackWidget = useCallback(() => { - setShowFeedbackWidget(true); - }, [setShowFeedbackWidget]); - - useEffect(async () => { - const translationFeedback = await getTranslationFeedback({ - courseId, - translationLanguage, - unitId, - userId, - }); - setShowFeedbackWidget(!translationFeedback); - }, [ - courseId, - translationLanguage, - unitId, - userId, - ]); - - const openGratitudeText = useCallback(() => { - setShowGratitudeText(true); - setTimeout(() => { - setShowGratitudeText(false); - }, 3000); - }, [setShowGratitudeText]); - - const sendFeedback = useCallback(async (feedbackValue) => { - await createTranslationFeedback({ - courseId, - feedbackValue, - translationLanguage, - unitId, - userId, - }); - closeFeedbackWidget(); - openGratitudeText(); - }, [ - courseId, - translationLanguage, - unitId, - userId, - closeFeedbackWidget, - openGratitudeText, - ]); - - const onThumbsUpClick = useCallback(() => { - sendFeedback(true); - }, [sendFeedback]); - const onThumbsDownClick = useCallback(() => { - sendFeedback(false); - }, [sendFeedback]); - - return { - closeFeedbackWidget, - openFeedbackWidget, - openGratitudeText, - sendFeedback, - showFeedbackWidget, - showGratitudeText, - onThumbsUpClick, - onThumbsDownClick, - }; -}; - -export default useFeedbackWidget; diff --git a/plugins/UnitTranslationPlugin/feedback-widget/useFeedbackWidget.test.js b/plugins/UnitTranslationPlugin/feedback-widget/useFeedbackWidget.test.js deleted file mode 100644 index 810370857e..0000000000 --- a/plugins/UnitTranslationPlugin/feedback-widget/useFeedbackWidget.test.js +++ /dev/null @@ -1,163 +0,0 @@ -import { renderHook, act } from '@testing-library/react-hooks'; - -import useFeedbackWidget from './useFeedbackWidget'; -import { createTranslationFeedback, getTranslationFeedback } from '../data/api'; - -jest.mock('../data/api', () => ({ - createTranslationFeedback: jest.fn(), - getTranslationFeedback: jest.fn(), -})); - -const initialProps = { - courseId: 'course-v1:edX+DemoX+Demo_Course', - translationLanguage: 'es', - unitId: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc', - userId: 3, -}; - -const newProps = { - courseId: 'course-v1:edX+DemoX+Demo_Course', - translationLanguage: 'fr', - unitId: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc', - userId: 3, -}; - -describe('useFeedbackWidget', () => { - beforeEach(async () => { - getTranslationFeedback.mockReturnValue(''); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - test('closeFeedbackWidget behavior', () => { - const { result, waitFor } = renderHook(() => useFeedbackWidget(initialProps)); - waitFor(() => expect(result.current.showFeedbackWidget.toBe(true))); - act(() => { - result.current.closeFeedbackWidget(); - }); - expect(result.current.showFeedbackWidget).toBe(false); - }); - - test('openFeedbackWidget behavior', () => { - const { result } = renderHook(() => useFeedbackWidget(initialProps)); - 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(initialProps)); - - 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(initialProps)); - const feedbackValue = true; - - waitFor(() => expect(result.current.showFeedbackWidget.toBe(true))); - - expect(result.current.showGratitudeText).toBe(false); - act(() => { - result.current.sendFeedback(feedbackValue); - }); - - waitFor(() => { - expect(result.current.showFeedbackWidget).toBe(false); - expect(result.current.showGratitudeText).toBe(true); - }); - - expect(createTranslationFeedback).toHaveBeenCalledWith({ - courseId: initialProps.courseId, - feedbackValue, - translationLanguage: initialProps.translationLanguage, - unitId: initialProps.unitId, - userId: initialProps.userId, - }); - - // Wait for 3 seconds to hide the gratitude text - waitFor(() => { - expect(result.current.showGratitudeText).toBe(false); - }, { timeout: 3000 }); - }); - - test('onThumbsUpClick behavior', () => { - const { result } = renderHook(() => useFeedbackWidget(initialProps)); - - act(() => { - result.current.onThumbsUpClick(); - }); - - expect(createTranslationFeedback).toHaveBeenCalledWith({ - courseId: initialProps.courseId, - feedbackValue: true, - translationLanguage: initialProps.translationLanguage, - unitId: initialProps.unitId, - userId: initialProps.userId, - }); - }); - - test('onThumbsDownClick behavior', () => { - const { result } = renderHook(() => useFeedbackWidget(initialProps)); - - act(() => { - result.current.onThumbsDownClick(); - }); - - expect(createTranslationFeedback).toHaveBeenCalledWith({ - courseId: initialProps.courseId, - feedbackValue: false, - translationLanguage: initialProps.translationLanguage, - unitId: initialProps.unitId, - userId: initialProps.userId, - }); - }); - - test('fetch feedback on initialization', () => { - const { waitFor } = renderHook(() => useFeedbackWidget(initialProps)); - waitFor(() => { - expect(getTranslationFeedback).toHaveBeenCalledWith({ - courseId: initialProps.courseId, - translationLanguage: initialProps.translationLanguage, - unitId: initialProps.unitId, - userId: initialProps.userId, - }); - }); - }); - - test('fetch feedback on props update', () => { - const { rerender, waitFor } = renderHook(() => useFeedbackWidget(initialProps)); - waitFor(() => { - expect(getTranslationFeedback).toHaveBeenCalledWith({ - courseId: initialProps.courseId, - translationLanguage: initialProps.translationLanguage, - unitId: initialProps.unitId, - userId: initialProps.userId, - }); - }); - rerender(newProps); - waitFor(() => { - expect(getTranslationFeedback).toHaveBeenCalledWith({ - courseId: newProps.courseId, - translationLanguage: newProps.translationLanguage, - unitId: newProps.unitId, - userId: newProps.userId, - }); - }); - }); -}); diff --git a/plugins/UnitTranslationPlugin/index.jsx b/plugins/UnitTranslationPlugin/index.jsx deleted file mode 100644 index 54e3e1565c..0000000000 --- a/plugins/UnitTranslationPlugin/index.jsx +++ /dev/null @@ -1,53 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; - -import { useModel } from '@src/generic/model-store'; -import { VERIFIED_MODES } from '@src/constants'; - -import TranslationSelection from './translation-selection'; -import { fetchTranslationConfig } from './data/api'; - -const UnitTranslationPlugin = ({ id, courseId, unitId }) => { - const { language, enrollmentMode } = useModel('coursewareMeta', courseId); - const { isStaff } = useModel('courseHomeMeta', courseId); - const [translationConfig, setTranslationConfig] = useState({ - enabled: false, - availableLanguages: [], - }); - - const hasVerifiedEnrollment = isStaff || ( - enrollmentMode !== null - && enrollmentMode !== undefined - && VERIFIED_MODES.includes(enrollmentMode) - ); - - useEffect(() => { - if (hasVerifiedEnrollment) { - fetchTranslationConfig(courseId).then(setTranslationConfig); - } - }, []); - - const { enabled, availableLanguages } = translationConfig; - - if (!hasVerifiedEnrollment || !enabled || !language || !availableLanguages.length) { - return null; - } - - return ( - - ); -}; - -UnitTranslationPlugin.propTypes = { - id: PropTypes.string.isRequired, - courseId: PropTypes.string.isRequired, - unitId: PropTypes.string.isRequired, -}; - -export default UnitTranslationPlugin; diff --git a/plugins/UnitTranslationPlugin/index.test.jsx b/plugins/UnitTranslationPlugin/index.test.jsx deleted file mode 100644 index e606369b0f..0000000000 --- a/plugins/UnitTranslationPlugin/index.test.jsx +++ /dev/null @@ -1,102 +0,0 @@ -import { when } from 'jest-when'; - -import { shallow } from '@edx/react-unit-test-utils'; -import { useState } from 'react'; -import { useModel } from '@src/generic/model-store'; - -import UnitTranslationPlugin from './index'; - -jest.mock('@src/generic/model-store'); -jest.mock('./data/api', () => ({ - fetchTranslationConfig: jest.fn(), -})); -jest.mock('./translation-selection', () => 'TranslationSelection'); -jest.mock('react', () => ({ - ...jest.requireActual('react'), - useState: jest.fn(), -})); - -describe('', () => { - const props = { - id: 'id', - courseId: 'courseId', - unitId: 'unitId', - }; - const mockInitialState = ({ - enabled = true, - availableLanguages = ['en'], - language = 'en', - enrollmentMode = 'verified', - isStaff = false, - }) => { - useState.mockReturnValueOnce([{ enabled, availableLanguages }, jest.fn()]); - - when(useModel) - .calledWith('coursewareMeta', props.courseId) - .mockReturnValueOnce({ language, enrollmentMode }); - - when(useModel) - .calledWith('courseHomeMeta', props.courseId) - .mockReturnValueOnce({ isStaff }); - }; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('render empty when translation is not enabled', () => { - mockInitialState({ enabled: false }); - - const wrapper = shallow(); - - expect(wrapper.isEmptyRender()).toBe(true); - }); - it('render empty when available languages is empty', () => { - mockInitialState({ - availableLanguages: [], - }); - - const wrapper = shallow(); - - expect(wrapper.isEmptyRender()).toBe(true); - }); - - it('render empty when course language has not been set', () => { - mockInitialState({ - language: null, - }); - - const wrapper = shallow(); - - expect(wrapper.isEmptyRender()).toBe(true); - }); - - it('render empty when student is enroll as verified', () => { - mockInitialState({ - enrollmentMode: 'audit', - }); - - const wrapper = shallow(); - - expect(wrapper.isEmptyRender()).toBe(true); - }); - - it('render translation when the user is staff', () => { - mockInitialState({ - enrollmentMode: 'audit', - isStaff: true, - }); - - const wrapper = shallow(); - - expect(wrapper.snapshot).toMatchSnapshot(); - }); - - it('render TranslationSelection when translation is enabled and language is available', () => { - mockInitialState({}); - - const wrapper = shallow(); - - expect(wrapper.snapshot).toMatchSnapshot(); - }); -}); diff --git a/plugins/UnitTranslationPlugin/translation-selection/TranslationModal.jsx b/plugins/UnitTranslationPlugin/translation-selection/TranslationModal.jsx deleted file mode 100644 index b1de5bd961..0000000000 --- a/plugins/UnitTranslationPlugin/translation-selection/TranslationModal.jsx +++ /dev/null @@ -1,82 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import { useIntl } from '@edx/frontend-platform/i18n'; -import { - StandardModal, - ActionRow, - Button, - Icon, - ListBox, - ListBoxOption, -} from '@openedx/paragon'; -import { Check } from '@openedx/paragon/icons'; - -import useTranslationModal from './useTranslationModal'; -import messages from './messages'; - -import './TranslationModal.scss'; - -const TranslationModal = ({ - isOpen, - close, - selectedLanguage, - setSelectedLanguage, - availableLanguages, -}) => { - const { formatMessage } = useIntl(); - const { selectedIndex, setSelectedIndex, onSubmit } = useTranslationModal({ - selectedLanguage, - setSelectedLanguage, - close, - availableLanguages, - }); - - return ( - - - - - - )} - > - - {availableLanguages.map(({ code, label }, index) => ( - setSelectedIndex(index)} - > - {label} - {selectedIndex === index && } - - ))} - - - ); -}; - -TranslationModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - close: PropTypes.func.isRequired, - selectedLanguage: PropTypes.string.isRequired, - setSelectedLanguage: PropTypes.func.isRequired, - availableLanguages: PropTypes.arrayOf( - PropTypes.shape({ - code: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - }), - ).isRequired, -}; - -export default TranslationModal; diff --git a/plugins/UnitTranslationPlugin/translation-selection/TranslationModal.scss b/plugins/UnitTranslationPlugin/translation-selection/TranslationModal.scss deleted file mode 100644 index 060fc014f3..0000000000 --- a/plugins/UnitTranslationPlugin/translation-selection/TranslationModal.scss +++ /dev/null @@ -1,7 +0,0 @@ -.listbox-container { - max-height: 400px; - - :last-child { - margin-bottom: 5px; - } -} \ No newline at end of file diff --git a/plugins/UnitTranslationPlugin/translation-selection/TranslationModal.test.jsx b/plugins/UnitTranslationPlugin/translation-selection/TranslationModal.test.jsx deleted file mode 100644 index 81215be77c..0000000000 --- a/plugins/UnitTranslationPlugin/translation-selection/TranslationModal.test.jsx +++ /dev/null @@ -1,59 +0,0 @@ -import { shallow } from '@edx/react-unit-test-utils'; - -import TranslationModal from './TranslationModal'; - -jest.mock('./useTranslationModal', () => ({ - __esModule: true, - default: () => ({ - selectedIndex: 0, - setSelectedIndex: jest.fn(), - onSubmit: jest.fn().mockName('onSubmit'), - }), -})); -jest.mock('@openedx/paragon', () => jest.requireActual('@edx/react-unit-test-utils').mockComponents({ - StandardModal: 'StandardModal', - ActionRow: { - Spacer: 'Spacer', - }, - Button: 'Button', - Icon: 'Icon', - ListBox: 'ListBox', - ListBoxOption: 'ListBoxOption', -})); -jest.mock('@openedx/paragon/icons', () => ({ - Check: jest.fn().mockName('icons.Check'), -})); -jest.mock('@edx/frontend-platform/i18n', () => { - const i18n = jest.requireActual('@edx/frontend-platform/i18n'); - const { formatMessage } = jest.requireActual('@edx/react-unit-test-utils'); - return { - ...i18n, - useIntl: jest.fn(() => ({ - formatMessage, - })), - }; -}); - -describe('TranslationModal', () => { - const props = { - isOpen: true, - close: jest.fn().mockName('close'), - selectedLanguage: 'en', - setSelectedLanguage: jest.fn().mockName('setSelectedLanguage'), - availableLanguages: [ - { - code: 'en', - label: 'English', - }, - { - code: 'es', - label: 'Spanish', - }, - ], - }; - it('renders correctly', () => { - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); - expect(wrapper.instance.findByType('ListBoxOption')).toHaveLength(2); - }); -}); diff --git a/plugins/UnitTranslationPlugin/translation-selection/__snapshots__/TranslationModal.test.jsx.snap b/plugins/UnitTranslationPlugin/translation-selection/__snapshots__/TranslationModal.test.jsx.snap deleted file mode 100644 index 6a6c7feafe..0000000000 --- a/plugins/UnitTranslationPlugin/translation-selection/__snapshots__/TranslationModal.test.jsx.snap +++ /dev/null @@ -1,49 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TranslationModal renders correctly 1`] = ` - - - - - - } - isOpen={true} - onClose={[MockFunction close]} - title="Translate this course" -> - - - English - - - - Spanish - - - -`; diff --git a/plugins/UnitTranslationPlugin/translation-selection/__snapshots__/index.test.jsx.snap b/plugins/UnitTranslationPlugin/translation-selection/__snapshots__/index.test.jsx.snap deleted file mode 100644 index 432dda8a88..0000000000 --- a/plugins/UnitTranslationPlugin/translation-selection/__snapshots__/index.test.jsx.snap +++ /dev/null @@ -1,50 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` renders 1`] = ` - - - - - - -`; diff --git a/plugins/UnitTranslationPlugin/translation-selection/index.jsx b/plugins/UnitTranslationPlugin/translation-selection/index.jsx deleted file mode 100644 index 177194f2f3..0000000000 --- a/plugins/UnitTranslationPlugin/translation-selection/index.jsx +++ /dev/null @@ -1,115 +0,0 @@ -import React, { useContext, useEffect } from 'react'; -import PropTypes from 'prop-types'; - -import { sendTrackEvent } from '@edx/frontend-platform/analytics'; -import { AppContext } from '@edx/frontend-platform/react'; -import { IconButton, Icon, ProductTour } from '@openedx/paragon'; -import { Language } from '@openedx/paragon/icons'; -import { useDispatch } from 'react-redux'; -import { stringifyUrl } from 'query-string'; - -import { registerOverrideMethod } from '@src/generic/plugin-store'; - -import TranslationModal from './TranslationModal'; -import useTranslationTour from './useTranslationTour'; -import useSelectLanguage from './useSelectLanguage'; -import FeedbackWidget from '../feedback-widget'; - -const TranslationSelection = ({ - id, courseId, language, availableLanguages, unitId, -}) => { - const { - authenticatedUser: { userId }, - } = useContext(AppContext); - const dispatch = useDispatch(); - const { - translationTour, isOpen, open, close, - } = useTranslationTour(); - - const { selectedLanguage, setSelectedLanguage } = useSelectLanguage({ - courseId, - language, - }); - - useEffect(() => { - if ((courseId && language && selectedLanguage && unitId && userId) && (language !== selectedLanguage)) { - const eventName = 'edx.whole_course_translations.translation_requested'; - const eventProperties = { - courseId, - sourceLanguage: language, - targetLanguage: selectedLanguage, - unitId, - userId, - }; - sendTrackEvent(eventName, eventProperties); - } - }, [courseId, language, selectedLanguage, unitId, userId]); - - useEffect(() => { - dispatch( - registerOverrideMethod({ - pluginName: id, - methodName: 'getIFrameUrl', - method: (iframeUrl) => { - const finalUrl = stringifyUrl({ - url: iframeUrl, - query: { - ...(language - && selectedLanguage - && language !== selectedLanguage && { - src_lang: language, - dest_lang: selectedLanguage, - }), - }, - }); - return finalUrl; - }, - }), - ); - }, [language, selectedLanguage]); - - return ( - <> - - - - - - ); -}; - -TranslationSelection.propTypes = { - id: PropTypes.string.isRequired, - courseId: PropTypes.string.isRequired, - unitId: PropTypes.string.isRequired, - language: PropTypes.string.isRequired, - availableLanguages: PropTypes.arrayOf(PropTypes.shape({ - code: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - })).isRequired, -}; - -TranslationSelection.defaultProps = {}; - -export default TranslationSelection; diff --git a/plugins/UnitTranslationPlugin/translation-selection/index.test.jsx b/plugins/UnitTranslationPlugin/translation-selection/index.test.jsx deleted file mode 100644 index 9c117a5c12..0000000000 --- a/plugins/UnitTranslationPlugin/translation-selection/index.test.jsx +++ /dev/null @@ -1,84 +0,0 @@ -import { sendTrackEvent } from '@edx/frontend-platform/analytics'; -import { shallow } from '@edx/react-unit-test-utils'; - -import TranslationSelection from './index'; -import { initializeTestStore, render } from '../../../src/setupTest'; - -jest.mock('react', () => ({ - ...jest.requireActual('react'), - useContext: jest.fn().mockName('useContext').mockReturnValue({ - authenticatedUser: { - userId: '123', - }, - }), -})); -jest.mock('@openedx/paragon', () => ({ - IconButton: 'IconButton', - Icon: 'Icon', - ProductTour: 'ProductTour', -})); -jest.mock('@openedx/paragon/icons', () => ({ - Language: 'Language', -})); -jest.mock('./useTranslationTour', () => () => ({ - translationTour: { - abitrarily: 'defined', - }, - isOpen: false, - open: jest.fn().mockName('open'), - close: jest.fn().mockName('close'), -})); -jest.mock('react-redux', () => ({ - useDispatch: jest.fn().mockName('useDispatch'), -})); -jest.mock('@src/generic/plugin-store', () => ({ - registerOverrideMethod: jest.fn().mockName('registerOverrideMethod'), -})); -jest.mock('./TranslationModal', () => 'TranslationModal'); -jest.mock('./useSelectLanguage', () => () => ({ - selectedLanguage: 'en', - setSelectedLanguage: jest.fn().mockName('setSelectedLanguage'), -})); -jest.mock('../feedback-widget', () => 'FeedbackWidget'); -jest.mock('@edx/frontend-platform/analytics'); - -describe('', () => { - const props = { - id: 'plugin-test-id', - courseId: 'course-v1:edX+DemoX+Demo_Course', - language: 'en', - availableLanguages: [ - { - code: 'en', - label: 'English', - }, - { - code: 'es', - label: 'Spanish', - }, - ], - unitId: 'unit-test-id', - }; - it('renders', () => { - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); - }); - it('does not send track event when source != target language', async () => { - await initializeTestStore(); - render(); - expect(sendTrackEvent).not.toHaveBeenCalled(); - }); - it('sends track event when source != target language', async () => { - await initializeTestStore(); - render(); - const eventName = 'edx.whole_course_translations.translation_requested'; - const eventProperties = { - courseId: props.courseId, - sourceLanguage: props.language, - targetLanguage: 'en', - unitId: props.unitId, - userId: '123', - }; - expect(sendTrackEvent).not.toHaveBeenCalledWith(eventName, eventProperties); - }); -}); diff --git a/plugins/UnitTranslationPlugin/translation-selection/messages.js b/plugins/UnitTranslationPlugin/translation-selection/messages.js deleted file mode 100644 index 8965da2895..0000000000 --- a/plugins/UnitTranslationPlugin/translation-selection/messages.js +++ /dev/null @@ -1,41 +0,0 @@ -import { defineMessages } from '@edx/frontend-platform/i18n'; - -const messages = defineMessages({ - translationTourModalTitle: { - id: 'translationSelection.translationTourModalTitle', - defaultMessage: 'New translation feature!', - description: 'Title for the translation modal.', - }, - translationTourModalBody: { - id: 'translationSelection.translationTourModalBody', - defaultMessage: 'Now you can easily translate course content.', - description: 'Body for the translation modal.', - }, - tryItButtonText: { - id: 'translationSelection.tryItButtonText', - defaultMessage: 'Try it', - description: 'Button text for the translation modal.', - }, - dismissButtonText: { - id: 'translationSelection.dismissButtonText', - defaultMessage: 'Dismiss', - description: 'Button text for the translation modal.', - }, - languageSelectionModalTitle: { - id: 'translationSelection.languageSelectionModalTitle', - defaultMessage: 'Translate this course', - description: 'Title for the translation modal.', - }, - cancelButtonText: { - id: 'translationSelection.cancelButtonText', - defaultMessage: 'Cancel', - description: 'Button text for the translation modal.', - }, - submitButtonText: { - id: 'translationSelection.submitButtonText', - defaultMessage: 'Submit', - description: 'Button text for the translation modal.', - }, -}); - -export default messages; diff --git a/plugins/UnitTranslationPlugin/translation-selection/useSelectLanguage.js b/plugins/UnitTranslationPlugin/translation-selection/useSelectLanguage.js deleted file mode 100644 index 011bd6454a..0000000000 --- a/plugins/UnitTranslationPlugin/translation-selection/useSelectLanguage.js +++ /dev/null @@ -1,35 +0,0 @@ -import { useCallback } from 'react'; -import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils'; -import { - getLocalStorage, - setLocalStorage, -} from '@src/data/localStorage'; - -export const selectedLanguageKey = 'selectedLanguages'; - -export const stateKeys = StrictDict({ - selectedLanguage: 'selectedLanguage', -}); - -const useSelectLanguage = ({ courseId, language }) => { - const selectedLanguageItem = getLocalStorage(selectedLanguageKey) || {}; - const [selectedLanguage, updateSelectedLanguage] = useKeyedState( - stateKeys.selectedLanguage, - selectedLanguageItem[courseId] || language, - ); - - const setSelectedLanguage = useCallback((newSelectedLanguage) => { - setLocalStorage(selectedLanguageKey, { - ...selectedLanguageItem, - [courseId]: newSelectedLanguage, - }); - updateSelectedLanguage(newSelectedLanguage); - }); - - return { - selectedLanguage, - setSelectedLanguage, - }; -}; - -export default useSelectLanguage; diff --git a/plugins/UnitTranslationPlugin/translation-selection/useSelectLanguage.test.js b/plugins/UnitTranslationPlugin/translation-selection/useSelectLanguage.test.js deleted file mode 100644 index 5dda698fa7..0000000000 --- a/plugins/UnitTranslationPlugin/translation-selection/useSelectLanguage.test.js +++ /dev/null @@ -1,63 +0,0 @@ -import { mockUseKeyedState } from '@edx/react-unit-test-utils'; -import { - getLocalStorage, - setLocalStorage, -} from '@src/data/localStorage'; - -import useSelectLanguage, { - stateKeys, - selectedLanguageKey, -} from './useSelectLanguage'; - -const state = mockUseKeyedState(stateKeys); - -jest.mock('react', () => ({ - ...jest.requireActual('react'), - useCallback: jest.fn((cb, prereqs) => (...args) => [ - cb(...args), - { cb, prereqs }, - ]), -})); -jest.mock('@src/data/localStorage', () => ({ - getLocalStorage: jest.fn(), - setLocalStorage: jest.fn(), -})); - -describe('useSelectLanguage', () => { - const props = { - courseId: 'test-course-id', - language: 'en', - }; - const languages = [ - { code: 'en', label: 'English' }, - { code: 'es', label: 'Spanish' }, - ]; - - beforeEach(() => { - jest.clearAllMocks(); - state.mock(); - }); - afterEach(() => { - state.resetVals(); - }); - - languages.forEach(({ code, label }) => { - it(`initializes selectedLanguage to the selected language (${label})`, () => { - getLocalStorage.mockReturnValueOnce({ [props.courseId]: code }); - const { selectedLanguage } = useSelectLanguage(props); - - state.expectInitializedWith(stateKeys.selectedLanguage, code); - expect(selectedLanguage).toBe(code); - }); - }); - - test('setSelectedLanguage behavior', () => { - const { setSelectedLanguage } = useSelectLanguage(props); - - setSelectedLanguage('es'); - state.expectSetStateCalledWith(stateKeys.selectedLanguage, 'es'); - expect(setLocalStorage).toHaveBeenCalledWith(selectedLanguageKey, { - [props.courseId]: 'es', - }); - }); -}); diff --git a/plugins/UnitTranslationPlugin/translation-selection/useTranslationModal.js b/plugins/UnitTranslationPlugin/translation-selection/useTranslationModal.js deleted file mode 100644 index be5f1939b2..0000000000 --- a/plugins/UnitTranslationPlugin/translation-selection/useTranslationModal.js +++ /dev/null @@ -1,29 +0,0 @@ -import { useCallback } from 'react'; -import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils'; - -export const stateKeys = StrictDict({ - selectedIndex: 'selectedIndex', -}); - -const useTranslationModal = ({ - selectedLanguage, setSelectedLanguage, close, availableLanguages, -}) => { - const [selectedIndex, setSelectedIndex] = useKeyedState( - stateKeys.selectedIndex, - availableLanguages.findIndex((lang) => lang.code === selectedLanguage), - ); - - const onSubmit = useCallback(() => { - const newSelectedLanguage = availableLanguages[selectedIndex].code; - setSelectedLanguage(newSelectedLanguage); - close(); - }, [selectedIndex]); - - return { - selectedIndex, - setSelectedIndex, - onSubmit, - }; -}; - -export default useTranslationModal; diff --git a/plugins/UnitTranslationPlugin/translation-selection/useTranslationModal.test.js b/plugins/UnitTranslationPlugin/translation-selection/useTranslationModal.test.js deleted file mode 100644 index ce1b24905c..0000000000 --- a/plugins/UnitTranslationPlugin/translation-selection/useTranslationModal.test.js +++ /dev/null @@ -1,49 +0,0 @@ -import { mockUseKeyedState } from '@edx/react-unit-test-utils'; - -import useTranslationModal, { stateKeys } from './useTranslationModal'; - -const state = mockUseKeyedState(stateKeys); - -jest.mock('react', () => ({ - ...jest.requireActual('react'), - useCallback: jest.fn((cb, prereqs) => (...args) => ([ - cb(...args), { cb, prereqs }, - ])), -})); - -describe('useTranslationModal', () => { - const props = { - selectedLanguage: 'en', - setSelectedLanguage: jest.fn(), - close: jest.fn(), - availableLanguages: [ - { code: 'en', label: 'English' }, - { code: 'es', label: 'Spanish' }, - ], - }; - - beforeEach(() => { - jest.clearAllMocks(); - state.mock(); - }); - afterEach(() => { - state.resetVals(); - }); - - it('initializes selectedIndex to the index of the selected language', () => { - const { selectedIndex } = useTranslationModal(props); - - state.expectInitializedWith(stateKeys.selectedIndex, 0); - expect(selectedIndex).toBe(0); - }); - - it('onSubmit updates the selected language and closes the modal', () => { - const { onSubmit } = useTranslationModal({ - ...props, - selectedLanguage: 'es', - }); - onSubmit(); - expect(props.setSelectedLanguage).toHaveBeenCalledWith('es'); - expect(props.close).toHaveBeenCalled(); - }); -}); diff --git a/plugins/UnitTranslationPlugin/translation-selection/useTranslationTour.js b/plugins/UnitTranslationPlugin/translation-selection/useTranslationTour.js deleted file mode 100644 index c386a59794..0000000000 --- a/plugins/UnitTranslationPlugin/translation-selection/useTranslationTour.js +++ /dev/null @@ -1,62 +0,0 @@ -import { useCallback } from 'react'; - -import { useIntl } from '@edx/frontend-platform/i18n'; -import { useToggle } from '@openedx/paragon'; -import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils'; - -import messages from './messages'; - -const hasSeenTranslationTourKey = 'hasSeenTranslationTour'; - -export const stateKeys = StrictDict({ - showTranslationTour: 'showTranslationTour', -}); - -const useTranslationTour = () => { - const { formatMessage } = useIntl(); - - const [isTourEnabled, setIsTourEnabled] = useKeyedState( - stateKeys.showTranslationTour, - global.localStorage.getItem(hasSeenTranslationTourKey) !== 'true', - ); - const [isOpen, open, close] = useToggle(false); - - const endTour = useCallback(() => { - global.localStorage.setItem(hasSeenTranslationTourKey, 'true'); - setIsTourEnabled(false); - }, [isTourEnabled, setIsTourEnabled]); - - const tryIt = useCallback(() => { - endTour(); - open(); - }, [endTour, open]); - - const translationTour = isTourEnabled - ? { - tourId: 'translation', - enabled: isTourEnabled, - onDismiss: endTour, - onEnd: tryIt, - checkpoints: [ - { - title: formatMessage(messages.translationTourModalTitle), - body: formatMessage(messages.translationTourModalBody), - placement: 'bottom', - target: '#translation-selection-button', - showDismissButton: true, - endButtonText: formatMessage(messages.tryItButtonText), - dismissButtonText: formatMessage(messages.dismissButtonText), - }, - ], - } - : {}; - - return { - translationTour, - isOpen, - open, - close, - }; -}; - -export default useTranslationTour; diff --git a/plugins/UnitTranslationPlugin/translation-selection/useTranslationTour.test.js b/plugins/UnitTranslationPlugin/translation-selection/useTranslationTour.test.js deleted file mode 100644 index e2708eeeef..0000000000 --- a/plugins/UnitTranslationPlugin/translation-selection/useTranslationTour.test.js +++ /dev/null @@ -1,95 +0,0 @@ -import { mockUseKeyedState } from '@edx/react-unit-test-utils'; -import { useToggle } from '@openedx/paragon'; - -import useTranslationTour, { stateKeys } from './useTranslationTour'; - -jest.mock('react', () => ({ - ...jest.requireActual('react'), - useCallback: jest.fn((cb, prereqs) => () => { - cb(); - return { useCallback: { cb, prereqs } }; - }), -})); -jest.mock('@openedx/paragon', () => ({ - useToggle: jest.fn(), -})); -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', - }; -}); -jest.mock('@src/data/localStorage', () => ({ - getLocalStorage: jest.fn(), - setLocalStorage: jest.fn(), -})); - -const state = mockUseKeyedState(stateKeys); - -describe('useTranslationSelection', () => { - const mockLocalStroage = { - getItem: jest.fn(), - setItem: jest.fn(), - }; - - const toggleOpen = jest.fn(); - const toggleClose = jest.fn(); - - useToggle.mockReturnValue([false, toggleOpen, toggleClose]); - - beforeEach(() => { - jest.clearAllMocks(); - state.mock(); - window.localStorage = mockLocalStroage; - }); - afterEach(() => { - state.resetVals(); - delete window.localStorage; - }); - - it('do not have translation tour if user already seen it', () => { - mockLocalStroage.getItem.mockReturnValueOnce('not seen'); - const { translationTour } = useTranslationTour(); - - expect(translationTour.enabled).toBe(true); - }); - - it('show translation tour if user has not seen it', () => { - mockLocalStroage.getItem.mockReturnValueOnce('true'); - const { translationTour } = useTranslationTour(); - - expect(translationTour).toMatchObject({}); - }); - test('open and close as pass from useToggle', () => { - const { isOpen, open, close } = useTranslationTour(); - expect(isOpen).toBe(false); - expect(toggleOpen).toBe(open); - expect(toggleClose).toBe(close); - }); - test('end tour on dismiss button click', () => { - mockLocalStroage.getItem.mockReturnValueOnce('not seen'); - const { translationTour } = useTranslationTour(); - translationTour.onDismiss(); - expect(mockLocalStroage.setItem).toHaveBeenCalledWith( - 'hasSeenTranslationTour', - 'true', - ); - state.expectSetStateCalledWith(stateKeys.showTranslationTour, false); - }); - test('end tour and open modal on try it button click', () => { - mockLocalStroage.getItem.mockReturnValueOnce('not seen'); - const { translationTour } = useTranslationTour(); - translationTour.onEnd(); - state.expectSetStateCalledWith(stateKeys.showTranslationTour, false); - expect(toggleOpen).toHaveBeenCalled(); - }); -}); diff --git a/src/courseware/course/sequence/Sequence.jsx b/src/courseware/course/sequence/Sequence.jsx index 2ecce64931..f6109c34fe 100644 --- a/src/courseware/course/sequence/Sequence.jsx +++ b/src/courseware/course/sequence/Sequence.jsx @@ -14,6 +14,7 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { useSelector } from 'react-redux'; import SequenceExamWrapper from '@edx/frontend-lib-special-exams'; import { breakpoints, useWindowSize } from '@openedx/paragon'; +import { PluginSlot } from '@openedx/frontend-plugin-framework'; import PageLoading from '../../../generic/PageLoading'; import { useModel } from '../../../generic/model-store'; @@ -200,7 +201,13 @@ const Sequence = ({ {enableNewSidebar === 'true' ? : } -
+ ); diff --git a/src/index.jsx b/src/index.jsx index 99c2bd5ee2..7892399e39 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -150,7 +150,6 @@ initialize({ handlers: { config: () => { mergeConfig({ - AI_TRANSLATIONS_URL: process.env.AI_TRANSLATIONS_URL || null, CONTACT_URL: process.env.CONTACT_URL || null, CREDENTIALS_BASE_URL: process.env.CREDENTIALS_BASE_URL || null, CREDIT_HELP_LINK_URL: process.env.CREDIT_HELP_LINK_URL || null, diff --git a/webpack.dev.config.js b/webpack.dev.config.js index 08e5a12fbe..ddf63def17 100644 --- a/webpack.dev.config.js +++ b/webpack.dev.config.js @@ -6,7 +6,6 @@ const config = createConfig('webpack-dev'); config.resolve.alias = { ...config.resolve.alias, '@src': path.resolve(__dirname, 'src'), - '@plugins': path.resolve(__dirname, 'plugins'), }; module.exports = config; diff --git a/webpack.prod.config.js b/webpack.prod.config.js index 15b52d0b43..fbe410772d 100644 --- a/webpack.prod.config.js +++ b/webpack.prod.config.js @@ -18,7 +18,6 @@ config.plugins.push( config.resolve.alias = { ...config.resolve.alias, '@src': path.resolve(__dirname, 'src'), - '@plugins': path.resolve(__dirname, 'plugins'), }; module.exports = config;