Skip to content

Commit b065ec0

Browse files
committed
feat: enable markdown to olx conversion
1 parent 8fe5fb6 commit b065ec0

11 files changed

Lines changed: 232 additions & 59 deletions

File tree

src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.jsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import Randomization from './settingsComponents/Randomization';
2626
// This widget should be connected, grab all settings from store, update them as needed.
2727
const SettingsWidget = ({
2828
problemType,
29+
editorRef,
2930
// redux
3031
answers,
3132
groupFeedbackList,
@@ -45,6 +46,7 @@ const SettingsWidget = ({
4546
isMarkdownEditorEnabledForContext,
4647
} = useEditorContext();
4748
const rawMarkdown = useSelector(selectors.problem.rawMarkdown);
49+
const isMarkdownEditorEnabled = useSelector(selectors.problem.isMarkdownEditorEnabled);
4850
const showMarkdownEditorButton = isMarkdownEditorEnabledForContext && rawMarkdown;
4951
const { isAdvancedCardsVisible, showAdvancedCards } = showAdvancedSettingsCards();
5052
const feedbackCard = () => {
@@ -159,12 +161,12 @@ const SettingsWidget = ({
159161
</div>
160162
)}
161163
<div className="my-3">
162-
<SwitchEditorCard problemType={problemType} editorType="advanced" />
164+
<SwitchEditorCard problemType={problemType} editorType="advanced" editorRef={editorRef} />
163165
</div>
164-
{ showMarkdownEditorButton
166+
{ (showMarkdownEditorButton && !isMarkdownEditorEnabled) // Only show button if not already in markdown editor
165167
&& (
166168
<div className="my-3">
167-
<SwitchEditorCard problemType={problemType} editorType="markdown" />
169+
<SwitchEditorCard problemType={problemType} editorType="markdown" editorRef={editorRef} />
168170
</div>
169171
)}
170172
</Collapsible.Body>

src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.test.tsx

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import React from 'react';
22

33
import { ProblemTypeKeys } from '@src/editors/data/constants/problem';
44
import { screen, initializeMocks } from '@src/testUtils';
5-
import { editorRender } from '@src/editors/editorTestRender';
5+
import { editorRender, type PartialEditorState } from '@src/editors/editorTestRender';
66
import { mockWaffleFlags } from '@src/data/apiHooks.mock';
77
import * as hooks from './hooks';
88
import { SettingsWidgetInternal as SettingsWidget } from '.';
9+
import { initialState } from '@src/course-outline/__mocks__/courseOutlineIndex';
910

1011
jest.mock('./settingsComponents/GeneralFeedback', () => 'GeneralFeedback');
1112
jest.mock('./settingsComponents/GroupFeedback', () => 'GroupFeedback');
@@ -17,17 +18,19 @@ jest.mock('./settingsComponents/ShowAnswerCard', () => 'ShowAnswerCard');
1718
jest.mock('./settingsComponents/SwitchEditorCard', () => 'SwitchEditorCard');
1819
jest.mock('./settingsComponents/TimerCard', () => 'TimerCard');
1920
jest.mock('./settingsComponents/TypeCard', () => 'TypeCard');
21+
// NOTE: Do NOT mock useSelector here. We rely on the real hook so that
22+
// editorRender's provided initialState flows into the component under test.
2023
mockWaffleFlags();
2124

2225
describe('SettingsWidget', () => {
2326
const showAdvancedSettingsCardsBaseProps = {
2427
isAdvancedCardsVisible: false,
2528
showAdvancedCards: jest.fn().mockName('showAdvancedSettingsCards.showAdvancedCards'),
26-
setResetTrue: jest.fn().mockName('showAdvancedSettingsCards.setResetTrue'),
2729
};
2830

2931
const props = {
3032
problemType: ProblemTypeKeys.TEXTINPUT,
33+
editorRef: { current: null},
3134
settings: {},
3235
defaultSettings: {
3336
maxAttempts: 2,
@@ -92,6 +95,45 @@ describe('SettingsWidget', () => {
9295
expect(container.querySelector('randomization')).toBeInTheDocument();
9396
});
9497
});
98+
describe('SwitchEditorCard rendering (markdown vs advanced)', () => {
99+
test('shows two SwitchEditorCard components when markdown is available and not currently enabled', () => {
100+
const showAdvancedSettingsCardsProps = {
101+
...showAdvancedSettingsCardsBaseProps,
102+
isAdvancedCardsVisible: true,
103+
};
104+
jest.spyOn(hooks, 'showAdvancedSettingsCards').mockReturnValue(showAdvancedSettingsCardsProps);
105+
const modifiedInitialState: PartialEditorState = {
106+
problem: {
107+
problemType: null, // non-advanced problem
108+
isMarkdownEditorEnabled: false, // currently in advanced/raw (or standard) editor
109+
rawOLX: '<problem></problem>',
110+
rawMarkdown: '## Problem', // markdown content exists so button should appear
111+
isDirty: false,
112+
},
113+
};
114+
const { container } = editorRender(<SettingsWidget {...props} />, { initialState: modifiedInitialState });
115+
expect(container.querySelectorAll('switcheditorcard')).toHaveLength(2);
116+
});
117+
118+
test('shows only the advanced SwitchEditorCard when already in markdown mode', () => {
119+
const showAdvancedSettingsCardsProps = {
120+
...showAdvancedSettingsCardsBaseProps,
121+
isAdvancedCardsVisible: true,
122+
};
123+
jest.spyOn(hooks, 'showAdvancedSettingsCards').mockReturnValue(showAdvancedSettingsCardsProps);
124+
const modifiedInitialState: PartialEditorState = {
125+
problem: {
126+
problemType: null,
127+
isMarkdownEditorEnabled: true, // already in markdown editor, so markdown button hidden
128+
rawOLX: '<problem></problem>',
129+
rawMarkdown: '## Problem',
130+
isDirty: false,
131+
},
132+
};
133+
const { container } = editorRender(<SettingsWidget {...props} />, { initialState: modifiedInitialState });
134+
expect(container.querySelectorAll('switcheditorcard')).toHaveLength(1);
135+
});
136+
});
95137

96138
describe('isLibrary', () => {
97139
const libraryProps = {

src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/SwitchEditorCard.jsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import React from 'react';
2-
import { useDispatch, useSelector } from 'react-redux';
2+
import { useDispatch } from 'react-redux';
33
import { FormattedMessage } from '@edx/frontend-platform/i18n';
44
import { Card } from '@openedx/paragon';
55
import PropTypes from 'prop-types';
6-
import { useEditorContext } from '@src/editors/EditorContext';
7-
import { selectors, thunkActions } from '@src/editors/data/redux';
6+
import { thunkActions } from '@src/editors/data/redux';
87
import BaseModal from '@src/editors/sharedComponents/BaseModal';
98
import Button from '@src/editors/sharedComponents/Button';
109
import { ProblemTypeKeys } from '@src/editors/data/constants/problem';
@@ -14,14 +13,11 @@ import { handleConfirmEditorSwitch } from '../hooks';
1413
const SwitchEditorCard = ({
1514
editorType,
1615
problemType,
16+
editorRef,
1717
}) => {
1818
const [isConfirmOpen, setConfirmOpen] = React.useState(false);
19-
const { isMarkdownEditorEnabledForContext } = useEditorContext();
20-
const isMarkdownEditorEnabled = useSelector(selectors.problem.isMarkdownEditorEnabled);
2119
const dispatch = useDispatch();
22-
23-
const isMarkdownEditorActive = isMarkdownEditorEnabled && isMarkdownEditorEnabledForContext;
24-
if (isMarkdownEditorActive || problemType === ProblemTypeKeys.ADVANCED) { return null; }
20+
if (problemType === ProblemTypeKeys.ADVANCED) { return null; }
2521

2622
return (
2723
<Card className="border border-light-700 shadow-none">
@@ -33,7 +29,7 @@ const SwitchEditorCard = ({
3329
<Button
3430
/* istanbul ignore next */
3531
onClick={() => handleConfirmEditorSwitch({
36-
switchEditor: () => dispatch(thunkActions.problem.switchEditor(editorType)),
32+
switchEditor: () => dispatch(thunkActions.problem.switchEditor(editorType, editorRef)),
3733
setConfirmOpen,
3834
})}
3935
variant="primary"

src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/SwitchEditorCard.test.tsx

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ describe('SwitchEditorCard - markdown', () => {
1212
const baseProps = {
1313
problemType: 'stringresponse',
1414
editorType: 'markdown',
15+
editorRef: { current: null },
1516
};
1617

1718
beforeEach(() => {
@@ -49,7 +50,7 @@ describe('SwitchEditorCard - markdown', () => {
4950
expect(confirmButton).toBeInTheDocument();
5051
expect(switchEditorSpy).not.toHaveBeenCalled();
5152
await user.click(confirmButton);
52-
expect(switchEditorSpy).toHaveBeenCalledWith('markdown');
53+
expect(switchEditorSpy).toHaveBeenCalledWith('markdown', { current: null });
5354
// Markdown editor would now be active.
5455
});
5556

@@ -59,19 +60,4 @@ describe('SwitchEditorCard - markdown', () => {
5960
expect(reduxWrapper?.innerHTML).toBe('');
6061
});
6162

62-
test('returns null when editor is already Markdown', () => {
63-
// Markdown Editor support is on for this course:
64-
mockWaffleFlags({ useReactMarkdownEditor: true });
65-
// The markdown editor *IS* currently active (default)
66-
67-
const { container } = editorRender(<SwitchEditorCard {...baseProps} />, {
68-
initialState: {
69-
problem: {
70-
isMarkdownEditorEnabled: true,
71-
},
72-
},
73-
});
74-
const reduxWrapper = (container.firstChild as HTMLElement | null);
75-
expect(reduxWrapper?.innerHTML).toBe('');
76-
});
7763
});

src/editors/containers/ProblemEditor/components/EditProblemView/hooks.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,10 @@ export const parseState = ({
9090
return {
9191
settings: {
9292
...settings,
93-
...(isMarkdownEditorEnabled && { markdown: contentString }),
93+
// If the save action isn’t triggered from the Markdown editor, the Markdown content might be outdated. Since the
94+
// Markdown editor shouldn't be displayed in future in this case, we’re sending `null` instead.
95+
// TODO: Implement OLX-to-Markdown conversion to properly handle this scenario.
96+
markdown: isMarkdownEditorEnabled ? contentString : null,
9497
markdown_edited: isMarkdownEditorEnabled,
9598
},
9699
olx: isAdvanced || isMarkdownEditorEnabled ? rawOLX : reactBuiltOlx,

src/editors/containers/ProblemEditor/components/EditProblemView/hooks.test.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ describe('EditProblemView hooks parseState', () => {
165165
assets: {},
166166
})();
167167
expect(res.olx).toBe(mockRawOLX);
168+
expect(res.settings.markdown).toBe(null);
168169
});
169170
it('markdown problem', () => {
170171
const res = hooks.parseState({
@@ -306,13 +307,16 @@ describe('EditProblemView hooks parseState', () => {
306307
show_reset_button: false,
307308
submission_wait_seconds: 0,
308309
attempts_before_showanswer_button: 0,
310+
markdown: null,
311+
markdown_edited: false,
309312
};
310313
const openSaveWarningModal = jest.fn();
311314

312315
it('default visual save and returns parseState data', () => {
313316
const problem = { ...problemState, problemType: ProblemTypeKeys.NUMERIC, answers: [{ id: 'A', title: 'problem', correct: true }] };
314317
const content = hooks.getContent({
315318
isAdvancedProblemType: false,
319+
isMarkdownEditorEnabled: false,
316320
problemState: problem,
317321
editorRef,
318322
assets,
@@ -339,6 +343,7 @@ describe('EditProblemView hooks parseState', () => {
339343
};
340344
const { settings } = hooks.getContent({
341345
isAdvancedProblemType: false,
346+
isMarkdownEditorEnabled: false,
342347
problemState: problem,
343348
editorRef,
344349
assets,
@@ -353,12 +358,15 @@ describe('EditProblemView hooks parseState', () => {
353358
attempts_before_showanswer_button: 0,
354359
submission_wait_seconds: 0,
355360
weight: 1,
361+
markdown: null,
362+
markdown_edited: false,
356363
});
357364
});
358365

359366
it('default advanced save and returns parseState data', () => {
360367
const content = hooks.getContent({
361368
isAdvancedProblemType: true,
369+
isMarkdownEditorEnabled: false,
362370
problemState,
363371
editorRef,
364372
assets,

src/editors/containers/ProblemEditor/components/EditProblemView/index.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ const EditProblemView = ({ returnFunction }) => {
133133
)}
134134

135135
<span className="editProblemView-settingsColumn">
136-
<SettingsWidget problemType={problemType} />
136+
<SettingsWidget problemType={problemType} editorRef={editorRef} />
137137
</span>
138138
</div>
139139
</EditorContainer>

src/editors/data/redux/thunkActions/problem.test.ts

Lines changed: 73 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -45,31 +45,86 @@ describe('problem thunkActions', () => {
4545
let dispatch;
4646
let getState;
4747
let dispatchedAction;
48+
let mockEditorRef;
49+
50+
const mockProblemState = (isMarkdownEditorEnabled) => ({
51+
problem: {
52+
isMarkdownEditorEnabled,
53+
rawOLX: 'PREVIOUS_OLX',
54+
},
55+
app: {
56+
learningContextId: 'course-v1:org+course+run',
57+
blockValue,
58+
},
59+
});
60+
61+
const createMockEditorRef = (content = 'MockMarkdownContent') => ({
62+
current: {
63+
state: {
64+
doc: { toString: jest.fn(() => content) },
65+
},
66+
},
67+
});
68+
4869
beforeEach(() => {
4970
dispatch = jest.fn((action) => ({ dispatch: action }));
50-
getState = jest.fn(() => ({
51-
problem: {
52-
},
53-
app: {
54-
learningContextId: 'course-v1:org+course+run',
55-
blockValue,
56-
},
57-
}));
71+
mockEditorRef = createMockEditorRef();
5872
});
5973

6074
afterEach(() => {
6175
jest.restoreAllMocks();
6276
});
63-
test('initializeProblem visual Problem :', () => {
64-
initializeProblem(blockValue)(dispatch, getState);
65-
expect(dispatch).toHaveBeenCalled();
77+
78+
describe('when markdown editor is enabled', () => {
79+
beforeEach(() => {
80+
getState = jest.fn(() => mockProblemState(true));
81+
});
82+
83+
test('initializeProblem triggers dispatch', () => {
84+
initializeProblem(blockValue)(dispatch, getState);
85+
expect(dispatch).toHaveBeenCalled();
86+
});
87+
88+
test('switchToAdvancedEditor converts markdown to OLX', () => {
89+
switchToAdvancedEditor(mockEditorRef)(dispatch, getState);
90+
expect(dispatch).toHaveBeenCalledWith(
91+
actions.problem.updateField({
92+
problemType: ProblemTypeKeys.ADVANCED,
93+
rawOLX: `<problem>\n<p>MockMarkdownContent</p>\n</problem>`,
94+
isMarkdownEditorEnabled: false,
95+
}),
96+
);
97+
});
98+
99+
test('switchToAdvancedEditor falls back to previous OLX if editorRef missing', () => {
100+
switchToAdvancedEditor(null)(dispatch, getState);
101+
expect(dispatch).toHaveBeenCalledWith(
102+
actions.problem.updateField({
103+
problemType: ProblemTypeKeys.ADVANCED,
104+
rawOLX: 'PREVIOUS_OLX',
105+
isMarkdownEditorEnabled: false,
106+
}),
107+
);
108+
});
66109
});
67-
test('switchToAdvancedEditor visual Problem', () => {
68-
switchToAdvancedEditor()(dispatch, getState);
69-
expect(dispatch).toHaveBeenCalledWith(
70-
actions.problem.updateField({ problemType: ProblemTypeKeys.ADVANCED, rawOLX: mockOlx }),
71-
);
110+
111+
describe('when markdown editor is disabled', () => {
112+
beforeEach(() => {
113+
getState = jest.fn(() => mockProblemState(false));
114+
});
115+
116+
test('switchToAdvancedEditor uses ReactStateOLXParser', () => {
117+
switchToAdvancedEditor(mockEditorRef)(dispatch, getState);
118+
expect(dispatch).toHaveBeenCalledWith(
119+
actions.problem.updateField({
120+
problemType: ProblemTypeKeys.ADVANCED,
121+
rawOLX: mockOlx,
122+
isMarkdownEditorEnabled: false,
123+
}),
124+
);
125+
});
72126
});
127+
73128
test('switchToMarkdownEditor dispatches correct actions', () => {
74129
switchToMarkdownEditor()(dispatch);
75130

@@ -94,12 +149,12 @@ describe('problem thunkActions', () => {
94149
});
95150

96151
test('dispatches switchToAdvancedEditor when editorType is advanced', () => {
97-
switchEditor('advanced')(dispatch, getState);
152+
switchEditor('advanced', mockEditorRef)(dispatch, getState);
98153
expect(switchToAdvancedEditorMock).toHaveBeenCalledWith(dispatch, getState);
99154
});
100155

101156
test('dispatches switchToMarkdownEditor when editorType is markdown', () => {
102-
switchEditor('markdown')(dispatch, getState);
157+
switchEditor('markdown', mockEditorRef)(dispatch, getState);
103158
expect(switchToMarkdownEditorMock).toHaveBeenCalledWith(dispatch);
104159
});
105160
});

0 commit comments

Comments
 (0)