feat: add autocreate new xblock (#67)
This PR adds an automated script for generating the boilerplate code for adding a new xblock editor. As well as documentation for how to do so.
This commit is contained in:
72
README.md
72
README.md
@@ -1,12 +1,64 @@
|
||||
# frontend-lib-content-components
|
||||
A library of high-level components for content handling (viewing, editing, etc. of HTML, video, problems, etc.), to be shared by multiple MFEs.
|
||||
|
||||
# How to develop this package
|
||||
There are to distinct way to observe your changes. One can either either require your MFE to pull in its javascript from a local version of this package, or simply run the gallery view.
|
||||
# How to setup development of V2 content Editors for this Package for use in Studio and course Authoring MFE.
|
||||
|
||||
# Using your prexisting MFE and moduleconfig.js
|
||||
follow the below guide:
|
||||
https://github.com/openedx/frontend-build#local-module-configuration-for-webpack
|
||||
This guide presumes you have a functioning devstack.
|
||||
|
||||
1. Enable Studio to use an editor for your xblock using waffle flags
|
||||
1. Add the string name of your editor e.g. `html` to the flag check at line 3976 of `edx-platform/common/lib/xmodule/xmodule/js/common_static/bundles/js/factories/container.js`
|
||||
2. In devstack + venv, run `$make dev up frontend-app-course-authoring` to make up the required services
|
||||
3. run `$make dev.shell.lms` to enter the lms shell, then run `$paver update_assets lms` to update the static assets. This could take a minute.
|
||||
4. In http://localhost:18000/admin/waffle/flag/ turn on `new_core_editors.use_new_text_editor`
|
||||
5. refresh the studio page.
|
||||
2. clone this repo into src directory the sibling repo of your edx devstack `/src`.
|
||||
3. In the course authoring app, follow the guide to use your local verison of frontend-lib-content-components. https://github.com/openedx/frontend-build#local-module-configuration-for-webpack. your moduleconfig.js will look like something like this:
|
||||
|
||||
```
|
||||
module.exports = {
|
||||
/*
|
||||
Modules you want to use from local source code. Adding a module here means that when this app
|
||||
runs its build, it'll resolve the source from peer directories of this app.
|
||||
|
||||
moduleName: the name you use to import code from the module.
|
||||
dir: The relative path to the module's source code.
|
||||
dist: The sub-directory of the source code where it puts its build artifact. Often "dist".
|
||||
|
||||
To use a module config:
|
||||
|
||||
1. Copy module.config.js.example and remove the '.example' extension
|
||||
2. Uncomment modules below in the localModules array to load them from local source code.
|
||||
3. Remember to re-build the production builds of those local modules if they have one.
|
||||
See note below.
|
||||
*/
|
||||
localModules: [
|
||||
/*********************************************************************************************
|
||||
IMPORTANT NOTE: If any of the below packages (like paragon or frontend-platform) have a build
|
||||
step that populates their 'dist' directories, you must manually run that step. For paragon
|
||||
and frontend-platform, for instance, you need to run `npm run build` in the repo before
|
||||
module.config.js will work.
|
||||
**********************************************************************************************/
|
||||
|
||||
// { moduleName: '@edx/brand', dir: '../brand-openedx' }, // replace with your brand checkout
|
||||
// { moduleName: '@edx/paragon/scss/core', dir: '../paragon', dist: 'scss/core' },
|
||||
// { moduleName: '@edx/paragon/icons', dir: '../paragon', dist: 'icons' },
|
||||
// { moduleName: '@edx/paragon', dir: '../paragon', dist: 'dist' },
|
||||
// { moduleName: '@edx/frontend-platform', dir: '../frontend-platform', dist: 'dist' },
|
||||
{ moduleName: '@edx/frontend-lib-content-components', dir: '../../src/frontend-lib-content-components', dist: 'dist' },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
4. open a terminal at the folder you just cloned in, frontend-lib-content-components.
|
||||
1. run `$ npm install`
|
||||
2. run `$ npm run-script-build` when you want to see your changes.
|
||||
|
||||
# Add A New Xblock Editor
|
||||
1. run `$ npm run-script addXblock <yourxblock string name ex: html>`
|
||||
2. edit your componentry at `src/frontend-lib-content-components/src/editors/containers`
|
||||
|
||||
5. Now, after a `make dev.down` and `make dev.up` for your devstack services, you should be able to realize your changes.
|
||||
|
||||
# Using the gallery view.
|
||||
The gallery view runs the editor components with mocked out block data, and sometimes does not replicate all desired behaviors, but can be used for faster iteration on UI related changes. To run the gallery view, from the root directory, run
|
||||
@@ -14,4 +66,12 @@ The gallery view runs the editor components with mocked out block data, and some
|
||||
$ cd www
|
||||
$ npm start
|
||||
|
||||
and now the gallery will be live at http://localhost:8080/index.html. use the toggle at the top to switch between availible editors.
|
||||
and now the gallery will be live at http://localhost:8080/index.html. use the toggle at the top to switch between availible editors.
|
||||
|
||||
# Creating Your own editor
|
||||
If you wish to make your own editor, to being coding the editor, simply run the command
|
||||
|
||||
$ npm run-script addXblock <name of xblock>
|
||||
|
||||
from the frontend-lib-content-components source directory. It will create an editor you can then veiw at src/editors/containers. It will also configure the editor to be viewable in the gallery view. Adding the editor to be used in studio will require the following steps:
|
||||
|
||||
|
||||
75
addXblock.js
Normal file
75
addXblock.js
Normal file
@@ -0,0 +1,75 @@
|
||||
// A script to create a new xblock editor in this repo.
|
||||
/* eslint no-console: 0 */
|
||||
|
||||
// xblock name is is the third argument after node and fedx-scripts
|
||||
const xblockName = process.argv[2];
|
||||
|
||||
// I. Create Editor Files
|
||||
const fs = require('fs');
|
||||
|
||||
const filepath = `src/editors/containers/${xblockName}Editor/index.jsx`;
|
||||
const contents = fs.readFileSync('./src/example.jsx');
|
||||
|
||||
fs.mkdir(`src/editors/containers/${xblockName}Editor/`, { recursive: true }, (err) => {
|
||||
if (err) { throw err; }
|
||||
});
|
||||
|
||||
fs.writeFile(filepath, contents, (err) => {
|
||||
if (err) { throw err; } else {
|
||||
console.log(`Editor is created successfully at ${filepath}`);
|
||||
}
|
||||
});
|
||||
|
||||
const openFileToArray = (filename) => {
|
||||
const content = fs.readFileSync(filename, 'utf8');
|
||||
return content.split('\n');
|
||||
};
|
||||
|
||||
const WriteIntoFile = (path, target, addition) => {
|
||||
const contentArray = openFileToArray(path);
|
||||
|
||||
contentArray.every((value, index) => {
|
||||
if (value.includes(addition)) {
|
||||
return true; // don't add the message in again.p
|
||||
}
|
||||
if (value.includes(target)) {
|
||||
contentArray.splice(index, 0, `${addition}\n`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const newContent = contentArray.join('\n');
|
||||
fs.writeFileSync(path, newContent, (err) => {
|
||||
if (err) { throw err; } else {
|
||||
console.log(`Editor is created successfully at ${filepath}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// II. Update Constants
|
||||
// Add a new line at line 5 src/frontend-lib-content-components/src/editors/data/constants/app.js
|
||||
// with <name>: '<name>',
|
||||
const tag = 'ADDED_EDITORS';
|
||||
WriteIntoFile(
|
||||
'src/editors/data/constants/app.js',
|
||||
tag,
|
||||
` ${xblockName}: '${xblockName}',`,
|
||||
);
|
||||
|
||||
const importTag = 'ADDED_EDITOR_IMPORTS';
|
||||
WriteIntoFile(
|
||||
'src/editors/supportedEditors.js',
|
||||
importTag,
|
||||
`import ${xblockName}Editor from './containers/${xblockName}Editor'`,
|
||||
);
|
||||
|
||||
const useTag = 'ADDED_EDITORS';
|
||||
WriteIntoFile(
|
||||
'src/editors/supportedEditors.js',
|
||||
useTag,
|
||||
` [blockTypes.${xblockName}]: ${xblockName}Editor,`,
|
||||
);
|
||||
|
||||
// Add to src/frontend-lib-content-components/src/editors/supportedEditors.js
|
||||
// at line 0 add: import <name>Editor from './containers/<name>Editor';
|
||||
// add at 5th to last line: [blockTypes.<name>]: <name>Editor,
|
||||
@@ -12,7 +12,8 @@
|
||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"test": "fedx-scripts jest --coverage"
|
||||
"test": "fedx-scripts jest --coverage",
|
||||
"addXblock": "node addXblock"
|
||||
},
|
||||
"files": [
|
||||
"/dist"
|
||||
|
||||
@@ -3,20 +3,10 @@ import { useDispatch } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { blockTypes } from './data/constants/app';
|
||||
|
||||
import TextEditor from './containers/TextEditor';
|
||||
import VideoEditor from './containers/VideoEditor';
|
||||
import ProblemEditor from './containers/ProblemEditor/ProblemEditor';
|
||||
|
||||
import messages from './messages';
|
||||
import * as hooks from './hooks';
|
||||
|
||||
export const supportedEditors = {
|
||||
[blockTypes.html]: TextEditor,
|
||||
[blockTypes.video]: VideoEditor,
|
||||
[blockTypes.problem]: ProblemEditor,
|
||||
};
|
||||
import supportedEditors from './supportedEditors';
|
||||
|
||||
export const Editor = ({
|
||||
courseId,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Editor, supportedEditors } from './Editor';
|
||||
import { Editor } from './Editor';
|
||||
import supportedEditors from './supportedEditors';
|
||||
import * as hooks from './hooks';
|
||||
import { blockTypes } from './data/constants/app';
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.pgn__modal-layer {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ describe('EditorContainer component', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(<EditorContainer {...props}>{testContent}</EditorContainer>);
|
||||
});
|
||||
|
||||
test('close behavior is linked to modal onClose', () => {
|
||||
const expected = hooks.handleCancelClicked({ onClose: props.onClose });
|
||||
expect(el.find(IconButton)
|
||||
|
||||
@@ -49,6 +49,7 @@ export const editorConfig = ({
|
||||
},
|
||||
initialValue: blockValue ? blockValue.data.data : '',
|
||||
init: {
|
||||
|
||||
setup: module.setupCustomBehavior({ openModal, setImage: setSelection }),
|
||||
plugins: pluginConfig.plugins,
|
||||
imagetools_toolbar: pluginConfig.imageToolbar,
|
||||
|
||||
@@ -117,6 +117,7 @@ describe('TextEditor hooks', () => {
|
||||
// Commented out as we investigate whether this is only needed for image proxy
|
||||
// expect(output.init.imagetools_cors_hosts).toMatchObject([props.lmsEndpointUrl]);
|
||||
});
|
||||
|
||||
it('calls setupCustomBehavior on setup', () => {
|
||||
expect(output.init.setup).toEqual(
|
||||
setupCustomBehavior({ openModal: props.openModal, setImage: props.setSelection }),
|
||||
|
||||
@@ -5,4 +5,5 @@ export const blockTypes = StrictDict({
|
||||
html: 'html',
|
||||
video: 'video',
|
||||
problem: 'problem',
|
||||
// ADDED_EDITORS GO BELOW
|
||||
});
|
||||
|
||||
16
src/editors/supportedEditors.js
Normal file
16
src/editors/supportedEditors.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import TextEditor from './containers/TextEditor';
|
||||
import VideoEditor from './containers/VideoEditor';
|
||||
import ProblemEditor from './containers/ProblemEditor/ProblemEditor';
|
||||
|
||||
// ADDED_EDITOR_IMPORTS GO HERE
|
||||
|
||||
import { blockTypes } from './data/constants/app';
|
||||
|
||||
const supportedEditors = {
|
||||
[blockTypes.html]: TextEditor,
|
||||
[blockTypes.video]: VideoEditor,
|
||||
[blockTypes.problem]: ProblemEditor,
|
||||
// ADDED_EDITORS GO BELOW
|
||||
};
|
||||
|
||||
export default supportedEditors;
|
||||
95
src/example.jsx
Normal file
95
src/example.jsx
Normal file
@@ -0,0 +1,95 @@
|
||||
/* eslint-disable import/extensions */
|
||||
/* eslint-disable import/no-unresolved */
|
||||
/**
|
||||
* This is an example component for an xblock Editor
|
||||
* It uses pre-existing components to handle the saving of a the result of a function into the xblock's data.
|
||||
* To use run npm run-script addXblock <your>
|
||||
*/
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Spinner } from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import EditorContainer from '../EditorContainer';
|
||||
import * as module from '.';
|
||||
import { actions, selectors } from '../../data/redux';
|
||||
import { RequestKeys } from '../../data/constants/requests';
|
||||
|
||||
export const hooks = {
|
||||
getContent: () => ({
|
||||
some: 'content',
|
||||
}),
|
||||
};
|
||||
|
||||
export const thumbEditor = ({
|
||||
onClose,
|
||||
// redux
|
||||
blockValue,
|
||||
lmsEndpointUrl,
|
||||
blockFailed,
|
||||
blockFinished,
|
||||
initializeEditor,
|
||||
// inject
|
||||
intl,
|
||||
}) => (
|
||||
<EditorContainer
|
||||
getContent={module.hooks.getContent}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="editor-body h-75 overflow-auto">
|
||||
{!blockFinished
|
||||
? (
|
||||
<div className="text-center p-6">
|
||||
<Spinner
|
||||
animation="border"
|
||||
className="m-3"
|
||||
// Use a messages.js file for intl messages.
|
||||
screenreadertext={intl.formatMessage('Loading Spinner')}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<p>
|
||||
Your Editor Goes here.
|
||||
You can get at the xblock data with the blockValue field.
|
||||
here is what is in your xblock: {JSON.stringify(blockValue)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</EditorContainer>
|
||||
);
|
||||
thumbEditor.defaultProps = {
|
||||
blockValue: null,
|
||||
lmsEndpointUrl: null,
|
||||
};
|
||||
thumbEditor.propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
// redux
|
||||
blockValue: PropTypes.shape({
|
||||
data: PropTypes.shape({ data: PropTypes.string }),
|
||||
}),
|
||||
lmsEndpointUrl: PropTypes.string,
|
||||
blockFailed: PropTypes.bool.isRequired,
|
||||
blockFinished: PropTypes.bool.isRequired,
|
||||
initializeEditor: PropTypes.func.isRequired,
|
||||
// inject
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
blockValue: selectors.app.blockValue(state),
|
||||
lmsEndpointUrl: selectors.app.lmsEndpointUrl(state),
|
||||
blockFailed: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchBlock }),
|
||||
blockFinished: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchBlock }),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
initializeEditor: actions.app.initializeEditor,
|
||||
};
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(thumbEditor));
|
||||
@@ -26,9 +26,10 @@ export const EditorGallery = () => {
|
||||
onChange={handleChange}
|
||||
value={blockType}
|
||||
>
|
||||
<Form.Radio value="html">Text</Form.Radio>
|
||||
<Form.Radio value="video">Video</Form.Radio>
|
||||
<Form.Radio value="problem">Problem</Form.Radio>
|
||||
{ Object.values(blockTypes).map((e) => (
|
||||
<Form.Radio value={e}> {e} </Form.Radio>
|
||||
))}
|
||||
|
||||
</Form.RadioSet>
|
||||
</Form.Group>
|
||||
<EditorPage
|
||||
|
||||
Reference in New Issue
Block a user