Skip to content

Commit fd48fef

Browse files
feat: edit Text components within content libraries [FC-0062] (#1240)
1 parent 9b61037 commit fd48fef

36 files changed

+724
-233
lines changed

src/CourseAuthoringRoutes.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ const CourseAuthoringRoutes = () => {
8888
/>
8989
<Route
9090
path="editor/:blockType/:blockId?"
91-
element={<PageWrap><EditorContainer courseId={courseId} /></PageWrap>}
91+
element={<PageWrap><EditorContainer learningContextId={courseId} /></PageWrap>}
9292
/>
9393
<Route
9494
path="settings/details"

src/editors/EditorContainer.jsx

Lines changed: 0 additions & 28 deletions
This file was deleted.

src/editors/EditorContainer.test.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jest.mock('react-router', () => ({
1010
}),
1111
}));
1212

13-
const props = { courseId: 'cOuRsEId' };
13+
const props = { learningContextId: 'cOuRsEId' };
1414

1515
describe('Editor Container', () => {
1616
describe('snapshots', () => {

src/editors/EditorContainer.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import React from 'react';
2+
import { useParams } from 'react-router-dom';
3+
import { getConfig } from '@edx/frontend-platform';
4+
5+
import EditorPage from './EditorPage';
6+
7+
interface Props {
8+
/** Course ID or Library ID */
9+
learningContextId: string;
10+
/** Event handler for when user cancels out of the editor page */
11+
onClose?: () => void;
12+
/** Event handler called after when user saves their changes using an editor */
13+
afterSave?: () => (newData: Record<string, any>) => void;
14+
}
15+
16+
const EditorContainer: React.FC<Props> = ({
17+
learningContextId,
18+
onClose,
19+
afterSave,
20+
}) => {
21+
const { blockType, blockId } = useParams();
22+
if (blockType === undefined || blockId === undefined) {
23+
// istanbul ignore next - This shouldn't be possible; it's just here to satisfy the type checker.
24+
return <div>Error: missing URL parameters</div>;
25+
}
26+
if (!!onClose !== !!afterSave) {
27+
/* istanbul ignore next */
28+
throw new Error('You must specify both onClose and afterSave or neither.');
29+
// These parameters are a bit messy so I'm trying to help make it more
30+
// consistent here. For example, if you specify onClose, then returnFunction
31+
// is only called if the save is successful. But if you leave onClose
32+
// undefined, then returnFunction is called in either case, and with
33+
// different arguments. The underlying EditorPage should be refactored to
34+
// have more clear events like onCancel and onSaveSuccess
35+
}
36+
return (
37+
<div className="editor-page">
38+
<EditorPage
39+
courseId={learningContextId}
40+
blockType={blockType}
41+
blockId={blockId}
42+
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
43+
lmsEndpointUrl={getConfig().LMS_BASE_URL}
44+
onClose={onClose}
45+
returnFunction={afterSave}
46+
/>
47+
</div>
48+
);
49+
};
50+
51+
export default EditorContainer;

src/editors/containers/EditorContainer/__snapshots__/index.test.jsx.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ exports[`EditorContainer component render snapshot: initialized. enable save and
5151
/>
5252
</h2>
5353
<IconButton
54+
alt="Exit the editor"
5455
iconAs="Icon"
5556
onClick={[MockFunction openCancelConfirmModal]}
5657
src={[MockFunction icons.Close]}
@@ -132,6 +133,7 @@ exports[`EditorContainer component render snapshot: not initialized. disable sav
132133
/>
133134
</h2>
134135
<IconButton
136+
alt="Exit the editor"
135137
iconAs="Icon"
136138
onClick={[MockFunction openCancelConfirmModal]}
137139
src={[MockFunction icons.Close]}

src/editors/containers/EditorContainer/index.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ const EditorContainer = ({
6363
src={Close}
6464
iconAs={Icon}
6565
onClick={openCancelConfirmModal}
66+
alt={intl.formatMessage(messages.exitButtonAlt)}
6667
/>
6768
</div>
6869
</ModalDialog.Header>

src/editors/containers/EditorContainer/messages.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ const messages = defineMessages({
1212
defaultMessage: 'Are you sure you want to exit the editor? Any unsaved changes will be lost.',
1313
description: 'Description text for modal confirming cancellation',
1414
},
15+
exitButtonAlt: {
16+
id: 'authoring.editorContainer.exitButton.alt',
17+
defaultMessage: 'Exit the editor',
18+
description: 'Alt text for the Exit button',
19+
},
1520
okButtonLabel: {
1621
id: 'authoring.editorContainer.okButton.label',
1722
defaultMessage: 'OK',

src/editors/containers/TextEditor/__snapshots__/index.test.jsx.snap

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,7 @@ exports[`TextEditor snapshots block failed to load, Toast is shown 1`] = `
2424
onClose={[MockFunction hooks.nullMethod]}
2525
show={true}
2626
>
27-
<FormattedMessage
28-
defaultMessage="Error: Could Not Load Text Content"
29-
description="Error Message Dispayed When HTML content fails to Load"
30-
id="authoring.texteditor.load.error"
31-
/>
27+
Error: Could Not Load Text Content
3228
</Toast>
3329
<TinyMceWidget
3430
disabled={false}
@@ -81,11 +77,7 @@ exports[`TextEditor snapshots loaded, raw editor 1`] = `
8177
onClose={[MockFunction hooks.nullMethod]}
8278
show={false}
8379
>
84-
<FormattedMessage
85-
defaultMessage="Error: Could Not Load Text Content"
86-
description="Error Message Dispayed When HTML content fails to Load"
87-
id="authoring.texteditor.load.error"
88-
/>
80+
Error: Could Not Load Text Content
8981
</Toast>
9082
<RawEditor
9183
content={
@@ -132,11 +124,7 @@ exports[`TextEditor snapshots not yet loaded, Spinner appears 1`] = `
132124
onClose={[MockFunction hooks.nullMethod]}
133125
show={false}
134126
>
135-
<FormattedMessage
136-
defaultMessage="Error: Could Not Load Text Content"
137-
description="Error Message Dispayed When HTML content fails to Load"
138-
id="authoring.texteditor.load.error"
139-
/>
127+
Error: Could Not Load Text Content
140128
</Toast>
141129
<div
142130
className="text-center p-6"
@@ -175,11 +163,7 @@ exports[`TextEditor snapshots renders as expected with default behavior 1`] = `
175163
onClose={[MockFunction hooks.nullMethod]}
176164
show={false}
177165
>
178-
<FormattedMessage
179-
defaultMessage="Error: Could Not Load Text Content"
180-
description="Error Message Dispayed When HTML content fails to Load"
181-
id="authoring.texteditor.load.error"
182-
/>
166+
Error: Could Not Load Text Content
183167
</Toast>
184168
<TinyMceWidget
185169
disabled={false}
@@ -232,11 +216,7 @@ exports[`TextEditor snapshots renders static images with relative paths 1`] = `
232216
onClose={[MockFunction hooks.nullMethod]}
233217
show={false}
234218
>
235-
<FormattedMessage
236-
defaultMessage="Error: Could Not Load Text Content"
237-
description="Error Message Dispayed When HTML content fails to Load"
238-
id="authoring.texteditor.load.error"
239-
/>
219+
Error: Could Not Load Text Content
240220
</Toast>
241221
<TinyMceWidget
242222
disabled={false}

src/editors/containers/TextEditor/index.jsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
Spinner,
77
Toast,
88
} from '@openedx/paragon';
9-
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
9+
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
1010

1111
import { actions, selectors } from '../../data/redux';
1212
import { RequestKeys } from '../../data/constants/requests';
@@ -78,7 +78,7 @@ const TextEditor = ({
7878
>
7979
<div className="editor-body h-75 overflow-auto">
8080
<Toast show={blockFailed} onClose={hooks.nullMethod}>
81-
<FormattedMessage {...messages.couldNotLoadTextContext} />
81+
{ intl.formatMessage(messages.couldNotLoadTextContext) }
8282
</Toast>
8383

8484
{(!blockFinished)
@@ -111,7 +111,7 @@ TextEditor.propTypes = {
111111
initializeEditor: PropTypes.func.isRequired,
112112
showRawEditor: PropTypes.bool.isRequired,
113113
blockFinished: PropTypes.bool,
114-
learningContextId: PropTypes.string.isRequired,
114+
learningContextId: PropTypes.string, // This should be required but is NULL when the store is in initial state :/
115115
images: PropTypes.shape({}).isRequired,
116116
isLibrary: PropTypes.bool.isRequired,
117117
// inject

src/editors/data/redux/app/selectors.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,9 @@ export const returnUrl = createSelector(
4242

4343
export const isInitialized = createSelector(
4444
[
45-
module.simpleSelectors.unitUrl,
4645
module.simpleSelectors.blockValue,
4746
],
48-
(unitUrl, blockValue) => !!(unitUrl && blockValue),
47+
(blockValue) => !!(blockValue),
4948
);
5049

5150
export const displayTitle = createSelector(

src/editors/data/redux/app/selectors.test.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,23 +78,20 @@ describe('app selectors unit tests', () => {
7878
});
7979
});
8080
describe('isInitialized selector', () => {
81-
it('is memoized based on unitUrl, editorInitialized, and blockValue', () => {
81+
it('is memoized based on editorInitialized and blockValue', () => {
8282
expect(selectors.isInitialized.preSelectors).toEqual([
83-
simpleSelectors.unitUrl,
8483
simpleSelectors.blockValue,
8584
]);
8685
});
87-
it('returns true iff unitUrl, blockValue, and editorInitialized are all truthy', () => {
86+
it('returns true iff blockValue and editorInitialized are truthy', () => {
8887
const { cb } = selectors.isInitialized;
8988
const truthy = {
90-
url: { url: 'data' },
9189
blockValue: { block: 'value' },
9290
};
9391

9492
[
95-
[[null, truthy.blockValue], false],
96-
[[truthy.url, null], false],
97-
[[truthy.url, truthy.blockValue], true],
93+
[[truthy.blockValue], true],
94+
[[null], false],
9895
].map(([args, expected]) => expect(cb(...args)).toEqual(expected));
9996
});
10097
});

src/editors/data/redux/thunkActions/app.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ export const initialize = (data) => (dispatch) => {
8989
const editorType = data.blockType;
9090
dispatch(actions.app.initialize(data));
9191
dispatch(module.fetchBlock());
92-
dispatch(module.fetchUnit());
92+
if (data.blockId?.startsWith('block-v1:')) {
93+
dispatch(module.fetchUnit());
94+
}
9395
switch (editorType) {
9496
case 'problem':
9597
dispatch(module.fetchImages({ pageNumber: 0 }));
@@ -100,7 +102,12 @@ export const initialize = (data) => (dispatch) => {
100102
dispatch(module.fetchCourseDetails());
101103
break;
102104
case 'html':
103-
dispatch(module.fetchImages({ pageNumber: 0 }));
105+
if (data.learningContextId?.startsWith('lib:')) {
106+
// eslint-disable-next-line no-console
107+
console.log('Not fetching image assets - not implemented yet for content libraries.');
108+
} else {
109+
dispatch(module.fetchImages({ pageNumber: 0 }));
110+
}
104111
break;
105112
default:
106113
break;

src/editors/data/redux/thunkActions/app.test.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,6 @@ describe('app thunkActions', () => {
187187
expect(dispatch.mock.calls).toEqual([
188188
[actions.app.initialize(testValue)],
189189
[thunkActions.fetchBlock()],
190-
[thunkActions.fetchUnit()],
191190
]);
192191
thunkActions.fetchBlock = fetchBlock;
193192
thunkActions.fetchUnit = fetchUnit;
@@ -216,6 +215,8 @@ describe('app thunkActions', () => {
216215
const data = {
217216
...testValue,
218217
blockType: 'html',
218+
blockId: 'block-v1:UniversityX+PHYS+1+type@problem+block@123',
219+
learningContextId: 'course-v1:UniversityX+PHYS+1',
219220
};
220221
thunkActions.initialize(data)(dispatch);
221222
expect(dispatch.mock.calls).toEqual([
@@ -251,6 +252,8 @@ describe('app thunkActions', () => {
251252
const data = {
252253
...testValue,
253254
blockType: 'problem',
255+
blockId: 'block-v1:UniversityX+PHYS+1+type@problem+block@123',
256+
learningContextId: 'course-v1:UniversityX+PHYS+1',
254257
};
255258
thunkActions.initialize(data)(dispatch);
256259
expect(dispatch.mock.calls).toEqual([
@@ -286,6 +289,8 @@ describe('app thunkActions', () => {
286289
const data = {
287290
...testValue,
288291
blockType: 'video',
292+
blockId: 'block-v1:UniversityX+PHYS+1+type@problem+block@123',
293+
learningContextId: 'course-v1:UniversityX+PHYS+1',
289294
};
290295
thunkActions.initialize(data)(dispatch);
291296
expect(dispatch.mock.calls).toEqual([

src/editors/data/services/cms/urls.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,7 @@ export const blockAncestor = ({ studioEndpointUrl, blockId }) => {
3838
if (blockId.includes('block-v1')) {
3939
return `${block({ studioEndpointUrl, blockId })}?fields=ancestorInfo`;
4040
}
41-
// this url only need to get info to build the return url, which isn't used by V2 blocks
42-
// (temporary) don't throw error, just return empty url. it will fail it's network connection but otherwise
43-
// the app will run
44-
// throw new Error('Block ancestor not available (and not needed) for V2 blocks');
45-
return '';
41+
throw new Error('Block ancestor not available (and not needed) for V2 blocks');
4642
};
4743

4844
export const blockStudioView = ({ studioEndpointUrl, blockId }) => (

src/editors/data/services/cms/urls.test.js

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,9 @@ describe('cms url methods', () => {
9595
expect(blockAncestor({ studioEndpointUrl, blockId }))
9696
.toEqual(`${block({ studioEndpointUrl, blockId })}?fields=ancestorInfo`);
9797
});
98-
// This test will probably be used in the future
99-
// it('throws error with studioEndpointUrl, v2 blockId and ancestor query', () => {
100-
// expect(() => { blockAncestor({ studioEndpointUrl, blockId: v2BlockId }); })
101-
// .toThrow('Block ancestor not available (and not needed) for V2 blocks');
102-
// });
103-
it('returns blank url with studioEndpointUrl, v2 blockId and ancestor query', () => {
104-
expect(blockAncestor({ studioEndpointUrl, blockId: v2BlockId }))
105-
.toEqual('');
98+
it('throws error with studioEndpointUrl, v2 blockId and ancestor query', () => {
99+
expect(() => { blockAncestor({ studioEndpointUrl, blockId: v2BlockId }); })
100+
.toThrow('Block ancestor not available (and not needed) for V2 blocks');
106101
});
107102
});
108103
describe('blockStudioView', () => {

0 commit comments

Comments
 (0)