Skip to content

Commit c90195e

Browse files
refactor: move 'isMarkdownEditorEnabledForContext' into EditorContext (#2398)
This just moves one single state variable, `isMarkdownEditorEnabledForCourse` out of the Redux state and into the `EditorContext`.
1 parent dfd3b93 commit c90195e

File tree

15 files changed

+120
-91
lines changed

15 files changed

+120
-91
lines changed

src/editors/Editor.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import { useDispatch } from 'react-redux';
55

66
import * as hooks from './hooks';
77

8-
import { useWaffleFlags } from '../data/apiHooks';
9-
import { isCourseKey } from '../generic/key-utils';
108
import supportedEditors from './supportedEditors';
119
import type { EditorComponent } from './EditorComponent';
1210
import AdvancedEditor from './AdvancedEditor';
@@ -28,15 +26,12 @@ const Editor: React.FC<Props> = ({
2826
onClose = null,
2927
returnFunction = null,
3028
}) => {
31-
const courseIdIfCourse = isCourseKey(learningContextId) ? learningContextId : undefined;
32-
const isMarkdownEditorEnabledForCourse = useWaffleFlags(courseIdIfCourse).useReactMarkdownEditor;
3329
const dispatch = useDispatch();
3430
const loading = hooks.useInitializeApp({
3531
dispatch,
3632
data: {
3733
blockId,
3834
blockType,
39-
isMarkdownEditorEnabledForCourse,
4035
learningContextId,
4136
lmsEndpointUrl,
4237
studioEndpointUrl,

src/editors/EditorContext.tsx

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { useWaffleFlags } from '@src/data/apiHooks';
2+
import { isCourseKey } from '@src/generic/key-utils';
13
import React from 'react';
24

35
/**
@@ -6,9 +8,19 @@ import React from 'react';
68
* Note: we're in the process of moving things from redux into this.
79
*/
810
export interface EditorContext {
11+
/**
12+
* The ID of the current course or library.
13+
* Use `isCourseKey()` from '@src/generic/key-utils' if you need to check what type it is.
14+
*/
915
learningContextId: string;
16+
/** Is the so-called "Markdown" problem editor available in this learning context? */
17+
isMarkdownEditorEnabledForContext: boolean;
1018
}
1119

20+
export type EditorContextInit = {
21+
learningContextId: string;
22+
};
23+
1224
const context = React.createContext<EditorContext | undefined>(undefined);
1325

1426
/** Hook to get the editor context (shared context) */
@@ -21,10 +33,20 @@ export function useEditorContext() {
2133
return ctx;
2234
}
2335

24-
export const EditorContextProvider: React.FC<{
25-
children: React.ReactNode,
26-
learningContextId: string;
27-
}> = ({ children, ...contextData }) => {
28-
const ctx: EditorContext = React.useMemo(() => ({ ...contextData }), []);
36+
export const EditorContextProvider: React.FC<{ children: React.ReactNode; } & EditorContextInit> = ({
37+
children,
38+
learningContextId,
39+
}) => {
40+
const courseIdIfCourse = isCourseKey(learningContextId) ? learningContextId : undefined;
41+
const isMarkdownEditorEnabledForContext = useWaffleFlags(courseIdIfCourse).useReactMarkdownEditor;
42+
43+
const ctx: EditorContext = React.useMemo(() => ({
44+
learningContextId,
45+
isMarkdownEditorEnabledForContext,
46+
}), [
47+
// Dependencies - make sure we update the context object if any of these values change:
48+
learningContextId,
49+
isMarkdownEditorEnabledForContext,
50+
]);
2951
return <context.Provider value={ctx}>{children}</context.Provider>;
3052
};

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
33
import { FormattedMessage } from '@edx/frontend-platform/i18n';
4-
import { connect } from 'react-redux';
4+
import { connect, useSelector } from 'react-redux';
55
import {
66
Button, Collapsible,
77
} from '@openedx/paragon';
8-
import { selectors, actions } from '../../../../../data/redux';
8+
import { useEditorContext } from '@src/editors/EditorContext';
9+
import { selectors, actions } from '@src/editors/data/redux';
910
import ScoringCard from './settingsComponents/ScoringCard';
1011
import ShowAnswerCard from './settingsComponents/ShowAnswerCard';
1112
import HintsCard from './settingsComponents/HintsCard';
@@ -38,9 +39,13 @@ const SettingsWidget = ({
3839
defaultSettings,
3940
images,
4041
isLibrary,
41-
learningContextId,
42-
showMarkdownEditorButton,
4342
}) => {
43+
const {
44+
learningContextId,
45+
isMarkdownEditorEnabledForContext,
46+
} = useEditorContext();
47+
const rawMarkdown = useSelector(selectors.problem.rawMarkdown);
48+
const showMarkdownEditorButton = isMarkdownEditorEnabledForContext && rawMarkdown;
4449
const { isAdvancedCardsVisible, showAdvancedCards } = showAdvancedSettingsCards();
4550
const feedbackCard = () => {
4651
if ([ProblemTypeKeys.MULTISELECT].includes(problemType)) {
@@ -199,11 +204,9 @@ SettingsWidget.propTypes = {
199204
rerandomize: PropTypes.string,
200205
}).isRequired,
201206
images: PropTypes.shape({}).isRequired,
202-
learningContextId: PropTypes.string.isRequired,
203207
isLibrary: PropTypes.bool.isRequired,
204208
// eslint-disable-next-line
205209
settings: PropTypes.any.isRequired,
206-
showMarkdownEditorButton: PropTypes.bool.isRequired,
207210
};
208211

209212
const mapStateToProps = (state) => ({
@@ -215,9 +218,6 @@ const mapStateToProps = (state) => ({
215218
defaultSettings: selectors.problem.defaultSettings(state),
216219
images: selectors.app.images(state),
217220
isLibrary: selectors.app.isLibrary(state),
218-
learningContextId: selectors.app.learningContextId(state),
219-
showMarkdownEditorButton: selectors.app.isMarkdownEditorEnabledForCourse(state)
220-
&& selectors.problem.rawMarkdown(state),
221221
});
222222

223223
export const mapDispatchToProps = {

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

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

33
import { ProblemTypeKeys } from '@src/editors/data/constants/problem';
4-
import {
5-
render, screen, initializeMocks,
6-
} from '@src/testUtils';
4+
import { screen, initializeMocks } from '@src/testUtils';
5+
import { editorRender } from '@src/editors/editorTestRender';
6+
import { mockWaffleFlags } from '@src/data/apiHooks.mock';
77
import * as hooks from './hooks';
88
import { SettingsWidgetInternal as SettingsWidget } from '.';
99

@@ -17,6 +17,7 @@ jest.mock('./settingsComponents/ShowAnswerCard', () => 'ShowAnswerCard');
1717
jest.mock('./settingsComponents/SwitchEditorCard', () => 'SwitchEditorCard');
1818
jest.mock('./settingsComponents/TimerCard', () => 'TimerCard');
1919
jest.mock('./settingsComponents/TypeCard', () => 'TypeCard');
20+
mockWaffleFlags();
2021

2122
describe('SettingsWidget', () => {
2223
const showAdvancedSettingsCardsBaseProps = {
@@ -55,15 +56,15 @@ describe('SettingsWidget', () => {
5556
describe('behavior', () => {
5657
it('calls showAdvancedSettingsCards when initialized', () => {
5758
jest.spyOn(hooks, 'showAdvancedSettingsCards').mockReturnValue(showAdvancedSettingsCardsBaseProps);
58-
render(<SettingsWidget {...props} />);
59+
editorRender(<SettingsWidget {...props} />);
5960
expect(hooks.showAdvancedSettingsCards).toHaveBeenCalled();
6061
});
6162
});
6263

6364
describe('renders', () => {
6465
test('renders Settings widget page', () => {
6566
jest.spyOn(hooks, 'showAdvancedSettingsCards').mockReturnValue(showAdvancedSettingsCardsBaseProps);
66-
render(<SettingsWidget {...props} />);
67+
editorRender(<SettingsWidget {...props} />);
6768
expect(screen.getByText('Show advanced settings')).toBeInTheDocument();
6869
});
6970

@@ -73,7 +74,7 @@ describe('SettingsWidget', () => {
7374
isAdvancedCardsVisible: true,
7475
};
7576
jest.spyOn(hooks, 'showAdvancedSettingsCards').mockReturnValue(showAdvancedSettingsCardsProps);
76-
const { container } = render(<SettingsWidget {...props} />);
77+
const { container } = editorRender(<SettingsWidget {...props} />);
7778
expect(screen.queryByText('Show advanced settings')).not.toBeInTheDocument();
7879
expect(container.querySelector('showanswercard')).toBeInTheDocument();
7980
expect(container.querySelector('resetcard')).toBeInTheDocument();
@@ -85,7 +86,7 @@ describe('SettingsWidget', () => {
8586
isAdvancedCardsVisible: true,
8687
};
8788
jest.spyOn(hooks, 'showAdvancedSettingsCards').mockReturnValue(showAdvancedSettingsCardsProps);
88-
const { container } = render(
89+
const { container } = editorRender(
8990
<SettingsWidget {...props} problemType={ProblemTypeKeys.ADVANCED} />,
9091
);
9192
expect(container.querySelector('randomization')).toBeInTheDocument();
@@ -99,7 +100,7 @@ describe('SettingsWidget', () => {
99100
};
100101
test('renders Settings widget page', () => {
101102
jest.spyOn(hooks, 'showAdvancedSettingsCards').mockReturnValue(showAdvancedSettingsCardsBaseProps);
102-
const { container } = render(<SettingsWidget {...libraryProps} />);
103+
const { container } = editorRender(<SettingsWidget {...libraryProps} />);
103104
expect(container.querySelector('timercard')).not.toBeInTheDocument();
104105
expect(container.querySelector('resetcard')).not.toBeInTheDocument();
105106
expect(container.querySelector('typecard')).toBeInTheDocument();
@@ -113,7 +114,7 @@ describe('SettingsWidget', () => {
113114
isAdvancedCardsVisible: true,
114115
};
115116
jest.spyOn(hooks, 'showAdvancedSettingsCards').mockReturnValue(showAdvancedSettingsCardsProps);
116-
const { container } = render(<SettingsWidget {...libraryProps} />);
117+
const { container } = editorRender(<SettingsWidget {...libraryProps} />);
117118
expect(screen.queryByText('Show advanced settings')).not.toBeInTheDocument();
118119
expect(container.querySelector('showanswearscard')).not.toBeInTheDocument();
119120
expect(container.querySelector('resetcard')).not.toBeInTheDocument();
@@ -127,7 +128,7 @@ describe('SettingsWidget', () => {
127128
isAdvancedCardsVisible: true,
128129
};
129130
jest.spyOn(hooks, 'showAdvancedSettingsCards').mockReturnValue(showAdvancedSettingsCardsProps);
130-
const { container } = render(<SettingsWidget {...libraryProps} problemType={ProblemTypeKeys.ADVANCED} />);
131+
const { container } = editorRender(<SettingsWidget {...libraryProps} problemType={ProblemTypeKeys.ADVANCED} />);
131132
expect(container.querySelector('randomization')).toBeInTheDocument();
132133
});
133134
});
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
11
import React from 'react';
2-
import { connect } from 'react-redux';
2+
import { useDispatch, useSelector } 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';
8+
import BaseModal from '@src/editors/sharedComponents/BaseModal';
9+
import Button from '@src/editors/sharedComponents/Button';
10+
import { ProblemTypeKeys } from '@src/editors/data/constants/problem';
611
import messages from '../messages';
7-
import { selectors, thunkActions } from '../../../../../../data/redux';
8-
import BaseModal from '../../../../../../sharedComponents/BaseModal';
9-
import Button from '../../../../../../sharedComponents/Button';
1012
import { handleConfirmEditorSwitch } from '../hooks';
11-
import { ProblemTypeKeys } from '../../../../../../data/constants/problem';
1213

1314
const SwitchEditorCard = ({
1415
editorType,
1516
problemType,
16-
switchEditor,
17-
isMarkdownEditorEnabled,
1817
}) => {
1918
const [isConfirmOpen, setConfirmOpen] = React.useState(false);
19+
const { isMarkdownEditorEnabledForContext } = useEditorContext();
20+
const isMarkdownEditorEnabled = useSelector(selectors.problem.isMarkdownEditorEnabled);
21+
const dispatch = useDispatch();
2022

21-
if (isMarkdownEditorEnabled || problemType === ProblemTypeKeys.ADVANCED) { return null; }
23+
const isMarkdownEditorActive = isMarkdownEditorEnabled && isMarkdownEditorEnabledForContext;
24+
if (isMarkdownEditorActive || problemType === ProblemTypeKeys.ADVANCED) { return null; }
2225

2326
return (
2427
<Card className="border border-light-700 shadow-none">
@@ -29,7 +32,10 @@ const SwitchEditorCard = ({
2932
confirmAction={(
3033
<Button
3134
/* istanbul ignore next */
32-
onClick={() => handleConfirmEditorSwitch({ switchEditor: () => switchEditor(editorType), setConfirmOpen })}
35+
onClick={() => handleConfirmEditorSwitch({
36+
switchEditor: () => dispatch(thunkActions.problem.switchEditor(editorType)),
37+
setConfirmOpen,
38+
})}
3339
variant="primary"
3440
>
3541
<FormattedMessage {...messages[`ConfirmSwitchButtonLabel-${editorType}`]} />
@@ -52,19 +58,8 @@ const SwitchEditorCard = ({
5258
};
5359

5460
SwitchEditorCard.propTypes = {
55-
switchEditor: PropTypes.func.isRequired,
56-
isMarkdownEditorEnabled: PropTypes.bool.isRequired,
5761
problemType: PropTypes.string.isRequired,
5862
editorType: PropTypes.string.isRequired,
5963
};
6064

61-
export const mapStateToProps = (state) => ({
62-
isMarkdownEditorEnabled: selectors.problem.isMarkdownEditorEnabled(state)
63-
&& selectors.app.isMarkdownEditorEnabledForCourse(state),
64-
});
65-
export const mapDispatchToProps = {
66-
switchEditor: thunkActions.problem.switchEditor,
67-
};
68-
69-
export const SwitchEditorCardInternal = SwitchEditorCard; // For testing only
70-
export default connect(mapStateToProps, mapDispatchToProps)(SwitchEditorCard);
65+
export default SwitchEditorCard;
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,77 @@
11
import React from 'react';
2-
import {
3-
render, screen, initializeMocks, fireEvent,
4-
} from '@src/testUtils';
5-
import { SwitchEditorCardInternal as SwitchEditorCard, mapDispatchToProps } from './SwitchEditorCard';
6-
import { thunkActions } from '../../../../../../data/redux';
7-
8-
describe('SwitchEditorCard', () => {
9-
const mockSwitchEditor = jest.fn().mockName('switchEditor');
2+
import { screen, initializeMocks, within } from '@src/testUtils';
3+
import userEvent from '@testing-library/user-event';
4+
import { editorRender } from '@src/editors/editorTestRender';
5+
import { mockWaffleFlags } from '@src/data/apiHooks.mock';
6+
import { thunkActions } from '@src/editors/data/redux';
7+
import SwitchEditorCard from './SwitchEditorCard';
8+
9+
const switchEditorSpy = jest.spyOn(thunkActions.problem, 'switchEditor');
10+
11+
describe('SwitchEditorCard - markdown', () => {
1012
const baseProps = {
11-
switchEditor: mockSwitchEditor,
1213
problemType: 'stringresponse',
1314
editorType: 'markdown',
14-
isMarkdownEditorEnabled: false,
1515
};
1616

1717
beforeEach(() => {
1818
initializeMocks();
1919
});
2020

21-
test('renders SwitchEditorCard', () => {
22-
render(<SwitchEditorCard {...baseProps} />);
21+
test('renders SwitchEditorCard', async () => {
22+
// Markdown Editor support is on for this course:
23+
mockWaffleFlags({ useReactMarkdownEditor: true });
24+
// The markdown editor is not currently active (default)
25+
26+
editorRender(<SwitchEditorCard {...baseProps} />);
27+
const user = userEvent.setup();
2328
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
2429
const switchButton = screen.getByRole('button', { name: 'Switch to markdown editor' });
2530
expect(switchButton).toBeInTheDocument();
26-
fireEvent.click(switchButton);
31+
await user.click(switchButton);
32+
// A confirmation dialog is shown:
2733
expect(screen.getByRole('dialog')).toBeInTheDocument();
2834
});
2935

30-
test('calls switchEditor function when confirm button is clicked', () => {
31-
render(<SwitchEditorCard {...baseProps} />);
36+
test('calls switchEditor function when confirm button is clicked', async () => {
37+
// Markdown Editor support is on for this course:
38+
mockWaffleFlags({ useReactMarkdownEditor: true });
39+
// The markdown editor is not currently active (default)
40+
41+
editorRender(<SwitchEditorCard {...baseProps} />);
42+
const user = userEvent.setup();
3243
const switchButton = screen.getByRole('button', { name: 'Switch to markdown editor' });
3344
expect(switchButton).toBeInTheDocument();
34-
fireEvent.click(switchButton);
35-
const confirmButton = screen.getByRole('button', { name: 'Switch to markdown editor' });
45+
await user.click(switchButton);
46+
47+
const modal = screen.getByRole('dialog');
48+
const confirmButton = within(modal).getByRole('button', { name: 'Switch to markdown editor' });
3649
expect(confirmButton).toBeInTheDocument();
37-
fireEvent.click(confirmButton);
38-
expect(mockSwitchEditor).toHaveBeenCalledWith('markdown');
50+
expect(switchEditorSpy).not.toHaveBeenCalled();
51+
await user.click(confirmButton);
52+
expect(switchEditorSpy).toHaveBeenCalledWith('markdown');
53+
// Markdown editor would now be active.
3954
});
4055

4156
test('renders nothing for advanced problemType', () => {
42-
const { container } = render(<SwitchEditorCard {...baseProps} problemType="advanced" />);
57+
const { container } = editorRender(<SwitchEditorCard {...baseProps} problemType="advanced" />);
4358
const reduxWrapper = (container.firstChild as HTMLElement | null);
4459
expect(reduxWrapper?.innerHTML).toBe('');
4560
});
4661

47-
test('snapshot: SwitchEditorCard returns null when editor is Markdown', () => {
48-
const { container } = render(<SwitchEditorCard {...baseProps} editorType="markdown" isMarkdownEditorEnabled />);
49-
const reduxWrapper = (container.firstChild as HTMLElement | null);
50-
expect(reduxWrapper?.innerHTML).toBe('');
51-
});
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)
5266

53-
describe('mapDispatchToProps', () => {
54-
test('updateField from actions.problem.updateField', () => {
55-
expect(mapDispatchToProps.switchEditor).toEqual(thunkActions.problem.switchEditor);
67+
const { container } = editorRender(<SwitchEditorCard {...baseProps} />, {
68+
initialState: {
69+
problem: {
70+
isMarkdownEditorEnabled: true,
71+
},
72+
},
5673
});
74+
const reduxWrapper = (container.firstChild as HTMLElement | null);
75+
expect(reduxWrapper?.innerHTML).toBe('');
5776
});
5877
});

0 commit comments

Comments
 (0)