Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: edit Text components within content libraries [FC-0062] #1240

Merged
merged 20 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1ea2e06
feat: make the text editor work with content libraries
bradenmacdonald Aug 1, 2024
db477c7
feat: In libraries, allow opening the editor for text (html) components
bradenmacdonald Jul 31, 2024
42e50da
refactor: utility method for getting the "Edit" URL of a component
bradenmacdonald Sep 4, 2024
49a903a
test: update tests
bradenmacdonald Sep 4, 2024
e21adef
feat: developer info panel
bradenmacdonald Sep 4, 2024
9eec8a4
fix: sidebar needs to stick to the screen properly
bradenmacdonald Sep 6, 2024
19fb9e2
feat: open the editor when creating a new component
bradenmacdonald Sep 6, 2024
7fddc77
test: simplify the AddContentContainer tests
bradenmacdonald Sep 7, 2024
637e4a3
test: workflow test for editor
bradenmacdonald Sep 9, 2024
51470b3
test: update test coverage
bradenmacdonald Sep 10, 2024
7c7dee3
test: cleanups to AddContentContainer test (from review comments)
bradenmacdonald Sep 11, 2024
cdd2b63
refactor: cleanups from code review
bradenmacdonald Sep 11, 2024
b0f83d6
refactor: better validation for usageKey utils
bradenmacdonald Sep 11, 2024
9e62bc0
refactor: avoid footgun with EditorContainer
bradenmacdonald Sep 11, 2024
42df1c2
fix: console warning when TextEditor is initialized
bradenmacdonald Sep 11, 2024
b5e754a
fix: a11y - add alt text for "exit" button in the editors
bradenmacdonald Sep 11, 2024
f5796ac
refactor: separate xblock queryKeys from library queryKeys
bradenmacdonald Sep 11, 2024
bb051e6
test: simplify flaky test
bradenmacdonald Sep 11, 2024
be9d01b
fix: propTypes validation warning on Toast content string
bradenmacdonald Sep 11, 2024
0ed2c2a
chore: update with latest master
bradenmacdonald Sep 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/CourseAuthoringRoutes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ const CourseAuthoringRoutes = () => {
/>
<Route
path="editor/:blockType/:blockId?"
element={<PageWrap><EditorContainer courseId={courseId} /></PageWrap>}
element={<PageWrap><EditorContainer learningContextId={courseId} /></PageWrap>}
/>
<Route
path="settings/details"
Expand Down
28 changes: 0 additions & 28 deletions src/editors/EditorContainer.jsx

This file was deleted.

2 changes: 1 addition & 1 deletion src/editors/EditorContainer.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jest.mock('react-router', () => ({
}),
}));

const props = { courseId: 'cOuRsEId' };
const props = { learningContextId: 'cOuRsEId' };

