Skip to content

Commit abd2d87

Browse files
author
kyrylo.kireiev
committed
feat: Implement HTML and Problem plugin slots
1 parent 67a5694 commit abd2d87

13 files changed

Lines changed: 297 additions & 1 deletion

File tree

src/editors/containers/ProblemEditor/components/EditProblemView/index.jsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import QuestionWidget from './QuestionWidget';
1717
import EditorContainer from '../../../EditorContainer';
1818
import RawEditor from '../../../../sharedComponents/RawEditor';
1919
import { ProblemTypeKeys } from '../../../../data/constants/problem';
20+
import { blockTypes } from '../../../../data/constants/app';
2021

2122
import {
2223
checkIfEditorsDirty, parseState, saveWarningModalToggle, getContent,
@@ -29,6 +30,7 @@ import { saveBlock } from '../../../../hooks';
2930

3031
import { selectors } from '../../../../data/redux';
3132
import { ProblemEditorContextProvider } from './ProblemEditorContext';
33+
import { ProblemEditorPluginSlot } from '../../../../../plugin-slots/ProblemEditorPluginSlot';
3234

3335
const EditProblemView = ({ returnFunction }) => {
3436
const intl = useIntl();
@@ -128,6 +130,7 @@ const EditProblemView = ({ returnFunction }) => {
128130
</Container>
129131
) : (
130132
<span className="flex-grow-1 mb-5">
133+
<ProblemEditorPluginSlot blockType={problemType || blockTypes.problem} />
131134
<QuestionWidget />
132135
<ExplanationWidget />
133136
<AnswerWidget problemType={problemType} />

src/editors/containers/TextEditor/index.jsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
1010

1111
import { getConfig } from '@edx/frontend-platform';
1212
import { actions, selectors } from '../../data/redux';
13+
import { blockTypes } from '../../data/constants/app';
1314
import { RequestKeys } from '../../data/constants/requests';
1415

1516
import EditorContainer from '../EditorContainer';
@@ -18,6 +19,7 @@ import * as hooks from './hooks';
1819
import messages from './messages';
1920
import TinyMceWidget from '../../sharedComponents/TinyMceWidget';
2021
import { prepareEditorRef, replaceStaticWithAsset } from '../../sharedComponents/TinyMceWidget/hooks';
22+
import { TextEditorPluginSlot } from '../../../plugin-slots/TextEditorPluginSlot';
2123

2224
const TextEditor = ({
2325
onClose,
@@ -97,7 +99,12 @@ const TextEditor = ({
9799
screenreadertext={intl.formatMessage(messages.spinnerScreenReaderText)}
98100
/>
99101
</div>
100-
) : (selectEditor())}
102+
) : (
103+
<>
104+
<TextEditorPluginSlot blockType={blockTypes.html} />
105+
{selectEditor()}
106+
</>
107+
)}
101108
</div>
102109
</EditorContainer>
103110
);
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# ProblemEditorPluginSlot
2+
3+
### Slot ID: `org.openedx.frontend.authoring.problem_editor_plugin.v1`
4+
5+
### Slot ID Aliases
6+
* `problem_editor_plugin_slot`
7+
8+
### Plugin Props:
9+
10+
* `blockType` - String. The type of problem block being edited (e.g., `problem-single-select`, `problem-multi-select`, `problem`, `advanced`).
11+
12+
## Description
13+
14+
The `ProblemEditorPluginSlot` is rendered inside the Problem Editor modal window for all major
15+
problem XBlock types:
16+
17+
* single-select
18+
* multi-select
19+
* dropdown
20+
* numerical-input
21+
* text-input
22+
23+
It is a **generic extension point** that can host any React component, such as:
24+
25+
- **Problem authoring helpers** (validation, hints, accessibility tips)
26+
- **Preview or analysis tools** (show how a problem will render, check grading logic)
27+
- **Integrations** (external content sources, tagging, metadata editors)
28+
29+
Your component is responsible for interacting with the editor state (if needed) using
30+
Redux, `window.tinymce`, CodeMirror, or other utilities provided by `frontend-app-authoring`.
31+
32+
#### Interacting with Editor State (Reading State from Redux)
33+
34+
```jsx
35+
import { useSelector } from 'react-redux';
36+
import { selectors } from 'CourseAuthoring/editors/data/redux';
37+
38+
const MyComponent = ({ blockType }) => {
39+
// Read problem state
40+
const problemState = useSelector(selectors.problem.completeState);
41+
const learningContextId = useSelector(selectors.app.learningContextId);
42+
const showRawEditor = useSelector(selectors.app.showRawEditor);
43+
44+
// Access problem data
45+
const question = problemState?.question || '';
46+
const answers = problemState?.answers || [];
47+
48+
return <div>Question: {question}</div>;
49+
};
50+
```
51+
52+
## Examples
53+
54+
### Default content
55+
56+
![Problem editor with default content](./images/screenshot_default.png)
57+
58+
### Replaced with custom component
59+
60+
The following `env.config.tsx` will add a centered `h1` tag im Problem editor.
61+
62+
![🦶 in Problem editor slot](./images/screenshot_custom.png)
63+
64+
```tsx
65+
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
66+
67+
const config = {
68+
pluginSlots: {
69+
'org.openedx.frontend.authoring.problem_editor_plugin.v1': {
70+
plugins: [
71+
{
72+
op: PLUGIN_OPERATIONS.Insert,
73+
widget: {
74+
id: 'my-problem-editor-helper',
75+
type: DIRECT_PLUGIN,
76+
RenderWidget: () => (
77+
<h1 style={{ textAlign: 'center' }}>🦶</h1>
78+
),
79+
},
80+
},
81+
]
82+
}
83+
},
84+
}
85+
86+
export default config;
87+
```
88+
89+
### Custom component with plugin props
90+
91+
![Paragon Alert component in Problem editor slot](./images/screenshot_with_alert.png)
92+
93+
The following `env.config.tsx` example demonstrates how to add a custom component to the Problem Editor plugin slot that receives the plugin props. The example shows a Paragon Alert component that renders the current `blockType` provided by the slot:
94+
95+
```jsx
96+
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
97+
import { Alert } from '@openedx/paragon';
98+
99+
const config = {
100+
pluginSlots: {
101+
'org.openedx.frontend.authoring.problem_editor_plugin.v1': {
102+
plugins: [
103+
{
104+
op: PLUGIN_OPERATIONS.Insert,
105+
widget: {
106+
id: 'custom-problem-editor-assistant',
107+
priority: 1,
108+
type: DIRECT_PLUGIN,
109+
RenderWidget: ({ blockType }) => {
110+
return (
111+
<Alert variant="success">
112+
<Alert.Heading>Custom component for {blockType} problem editor 🤗🤗🤗</Alert.Heading>
113+
</Alert>
114+
);
115+
},
116+
},
117+
op: PLUGIN_OPERATIONS.Insert,
118+
},
119+
]
120+
}
121+
},
122+
}
123+
124+
export default config;
125+
```
141 KB
Loading
140 KB
Loading
148 KB
Loading
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { PluginSlot } from '@openedx/frontend-plugin-framework';
2+
3+
interface ProblemEditorPluginSlotProps {
4+
blockType: string | null;
5+
}
6+
7+
export const ProblemEditorPluginSlot = ({
8+
blockType,
9+
}: ProblemEditorPluginSlotProps) => (
10+
<PluginSlot
11+
id="org.openedx.frontend.authoring.problem_editor_plugin.v1"
12+
idAliases={['problem_editor_plugin_slot']}
13+
pluginProps={{
14+
blockType,
15+
}}
16+
/>
17+
);

src/plugin-slots/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
## Course Unit page
1010
* [`org.openedx.frontend.authoring.course_unit_header_actions.v1`](./CourseUnitHeaderActionsSlot/)
1111
* [`org.openedx.frontend.authoring.course_unit_sidebar.v1`](./CourseAuthoringUnitSidebarSlot/)
12+
* [`org.openedx.frontend.authoring.text_editor_plugin.v1`](./TextEditorPluginSlot/)
13+
* [`org.openedx.frontend.authoring.problem_editor_plugin.v1`](./ProblemEditorPluginSlot/)
1214

1315
## Other Slots
1416
* [`org.openedx.frontend.authoring.additional_course_content_plugin.v1`](./AdditionalCourseContentPluginSlot/)
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# TextEditorPluginSlot
2+
3+
### Slot ID: `org.openedx.frontend.authoring.text_editor_plugin.v1`
4+
5+
### Slot ID Aliases
6+
* `text_editor_plugin_slot`
7+
8+
### Plugin Props:
9+
10+
* `blockType` - String. The type of block being edited (e.g., `html`).
11+
12+
## Description
13+
14+
The `TextEditorPluginSlot` is rendered inside the Text Editor modal window for HTML XBlocks.
15+
By default, the slot is **empty**.
16+
It is intended as a generic extension point that can host **any React component** – for example:
17+
18+
- **Contextual helpers** (tips, validation messages, writing guides)
19+
- **Content utilities** (templates, reusable snippets, glossary insert tools)
20+
- **Integrations** (linking to external systems, analytics, metadata editors)
21+
22+
Your component is responsible for interacting with the editor (if needed) using Redux state,
23+
DOM APIs, or other utilities provided by `frontend-app-authoring`.
24+
25+
#### Interacting with Editor State
26+
27+
```jsx
28+
import { useSelector } from 'react-redux';
29+
import { selectors } from 'CourseAuthoring/editors/data/redux';
30+
31+
const MyComponent = ({ blockType }) => {
32+
// Read editor state
33+
const showRawEditor = useSelector(selectors.app.showRawEditor);
34+
const blockValue = useSelector(selectors.app.blockValue);
35+
36+
// Update CodeMirror (raw editor)
37+
const updateRawContent = (content) => {
38+
const cm = document.querySelector('.CodeMirror')?.CodeMirror;
39+
if (cm?.dispatch) {
40+
cm.dispatch(cm.state.update({
41+
changes: { from: 0, to: cm.state.doc.length, insert: content }
42+
}));
43+
}
44+
};
45+
46+
return <button onClick={() => showRawEditor ? updateRawContent('<p>New content</p>') : updateContent('<p>New content</p>')}>
47+
Update Editor
48+
</button>;
49+
};
50+
```
51+
52+
## Examples
53+
54+
### Default content
55+
56+
![HTML editor with default content](./images/screenshot_default.png)
57+
58+
### Replaced with custom component
59+
60+
The following `env.config.tsx` will add a centered `h1` tag im HTML editor.
61+
62+
![🦶 in HTML editor slot](./images/screenshot_custom.png)
63+
64+
```tsx
65+
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
66+
67+
const config = {
68+
pluginSlots: {
69+
'org.openedx.frontend.authoring.text_editor_plugin.v1': {
70+
plugins: [
71+
{
72+
op: PLUGIN_OPERATIONS.Insert,
73+
widget: {
74+
id: 'my-html-editor-helper',
75+
type: DIRECT_PLUGIN,
76+
RenderWidget: () => (
77+
<h1 style={{ textAlign: 'center' }}>🦶</h1>
78+
),
79+
},
80+
},
81+
]
82+
}
83+
},
84+
}
85+
86+
export default config;
87+
```
88+
89+
### Custom component with plugin props
90+
91+
![Paragon Alert component in HTML editor slot](./images/screenshot_with_alert.png)
92+
93+
The following `env.config.tsx` example demonstrates how to add a custom component to the HTML Editor plugin slot that receives the plugin props. The example shows a Paragon Alert component that renders the current `blockType` provided by the slot:
94+
95+
```jsx
96+
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
97+
import { Alert } from '@openedx/paragon';
98+
99+
const config = {
100+
pluginSlots: {
101+
'org.openedx.frontend.authoring.text_editor_plugin.v1': {
102+
plugins: [
103+
{
104+
op: PLUGIN_OPERATIONS.Insert,
105+
widget: {
106+
id: 'custom-html-editor-assistant',
107+
priority: 1,
108+
type: DIRECT_PLUGIN,
109+
RenderWidget: ({ blockType }) => {
110+
return (
111+
<Alert variant="success">
112+
<Alert.Heading>Custom component for {blockType} HTML editor 🤗🤗🤗</Alert.Heading>
113+
</Alert>
114+
);
115+
},
116+
},
117+
op: PLUGIN_OPERATIONS.Insert,
118+
},
119+
]
120+
}
121+
},
122+
}
123+
124+
export default config;
125+
```
96.8 KB
Loading

0 commit comments

Comments
 (0)