Merge pull request #27 from open-craft/felipetrz/bb-4961-tiny-mce-improvements
feat: TinyMCE improvements [BD-38] [TNL-8779] [BB-4961]
This commit is contained in:
41
package-lock.json
generated
41
package-lock.json
generated
@@ -5694,8 +5694,7 @@
|
||||
"@types/json-schema": {
|
||||
"version": "7.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
|
||||
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ=="
|
||||
},
|
||||
"@types/json5": {
|
||||
"version": "0.0.29",
|
||||
@@ -6210,7 +6209,6 @@
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -6221,8 +6219,7 @@
|
||||
"ajv-keywords": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
|
||||
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="
|
||||
},
|
||||
"alphanum-sort": {
|
||||
"version": "1.0.2",
|
||||
@@ -7036,8 +7033,7 @@
|
||||
"big.js": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
|
||||
"integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ=="
|
||||
},
|
||||
"bin-build": {
|
||||
"version": "3.0.0",
|
||||
@@ -9351,8 +9347,7 @@
|
||||
"emojis-list": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
|
||||
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q=="
|
||||
},
|
||||
"encodeurl": {
|
||||
"version": "1.0.2",
|
||||
@@ -10387,8 +10382,7 @@
|
||||
"fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||
},
|
||||
"fast-glob": {
|
||||
"version": "3.2.7",
|
||||
@@ -10460,8 +10454,7 @@
|
||||
"fast-json-stable-stringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
|
||||
},
|
||||
"fast-levenshtein": {
|
||||
"version": "2.0.6",
|
||||
@@ -16114,8 +16107,7 @@
|
||||
"json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
|
||||
},
|
||||
"json-stable-stringify-without-jsonify": {
|
||||
"version": "1.0.1",
|
||||
@@ -16127,7 +16119,6 @@
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
|
||||
"integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
@@ -16269,7 +16260,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
@@ -16831,8 +16821,7 @@
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
},
|
||||
"mixin-deep": {
|
||||
"version": "1.3.2",
|
||||
@@ -18591,8 +18580,7 @@
|
||||
"punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
|
||||
},
|
||||
"q": {
|
||||
"version": "1.5.1",
|
||||
@@ -18656,6 +18644,15 @@
|
||||
"unpipe": "1.0.0"
|
||||
}
|
||||
},
|
||||
"raw-loader": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz",
|
||||
"integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==",
|
||||
"requires": {
|
||||
"loader-utils": "^2.0.0",
|
||||
"schema-utils": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"react": {
|
||||
"version": "16.14.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz",
|
||||
@@ -19915,7 +19912,6 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
|
||||
"integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.8",
|
||||
"ajv": "^6.12.5",
|
||||
@@ -21844,7 +21840,6 @@
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"formik": "2.2.6",
|
||||
"lodash.snakecase": "4.1.1",
|
||||
"prop-types": "15.7.2",
|
||||
"raw-loader": "4.0.2",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"react-redux": "7.2.5",
|
||||
|
||||
@@ -1,38 +1,91 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Editor } from '@tinymce/tinymce-react';
|
||||
/* eslint import/no-webpack-loader-syntax: off */
|
||||
import contentCss from 'tinymce/skins/content/default/content.min.css';
|
||||
import contentUiCss from 'tinymce/skins/ui/oxide/content.min.css';
|
||||
import { useParams } from 'react-router';
|
||||
// TinyMCE so the global var exists
|
||||
// eslint-disable-next-line no-unused-vars,import/no-extraneous-dependencies
|
||||
import tinymce from 'tinymce/tinymce';
|
||||
|
||||
import { uploadFile } from '../discussions/posts/data/api';
|
||||
|
||||
import 'tinymce/plugins/code';
|
||||
// Theme
|
||||
import 'tinymce/themes/silver';
|
||||
// Toolbar icons
|
||||
import 'tinymce/icons/default';
|
||||
// Editor styles
|
||||
import 'tinymce/skins/ui/oxide/skin.min.css';
|
||||
import 'tinymce/skins/ui/oxide/skin.css';
|
||||
// importing the plugin js.
|
||||
import 'tinymce/plugins/autolink';
|
||||
import 'tinymce/plugins/autosave';
|
||||
import 'tinymce/plugins/codesample';
|
||||
import 'tinymce/plugins/image';
|
||||
import 'tinymce/plugins/imagetools';
|
||||
import 'tinymce/plugins/link';
|
||||
import 'tinymce/plugins/lists';
|
||||
/* eslint import/no-webpack-loader-syntax: off */
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import edxBrandCss from '!!raw-loader!sass-loader!../index.scss';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import contentCss from '!!raw-loader!tinymce/skins/content/default/content.min.css';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import contentUiCss from '!!raw-loader!tinymce/skins/ui/oxide/content.min.css';
|
||||
|
||||
const setup = (editor) => {
|
||||
editor.ui.registry.addButton('openedx_code', {
|
||||
icon: 'sourcecode',
|
||||
onAction: () => {
|
||||
editor.execCommand('CodeSample');
|
||||
},
|
||||
});
|
||||
editor.ui.registry.addButton('openedx_html', {
|
||||
text: 'HTML',
|
||||
onAction: () => {
|
||||
editor.execCommand('mceCodeEditor');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default function TinyMCEEditor(props) {
|
||||
// note that skin and content_css is disabled to avoid the normal
|
||||
// loading process and is instead loaded as a string via content_style
|
||||
|
||||
const { courseId, postId } = useParams();
|
||||
|
||||
const uploadHandler = async (blobInfo, success, failure) => {
|
||||
try {
|
||||
const blob = blobInfo.blob();
|
||||
const filename = blobInfo.filename();
|
||||
const { location } = await uploadFile(blob, filename, courseId, postId || 'root');
|
||||
success(location);
|
||||
} catch (e) {
|
||||
failure(e.toString(), { remove: true });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Editor
|
||||
init={{
|
||||
skin: false,
|
||||
menubar: false,
|
||||
plugins: 'codesample link lists code',
|
||||
toolbar: 'formatselect | bold italic underline | link | bullist numlist outdent indent | code |',
|
||||
branding: false,
|
||||
contextmenu: false,
|
||||
browser_spellcheck: true,
|
||||
a11y_advanced_options: true,
|
||||
autosave_interval: '1s',
|
||||
autosave_restore_when_empty: true,
|
||||
autosave_prefix: 'tinymce-autosave-{path}{query}',
|
||||
plugins: 'autosave codesample link lists image imagetools code',
|
||||
toolbar: 'formatselect | bold italic underline'
|
||||
+ ' | link blockquote openedx_code image'
|
||||
+ ' | bullist numlist outdent indent'
|
||||
+ ' | removeformat'
|
||||
+ ' | openedx_html'
|
||||
+ ' | undo redo',
|
||||
content_css: false,
|
||||
content_style: [contentCss, contentUiCss].join('\n'),
|
||||
content_style: [contentCss, contentUiCss, edxBrandCss].join('\n'),
|
||||
images_upload_handler: uploadHandler,
|
||||
setup,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -11,6 +11,7 @@ ensureConfig([
|
||||
const apiBaseUrl = getConfig().LMS_BASE_URL;
|
||||
|
||||
export const threadsApiUrl = `${apiBaseUrl}/api/discussion/v1/threads/`;
|
||||
export const coursesApiUrl = `${apiBaseUrl}/api/discussion/v1/courses/`;
|
||||
|
||||
/**
|
||||
* Fetches all the threads in the given course and topic.
|
||||
@@ -147,3 +148,23 @@ export async function deleteThread(threadId) {
|
||||
const url = `${threadsApiUrl}${threadId}/`;
|
||||
await getAuthenticatedHttpClient().delete(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file.
|
||||
* @param {Blob} blob The file body
|
||||
* @param {string} filename
|
||||
* @param {string} courseId
|
||||
* @param {string} threadKey
|
||||
* @returns {Promise<{ location: string }>}
|
||||
*/
|
||||
export async function uploadFile(blob, filename, courseId, threadKey) {
|
||||
const uploadUrl = `${coursesApiUrl}${courseId}/upload`;
|
||||
const formData = new FormData();
|
||||
formData.append('thread_key', threadKey);
|
||||
formData.append('uploaded_file', blob, filename);
|
||||
const { data } = await getAuthenticatedHttpClient().post(uploadUrl, formData);
|
||||
if (data.developer_message) {
|
||||
throw new Error(data.developer_message);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
35
src/discussions/posts/data/api.test.js
Normal file
35
src/discussions/posts/data/api.test.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { initializeMockApp } from '@edx/frontend-platform/testing';
|
||||
|
||||
import { coursesApiUrl, uploadFile } from './api';
|
||||
|
||||
const courseId = 'course-v1:edX+TestX+Test_Course';
|
||||
|
||||
let axiosMock = null;
|
||||
|
||||
describe('Threads/Posts api tests', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
axiosMock.reset();
|
||||
});
|
||||
|
||||
test('successfully completes upload requests', async () => {
|
||||
axiosMock.onPost(`${coursesApiUrl}${courseId}/upload`)
|
||||
.reply(200, { location: 'http://test/file.jpg' });
|
||||
const response = await uploadFile(new Blob(['sample data']), 'sample_file.jpg', courseId, 'root');
|
||||
expect(response.location).toEqual('http://test/file.jpg');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user