describe('Editor Container', () => {
describe('snapshots', () => {
Expand Down
51 changes: 51 additions & 0 deletions src/editors/EditorContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';
import { useParams } from 'react-router-dom';
import { getConfig } from '@edx/frontend-platform';

import EditorPage from './EditorPage';

interface Props {
/** Course ID or Library ID */
learningContextId: string;
/** Event handler for when user cancels out of the editor page */
onClose?: () => void;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This issue is with the underlying editor, but passing onClose={undefined} here actually crashes the TextEditor.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried to address this with 9e62bc0

/** Event handler called after when user saves their changes using an editor */
afterSave?: () => (newData: Record<string, any>) => void;
}

const EditorContainer: React.FC<Props> = ({
learningContextId,
onClose,
afterSave,
}) => {
const { blockType, blockId } = useParams();
if (blockType === undefined || blockId === undefined) {
// istanbul ignore next - This shouldn't be possible; it's just here to satisfy the type checker.
return <div>Error: missing URL parameters</div>;
}
if (!!onClose !== !!afterSave) {
/* istanbul ignore next */
throw new Error('You must specify both onClose and afterSave or neither.');
// These parameters are a bit messy so I'm trying to help make it more
// consistent here. For example, if you specify onClose, then returnFunction
// is only called if the save is successful. But if you leave onClose
// undefined, then returnFunction is called in either case, and with
// different arguments. The underlying EditorPage should be refactored to
// have more clear events like onCancel and onSaveSuccess
}
return (
<div className="editor-page">
<EditorPage
courseId={learningContextId}
blockType={blockType}
blockId={blockId}
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
lmsEndpointUrl={getConfig().LMS_BASE_URL}
onClose={onClose}
returnFunction={afterSave}
/>
</div>
);
};

export default EditorContainer;
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ exports[`EditorContainer component render snapshot: initialized. enable save and
/>
</h2>
<IconButton
alt="Exit the editor"
iconAs="Icon"
onClick={[MockFunction openCancelConfirmModal]}
src={[MockFunction icons.Close]}
Expand Down Expand Up @@ -132,6 +133,7 @@ exports[`EditorContainer component render snapshot: not initialized. disable sav
/>
</h2>
<IconButton
alt="Exit the editor"
iconAs="Icon"
onClick={[MockFunction openCancelConfirmModal]}
src={[MockFunction icons.Close]}
Expand Down
1 change: 1 addition & 0 deletions src/editors/containers/EditorContainer/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const EditorContainer = ({
src={Close}
iconAs={Icon}
onClick={openCancelConfirmModal}
alt={intl.formatMessage(messages.exitButtonAlt)}
/>
</div>
</ModalDialog.Header>
Expand Down
5 changes: 5 additions & 0 deletions src/editors/containers/EditorContainer/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ const messages = defineMessages({
defaultMessage: 'Are you sure you want to exit the editor? Any unsaved changes will be lost.',
description: 'Description text for modal confirming cancellation',
},
exitButtonAlt: {
id: 'authoring.editorContainer.exitButton.alt',
defaultMessage: 'Exit the editor',
description: 'Alt text for the Exit button',
},
okButtonLabel: {
id: 'authoring.editorContainer.okButton.label',
defaultMessage: 'OK',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,7 @@ exports[`TextEditor snapshots block failed to load, Toast is shown 1`] = `
onClose={[MockFunction hooks.nullMethod]}
show={true}
>
<FormattedMessage
defaultMessage="Error: Could Not Load Text Content"
description="Error Message Dispayed When HTML content fails to Load"
id="authoring.texteditor.load.error"
/>
Error: Could Not Load Text Content
</Toast>
<TinyMceWidget
disabled={false}
Expand Down Expand Up @@ -81,11 +77,7 @@ exports[`TextEditor snapshots loaded, raw editor 1`] = `
onClose={[MockFunction hooks.nullMethod]}
show={false}
>
<FormattedMessage
defaultMessage="Error: Could Not Load Text Content"
description="Error Message Dispayed When HTML content fails to Load"
id="authoring.texteditor.load.error"
/>
Error: Could Not Load Text Content
</Toast>
<RawEditor
content={
Expand Down Expand Up @@ -132,11 +124,7 @@ exports[`TextEditor snapshots not yet loaded, Spinner appears 1`] = `
onClose={[MockFunction hooks.nullMethod]}
show={false}
>
<FormattedMessage
defaultMessage="Error: Could Not Load Text Content"
description="Error Message Dispayed When HTML content fails to Load"
id="authoring.texteditor.load.error"
/>
Error: Could Not Load Text Content
</Toast>
<div
className="text-center p-6"
Expand Down Expand Up @@ -175,11 +163,7 @@ exports[`TextEditor snapshots renders as expected with default behavior 1`] = `
onClose={[MockFunction hooks.nullMethod]}
show={false}
>
<FormattedMessage
defaultMessage="Error: Could Not Load Text Content"
description="Error Message Dispayed When HTML content fails to Load"
id="authoring.texteditor.load.error"
/>
Error: Could Not Load Text Content
</Toast>
<TinyMceWidget
disabled={false}
Expand Down Expand Up @@ -232,11 +216,7 @@ exports[`TextEditor snapshots renders static images with relative paths 1`] = `
onClose={[MockFunction hooks.nullMethod]}
show={false}
>
<FormattedMessage
defaultMessage="Error: Could Not Load Text Content"
description="Error Message Dispayed When HTML content fails to Load"
id="authoring.texteditor.load.error"
/>
Error: Could Not Load Text Content
</Toast>
<TinyMceWidget
disabled={false}
Expand Down
6 changes: 3 additions & 3 deletions src/editors/containers/TextEditor/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
Spinner,
Toast,
} from '@openedx/paragon';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';

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

{(!blockFinished)
Expand Down Expand Up @@ -111,7 +111,7 @@ TextEditor.propTypes = {
initializeEditor: PropTypes.func.isRequired,
showRawEditor: PropTypes.bool.isRequired,
blockFinished: PropTypes.bool,
learningContextId: PropTypes.string.isRequired,
learningContextId: PropTypes.string, // This should be required but is NULL when the store is in initial state :/
images: PropTypes.shape({}).isRequired,
isLibrary: PropTypes.bool.isRequired,
// inject
Expand Down
3 changes: 1 addition & 2 deletions src/editors/data/redux/app/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,9 @@ export const returnUrl = createSelector(

export const isInitialized = createSelector(
[
module.simpleSelectors.unitUrl,
module.simpleSelectors.blockValue,
],
(unitUrl, blockValue) => !!(unitUrl && blockValue),
(blockValue) => !!(blockValue),
);

export const displayTitle = createSelector(
Expand Down
11 changes: 4 additions & 7 deletions src/editors/data/redux/app/selectors.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,23 +78,20 @@ describe('app selectors unit tests', () => {
});
});
describe('isInitialized selector', () => {
it('is memoized based on unitUrl, editorInitialized, and blockValue', () => {
it('is memoized based on editorInitialized and blockValue', () => {
expect(selectors.isInitialized.preSelectors).toEqual([
simpleSelectors.unitUrl,
simpleSelectors.blockValue,
]);
});
it('returns true iff unitUrl, blockValue, and editorInitialized are all truthy', () => {
it('returns true iff blockValue and editorInitialized are truthy', () => {
const { cb } = selectors.isInitialized;
const truthy = {
url: { url: 'data' },
blockValue: { block: 'value' },
};

[
[[null, truthy.blockValue], false],
[[truthy.url, null], false],
[[truthy.url, truthy.blockValue], true],
[[truthy.blockValue], true],
[[null], false],
].map(([args, expected]) => expect(cb(...args)).toEqual(expected));
});
});
Expand Down
11 changes: 9 additions & 2 deletions src/editors/data/redux/thunkActions/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ export const initialize = (data) => (dispatch) => {
const editorType = data.blockType;
dispatch(actions.app.initialize(data));
dispatch(module.fetchBlock());
dispatch(module.fetchUnit());
if (data.blockId?.startsWith('block-v1:')) {
dispatch(module.fetchUnit());
}
switch (editorType) {
case 'problem':
dispatch(module.fetchImages({ pageNumber: 0 }));
Expand All @@ -100,7 +102,12 @@ export const initialize = (data) => (dispatch) => {
dispatch(module.fetchCourseDetails());
break;
case 'html':
dispatch(module.fetchImages({ pageNumber: 0 }));
if (data.learningContextId?.startsWith('lib:')) {
// eslint-disable-next-line no-console
console.log('Not fetching image assets - not implemented yet for content libraries.');
} else {
dispatch(module.fetchImages({ pageNumber: 0 }));
}
break;
default:
break;
Expand Down
7 changes: 6 additions & 1 deletion src/editors/data/redux/thunkActions/app.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,6 @@ describe('app thunkActions', () => {
expect(dispatch.mock.calls).toEqual([
[actions.app.initialize(testValue)],
[thunkActions.fetchBlock()],
[thunkActions.fetchUnit()],
]);
thunkActions.fetchBlock = fetchBlock;
thunkActions.fetchUnit = fetchUnit;
Expand Down Expand Up @@ -216,6 +215,8 @@ describe('app thunkActions', () => {
const data = {
...testValue,
blockType: 'html',
blockId: 'block-v1:UniversityX+PHYS+1+type@problem+block@123',
learningContextId: 'course-v1:UniversityX+PHYS+1',
};
thunkActions.initialize(data)(dispatch);
expect(dispatch.mock.calls).toEqual([
Expand Down Expand Up @@ -251,6 +252,8 @@ describe('app thunkActions', () => {
const data = {
...testValue,
blockType: 'problem',
blockId: 'block-v1:UniversityX+PHYS+1+type@problem+block@123',
learningContextId: 'course-v1:UniversityX+PHYS+1',
};
thunkActions.initialize(data)(dispatch);
expect(dispatch.mock.calls).toEqual([
Expand Down Expand Up @@ -286,6 +289,8 @@ describe('app thunkActions', () => {
const data = {
...testValue,
blockType: 'video',
blockId: 'block-v1:UniversityX+PHYS+1+type@problem+block@123',
learningContextId: 'course-v1:UniversityX+PHYS+1',
};
thunkActions.initialize(data)(dispatch);
expect(dispatch.mock.calls).toEqual([
Expand Down
6 changes: 1 addition & 5 deletions src/editors/data/services/cms/urls.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,7 @@ export const blockAncestor = ({ studioEndpointUrl, blockId }) => {
if (blockId.includes('block-v1')) {
return `${block({ studioEndpointUrl, blockId })}?fields=ancestorInfo`;
}
// this url only need to get info to build the return url, which isn't used by V2 blocks
// (temporary) don't throw error, just return empty url. it will fail it's network connection but otherwise
// the app will run
// throw new Error('Block ancestor not available (and not needed) for V2 blocks');
return '';
throw new Error('Block ancestor not available (and not needed) for V2 blocks');
};

export const blockStudioView = ({ studioEndpointUrl, blockId }) => (
Expand Down
11 changes: 3 additions & 8 deletions src/editors/data/services/cms/urls.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,9 @@ describe('cms url methods', () => {
expect(blockAncestor({ studioEndpointUrl, blockId }))
.toEqual(`${block({ studioEndpointUrl, blockId })}?fields=ancestorInfo`);
});
// This test will probably be used in the future
// it('throws error with studioEndpointUrl, v2 blockId and ancestor query', () => {
// expect(() => { blockAncestor({ studioEndpointUrl, blockId: v2BlockId }); })
// .toThrow('Block ancestor not available (and not needed) for V2 blocks');
// });
it('returns blank url with studioEndpointUrl, v2 blockId and ancestor query', () => {
expect(blockAncestor({ studioEndpointUrl, blockId: v2BlockId }))
.toEqual('');
it('throws error with studioEndpointUrl, v2 blockId and ancestor query', () => {
expect(() => { blockAncestor({ studioEndpointUrl, blockId: v2BlockId }); })
.toThrow('Block ancestor not available (and not needed) for V2 blocks');
});
});
describe('blockStudioView', () => {
Expand Down
Loading