Skip to content

Commit 2586b45

Browse files
authored
Lk/text editor (#17)
* chore: add react query dev tools * chore: add tinyMCE skins and theme from lms * feat: add text editor * chore: remove id from editor * chore: update test * chore: most of the code on tinyMCE except skins and themes * chore: remove custom default skins and themes
1 parent cda2908 commit 2586b45

File tree

14 files changed

+5755
-1271
lines changed

14 files changed

+5755
-1271
lines changed

package-lock.json

Lines changed: 5494 additions & 1143 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"@fortawesome/free-solid-svg-icons": "5.15.4",
4848
"@fortawesome/react-fontawesome": "0.2.0",
4949
"@tanstack/react-query": "^4.29.25",
50+
"@tanstack/react-query-devtools": "^4.32.1",
5051
"@tinymce/tinymce-react": "3.8.4",
5152
"classnames": "^2.3.2",
5253
"core-js": "3.31.1",

src/components/TextResponse/Editor.jsx

Lines changed: 0 additions & 105 deletions
This file was deleted.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
4+
import { Editor } from '@tinymce/tinymce-react';
5+
import 'tinymce/tinymce.min';
6+
import 'tinymce/icons/default';
7+
import 'tinymce/plugins/link';
8+
import 'tinymce/plugins/lists';
9+
import 'tinymce/plugins/code';
10+
import 'tinymce/plugins/image';
11+
import 'tinymce/themes/silver';
12+
import 'tinymce/skins/ui/oxide/skin.min.css';
13+
14+
import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils';
15+
import { useIntl } from '@edx/frontend-platform/i18n';
16+
import messages from './messages';
17+
18+
export const stateKeys = StrictDict({
19+
value: 'value',
20+
});
21+
22+
const RichTextEditor = ({
23+
// id,
24+
initialValue,
25+
disabled,
26+
optional,
27+
}) => {
28+
const [value, setValue] = useKeyedState(stateKeys.value, initialValue);
29+
const { formatMessage } = useIntl();
30+
31+
const extraConfig = disabled ? {
32+
toolbar: false,
33+
readonly: 1,
34+
} : {
35+
// eslint-disable-next-line max-len
36+
toolbar: 'formatselect | bold italic underline | link blockquote image | numlist bullist outdent indent | strikethrough | code | undo redo',
37+
};
38+
39+
return (
40+
<div className="form-group">
41+
<label htmlFor="rich-text-response">
42+
{formatMessage(messages.yourResponse)} ({formatMessage(optional ? messages.optional : messages.required)})
43+
</label>
44+
<Editor
45+
name="rich-text-response"
46+
initialValue={value}
47+
init={{
48+
menubar: false,
49+
statusbar: false,
50+
skin: false,
51+
content_css: false,
52+
height: '300',
53+
schema: 'html5',
54+
plugins: 'code image link lists',
55+
...extraConfig,
56+
}}
57+
onChange={(e) => setValue(e.target.getContent())}
58+
disabled={disabled}
59+
/>
60+
</div>
61+
);
62+
};
63+
64+
RichTextEditor.defaultProps = {
65+
disabled: false,
66+
initialValue: '',
67+
optional: false,
68+
};
69+
70+
RichTextEditor.propTypes = {
71+
// id: PropTypes.string.isRequired,
72+
input: PropTypes.shape({
73+
value: PropTypes.string,
74+
name: PropTypes.string,
75+
onChange: PropTypes.func.isRequired,
76+
}).isRequired,
77+
meta: PropTypes.shape({
78+
touched: PropTypes.bool,
79+
submitFailed: PropTypes.bool,
80+
error: PropTypes.string,
81+
}).isRequired,
82+
disabled: PropTypes.bool,
83+
initialValue: PropTypes.string,
84+
optional: PropTypes.bool,
85+
};
86+
87+
export default RichTextEditor;
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import React, { useState } from 'react';
2+
import PropTypes from 'prop-types';
3+
4+
import { TextArea } from '@edx/paragon';
5+
import { StrictDict } from '@edx/react-unit-test-utils';
6+
import { useIntl } from '@edx/frontend-platform/i18n';
7+
import messages from './messages';
8+
9+
export const stateKeys = StrictDict({
10+
value: 'value',
11+
});
12+
13+
const TextEditor = ({
14+
// id,
15+
initialValue,
16+
disabled,
17+
optional,
18+
}) => {
19+
const { formatMessage } = useIntl();
20+
const [value, setValue] = useState(initialValue);
21+
22+
return (
23+
<TextArea
24+
name="text-response"
25+
className="textarea-response"
26+
label={`
27+
${formatMessage(messages.yourResponse)} (${formatMessage(optional ? messages.optional : messages.required)})
28+
`}
29+
value={value}
30+
onChange={setValue}
31+
placeholder={formatMessage(messages.textResponsePlaceholder)}
32+
disabled={disabled}
33+
/>
34+
);
35+
};
36+
37+
TextEditor.defaultProps = {
38+
disabled: false,
39+
initialValue: '',
40+
optional: false,
41+
};
42+
43+
TextEditor.propTypes = {
44+
// id: PropTypes.string.isRequired,
45+
input: PropTypes.shape({
46+
value: PropTypes.string,
47+
name: PropTypes.string,
48+
onChange: PropTypes.func.isRequired,
49+
}).isRequired,
50+
meta: PropTypes.shape({
51+
touched: PropTypes.bool,
52+
submitFailed: PropTypes.bool,
53+
error: PropTypes.string,
54+
}).isRequired,
55+
disabled: PropTypes.bool,
56+
initialValue: PropTypes.string,
57+
optional: PropTypes.bool,
58+
};
59+
60+
export default TextEditor;

src/components/TextResponse/index.jsx

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,30 @@ import React from 'react';
22
import PropTypes from 'prop-types';
33

44
import {
5-
// useSubmissionConfig,
6-
useSubmissionResponse,
5+
useSubmissionConfig,
6+
// useSubmissionResponse,
77
// useSubmissionStatus,
88
// useSubmissionTeamInfo,
99
} from 'data/services/lms/hooks/selectors';
1010

11-
export const TextResponse = ({
12-
promptIndex,
13-
}) => {
14-
// const config = useSubmissionConfig().textResponseConfig;
15-
const response = useSubmissionResponse();
16-
// const submissionStatus = useSubmissionStatus();
17-
// const teamInfo = useSubmissionTeamInfo();
11+
import TextEditor from 'components/TextResponse/TextEditor';
12+
import RichTextEditor from 'components/TextResponse/RichTextEditor';
13+
14+
import './index.scss';
15+
16+
export const TextResponse = () => {
17+
const { textResponseConfig } = useSubmissionConfig();
18+
const { optional, enabled } = textResponseConfig;
19+
const props = {
20+
optional,
21+
disabled: !enabled,
22+
};
23+
1824
return (
19-
<div>
20-
<h3>Text Response</h3>
21-
{response.textResponses[promptIndex]}
25+
<div className="mt-2">
26+
{
27+
textResponseConfig?.editorType === 'text' ? <TextEditor {...props} /> : <RichTextEditor {...props} />
28+
}
2229
</div>
2330
);
2431
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.textarea-response {
2+
min-height: 200px;
3+
max-height: 300px;
4+
overflow-y: scroll;
5+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { defineMessages } from '@edx/frontend-platform/i18n';
2+
3+
const messages = defineMessages({
4+
textResponsePlaceholder: {
5+
defaultMessage: 'Enter your response to the prompt above',
6+
description: 'Placeholder text for the text response input field',
7+
id: 'frontend-app-ora.TextResponse.textResponsePlaceholder',
8+
},
9+
yourResponse: {
10+
defaultMessage: 'Your response',
11+
description: 'Label for the text response input field',
12+
id: 'frontend-app-ora.TextResponse.yourResponse',
13+
},
14+
required: {
15+
defaultMessage: 'Required',
16+
description: 'Label for the required indicator',
17+
id: 'frontend-app-ora.TextResponse.required',
18+
},
19+
optional: {
20+
defaultMessage: 'Optional',
21+
description: 'Label for the optional indicator',
22+
id: 'frontend-app-ora.TextResponse.optional',
23+
},
24+
});
25+
26+
export default messages;

src/data/services/lms/hooks/api.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,18 @@ describe('lms api hooks', () => {
5050
expect(out.queryKey).toEqual([queryKeys.oraConfig]);
5151
});
5252
it('initializes query with promise pointing to assessment text', async () => {
53+
const old = window.location;
54+
Object.defineProperty(window, "location", {
55+
value: new URL(`http://dummy.com/text`),
56+
writable: true,
57+
});
5358
const response = await out.queryFn();
5459
expect(response).toEqual(fakeData.oraConfig.assessmentText);
60+
window.location = old;
61+
});
62+
it('initializes query with promise pointing to assessment tinyMCE', async () => {
63+
const response = await out.queryFn();
64+
expect(response).toEqual(fakeData.oraConfig.assessmentTinyMCE);
5565
});
5666
it('returns camelCase object from data if data has been returned', () => {
5767
expect(out.data).toEqual(camelCaseObject(fakeData.oraConfig.assessmentText));

src/data/services/lms/hooks/api.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ export const useORAConfig = (): types.QueryData<types.ORAConfig> => {
1212
const { data, ...status } = useQuery({
1313
queryKey: [queryKeys.oraConfig],
1414
// queryFn: () => getAuthenticatedClient().get(...),
15-
queryFn: () => Promise.resolve(fakeData.oraConfig.assessmentText),
15+
queryFn: () => {
16+
const result = window.location.pathname.endsWith('text') ? fakeData.oraConfig.assessmentText : fakeData.oraConfig.assessmentTinyMCE;
17+
return Promise.resolve(result);
18+
},
1619
});
1720
return {
1821
...status,

0 commit comments

Comments
 (0)