From b065ec07f5aca557ce44472ec09e269c3e701296 Mon Sep 17 00:00:00 2001 From: Muhammad Anas Date: Tue, 7 Oct 2025 19:11:00 +0500 Subject: [PATCH 1/6] feat: enable markdown to olx conversion --- .../EditProblemView/SettingsWidget/index.jsx | 8 +- .../SettingsWidget/index.test.tsx | 46 +++++++++- .../settingsComponents/SwitchEditorCard.jsx | 14 +-- .../SwitchEditorCard.test.tsx | 18 +--- .../components/EditProblemView/hooks.js | 5 +- .../components/EditProblemView/hooks.test.js | 8 ++ .../components/EditProblemView/index.jsx | 2 +- .../data/redux/thunkActions/problem.test.ts | 91 +++++++++++++++---- .../data/redux/thunkActions/problem.ts | 34 +++++-- .../sharedComponents/CodeEditor/index.jsx | 17 +++- .../CodeEditor/index.test.tsx | 48 +++++++++- 11 files changed, 232 insertions(+), 59 deletions(-) diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.jsx index 1ebabe0936..1b9c7cf3d5 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.jsx @@ -26,6 +26,7 @@ import Randomization from './settingsComponents/Randomization'; // This widget should be connected, grab all settings from store, update them as needed. const SettingsWidget = ({ problemType, + editorRef, // redux answers, groupFeedbackList, @@ -45,6 +46,7 @@ const SettingsWidget = ({ isMarkdownEditorEnabledForContext, } = useEditorContext(); const rawMarkdown = useSelector(selectors.problem.rawMarkdown); + const isMarkdownEditorEnabled = useSelector(selectors.problem.isMarkdownEditorEnabled); const showMarkdownEditorButton = isMarkdownEditorEnabledForContext && rawMarkdown; const { isAdvancedCardsVisible, showAdvancedCards } = showAdvancedSettingsCards(); const feedbackCard = () => { @@ -159,12 +161,12 @@ const SettingsWidget = ({ )}
- +
- { showMarkdownEditorButton + { (showMarkdownEditorButton && !isMarkdownEditorEnabled) // Only show button if not already in markdown editor && (
- +
)} diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.test.tsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.test.tsx index 89ded5dd7a..f4d5a0e6d8 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.test.tsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.test.tsx @@ -2,10 +2,11 @@ import React from 'react'; import { ProblemTypeKeys } from '@src/editors/data/constants/problem'; import { screen, initializeMocks } from '@src/testUtils'; -import { editorRender } from '@src/editors/editorTestRender'; +import { editorRender, type PartialEditorState } from '@src/editors/editorTestRender'; import { mockWaffleFlags } from '@src/data/apiHooks.mock'; import * as hooks from './hooks'; import { SettingsWidgetInternal as SettingsWidget } from '.'; +import { initialState } from '@src/course-outline/__mocks__/courseOutlineIndex'; jest.mock('./settingsComponents/GeneralFeedback', () => 'GeneralFeedback'); jest.mock('./settingsComponents/GroupFeedback', () => 'GroupFeedback'); @@ -17,17 +18,19 @@ jest.mock('./settingsComponents/ShowAnswerCard', () => 'ShowAnswerCard'); jest.mock('./settingsComponents/SwitchEditorCard', () => 'SwitchEditorCard'); jest.mock('./settingsComponents/TimerCard', () => 'TimerCard'); jest.mock('./settingsComponents/TypeCard', () => 'TypeCard'); +// NOTE: Do NOT mock useSelector here. We rely on the real hook so that +// editorRender's provided initialState flows into the component under test. mockWaffleFlags(); describe('SettingsWidget', () => { const showAdvancedSettingsCardsBaseProps = { isAdvancedCardsVisible: false, showAdvancedCards: jest.fn().mockName('showAdvancedSettingsCards.showAdvancedCards'), - setResetTrue: jest.fn().mockName('showAdvancedSettingsCards.setResetTrue'), }; const props = { problemType: ProblemTypeKeys.TEXTINPUT, + editorRef: { current: null}, settings: {}, defaultSettings: { maxAttempts: 2, @@ -92,6 +95,45 @@ describe('SettingsWidget', () => { expect(container.querySelector('randomization')).toBeInTheDocument(); }); }); + describe('SwitchEditorCard rendering (markdown vs advanced)', () => { + test('shows two SwitchEditorCard components when markdown is available and not currently enabled', () => { + const showAdvancedSettingsCardsProps = { + ...showAdvancedSettingsCardsBaseProps, + isAdvancedCardsVisible: true, + }; + jest.spyOn(hooks, 'showAdvancedSettingsCards').mockReturnValue(showAdvancedSettingsCardsProps); + const modifiedInitialState: PartialEditorState = { + problem: { + problemType: null, // non-advanced problem + isMarkdownEditorEnabled: false, // currently in advanced/raw (or standard) editor + rawOLX: '', + rawMarkdown: '## Problem', // markdown content exists so button should appear + isDirty: false, + }, + }; + const { container } = editorRender(, { initialState: modifiedInitialState }); + expect(container.querySelectorAll('switcheditorcard')).toHaveLength(2); + }); + + test('shows only the advanced SwitchEditorCard when already in markdown mode', () => { + const showAdvancedSettingsCardsProps = { + ...showAdvancedSettingsCardsBaseProps, + isAdvancedCardsVisible: true, + }; + jest.spyOn(hooks, 'showAdvancedSettingsCards').mockReturnValue(showAdvancedSettingsCardsProps); + const modifiedInitialState: PartialEditorState = { + problem: { + problemType: null, + isMarkdownEditorEnabled: true, // already in markdown editor, so markdown button hidden + rawOLX: '', + rawMarkdown: '## Problem', + isDirty: false, + }, + }; + const { container } = editorRender(, { initialState: modifiedInitialState }); + expect(container.querySelectorAll('switcheditorcard')).toHaveLength(1); + }); + }); describe('isLibrary', () => { const libraryProps = { diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/SwitchEditorCard.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/SwitchEditorCard.jsx index 1205b8395a..3d68cc1529 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/SwitchEditorCard.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/SwitchEditorCard.jsx @@ -1,10 +1,9 @@ import React from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { Card } from '@openedx/paragon'; import PropTypes from 'prop-types'; -import { useEditorContext } from '@src/editors/EditorContext'; -import { selectors, thunkActions } from '@src/editors/data/redux'; +import { thunkActions } from '@src/editors/data/redux'; import BaseModal from '@src/editors/sharedComponents/BaseModal'; import Button from '@src/editors/sharedComponents/Button'; import { ProblemTypeKeys } from '@src/editors/data/constants/problem'; @@ -14,14 +13,11 @@ import { handleConfirmEditorSwitch } from '../hooks'; const SwitchEditorCard = ({ editorType, problemType, + editorRef, }) => { const [isConfirmOpen, setConfirmOpen] = React.useState(false); - const { isMarkdownEditorEnabledForContext } = useEditorContext(); - const isMarkdownEditorEnabled = useSelector(selectors.problem.isMarkdownEditorEnabled); const dispatch = useDispatch(); - - const isMarkdownEditorActive = isMarkdownEditorEnabled && isMarkdownEditorEnabledForContext; - if (isMarkdownEditorActive || problemType === ProblemTypeKeys.ADVANCED) { return null; } + if (problemType === ProblemTypeKeys.ADVANCED) { return null; } return ( @@ -33,7 +29,7 @@ const SwitchEditorCard = ({ - - + + + + )} - > - {isAdvancedProblemType ? ( - - ) : ( - <> -
- -
-
- -
- - )} -
- -
- {isAdvancedProblemType || isMarkdownEditorEnabled ? ( - - - - ) : ( - - - - + > + {isAdvancedProblemType ? ( + + ) : ( + <> +
+ +
+
+ +
+ + )} + + +
+ {isAdvancedProblemType || isMarkdownEditorEnabled ? ( + + + + ) : ( + + + + + + )} + + + - )} - - - - -
+
); From 85011a8c2042b3b03ccccb3bac27748eb84da8b0 Mon Sep 17 00:00:00 2001 From: Muhammad Anas Date: Mon, 20 Oct 2025 15:36:37 +0500 Subject: [PATCH 6/6] fix: update the editor conversion messages and convert context file to tsx --- .../EditProblemView/ProblemEditorContext.jsx | 30 ------------------ .../EditProblemView/ProblemEditorContext.tsx | 31 +++++++++++++++++++ .../SettingsWidget/messages.js | 6 ++-- 3 files changed, 34 insertions(+), 33 deletions(-) delete mode 100644 src/editors/containers/ProblemEditor/components/EditProblemView/ProblemEditorContext.jsx create mode 100644 src/editors/containers/ProblemEditor/components/EditProblemView/ProblemEditorContext.tsx diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/ProblemEditorContext.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/ProblemEditorContext.jsx deleted file mode 100644 index 2fbd620370..0000000000 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/ProblemEditorContext.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import React, { createContext, useContext, useMemo } from 'react'; -import PropTypes from 'prop-types'; - -const ProblemEditorContext = createContext({ - editorRef: null, -}); - -export const ProblemEditorContextProvider = ({ editorRef, children }) => { - const contextValue = useMemo(() => ({ editorRef }), [editorRef]); - - return ( - - {children} - - ); -}; - -ProblemEditorContextProvider.propTypes = { - children: PropTypes.node.isRequired, - // eslint-disable-next-line react/forbid-prop-types - editorRef: PropTypes.object, -}; - -ProblemEditorContextProvider.defaultProps = { - editorRef: null, -}; - -export const useProblemEditorContext = () => useContext(ProblemEditorContext); - -export default ProblemEditorContext; diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/ProblemEditorContext.tsx b/src/editors/containers/ProblemEditor/components/EditProblemView/ProblemEditorContext.tsx new file mode 100644 index 0000000000..d05ad805e6 --- /dev/null +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/ProblemEditorContext.tsx @@ -0,0 +1,31 @@ +import React from 'react'; + +type ProblemEditorRef = React.MutableRefObject | React.RefObject | null; + +export interface ProblemEditorContextValue { + editorRef: ProblemEditorRef; +} + +export type ProblemEditorContextInit = { + editorRef?: ProblemEditorRef; +}; + +const context = React.createContext(undefined); + +export function useProblemEditorContext() { + const ctx = React.useContext(context); + if (ctx === undefined) { + /* istanbul ignore next */ + throw new Error('This component needs to be wrapped in '); + } + return ctx; +} + +export const ProblemEditorContextProvider: React.FC<{ children: React.ReactNode; } & ProblemEditorContextInit> = ({ + children, + editorRef = null, +}) => { + const ctx: ProblemEditorContextValue = React.useMemo(() => ({ editorRef }), [editorRef]); + + return {children}; +}; diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/messages.js b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/messages.js index 78d8cd5652..e915032539 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/messages.js +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/messages.js @@ -169,13 +169,13 @@ const messages = defineMessages({ }, 'ConfirmSwitchMessage-advanced': { id: 'authoring.problemeditor.settings.switchtoeditor.ConfirmSwitchMessage.advanced', - defaultMessage: 'If you use the advanced editor, this problem will be converted to OLX and you will not be able to return to the simple editor.', + defaultMessage: 'If you use the advanced editor, this problem will be converted to OLX. Depending on what edits you make to the OLX, you may not be able to return to the simple editor.', description: 'message to confirm that a user wants to use the advanced editor', }, 'ConfirmSwitchMessage-markdown': { id: 'authoring.problemeditor.settings.switchtoeditor.ConfirmSwitchMessage.markdown', - defaultMessage: 'If you use the markdown editor, this problem will be converted to markdown and you will not be able to return to the simple editor.', - description: 'message to confirm that a user wants to use the advanced editor', + defaultMessage: 'Some edits that are possible with the markdown editor are not supported by the simple editor, so you may not be able to change back to the simple editor.', + description: 'message to confirm that a user wants to use the markdown editor', }, 'ConfirmSwitchMessageTitle-advanced': { id: 'authoring.problemeditor.settings.switchtoeditor.ConfirmSwitchMessageTitle.advanced',