Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import QuestionWidget from './QuestionWidget';
import EditorContainer from '../../../EditorContainer';
import RawEditor from '../../../../sharedComponents/RawEditor';
import { ProblemTypeKeys } from '../../../../data/constants/problem';
import { blockTypes } from '../../../../data/constants/app';

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

import { selectors } from '../../../../data/redux';
import { ProblemEditorContextProvider } from './ProblemEditorContext';
import { ProblemEditorPluginSlot } from '../../../../../plugin-slots/ProblemEditorPluginSlot';

const EditProblemView = ({ returnFunction }) => {
const intl = useIntl();
Expand Down Expand Up @@ -128,6 +130,7 @@ const EditProblemView = ({ returnFunction }) => {
</Container>
) : (
<span className="flex-grow-1 mb-5">
<ProblemEditorPluginSlot blockType={problemType || blockTypes.problem} />
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure if we should mix blockType and problemType in a single field? Those seem like two different fields.

Also, what do you think about making the ProblemEditorPluginSlot wrap all of these rather than being before them? For example, what if I wanted to replace the standard editor with a custom editor? If the slot wrapped them, it would be possible, while still allowing insertion before/after the contents like you want. But if the slot is only before the rest, it's less flexible.

<QuestionWidget />
<ExplanationWidget />
<AnswerWidget problemType={problemType} />
Expand Down
9 changes: 8 additions & 1 deletion src/editors/containers/TextEditor/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';

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

import EditorContainer from '../EditorContainer';
Expand All @@ -18,6 +19,7 @@ import * as hooks from './hooks';
import messages from './messages';
import TinyMceWidget from '../../sharedComponents/TinyMceWidget';
import { prepareEditorRef, replaceStaticWithAsset } from '../../sharedComponents/TinyMceWidget/hooks';
import { TextEditorPluginSlot } from '../../../plugin-slots/TextEditorPluginSlot';

const TextEditor = ({
onClose,
Expand Down Expand Up @@ -97,7 +99,12 @@ const TextEditor = ({
screenreadertext={intl.formatMessage(messages.spinnerScreenReaderText)}
/>
</div>
) : (selectEditor())}
) : (
<>
<TextEditorPluginSlot blockType={blockTypes.html} />
{selectEditor()}
</>
)}
</div>
</EditorContainer>
);
Expand Down
125 changes: 125 additions & 0 deletions src/plugin-slots/ProblemEditorPluginSlot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# ProblemEditorPluginSlot

### Slot ID: `org.openedx.frontend.authoring.problem_editor_plugin.v1`

### Slot ID Aliases
* `problem_editor_plugin_slot`

### Plugin Props:

* `blockType` - String. The type of problem block being edited (e.g., `problem-single-select`, `problem-multi-select`, `problem`, `advanced`).

## Description

The `ProblemEditorPluginSlot` is rendered inside the Problem Editor modal window for all major
problem XBlock types:

* single-select
* multi-select
* dropdown
* numerical-input
* text-input

It is a **generic extension point** that can host any React component, such as:

- **Problem authoring helpers** (validation, hints, accessibility tips)
- **Preview or analysis tools** (show how a problem will render, check grading logic)
- **Integrations** (external content sources, tagging, metadata editors)

Copy link
Contributor

Choose a reason for hiding this comment

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

Should there be screenshots after the example title?

Copy link
Author

Choose a reason for hiding this comment

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

@ihor-romaniuk tell me to remove all screenshots with our AIAssistantWidget. So, I changed this phrase and removed screenshots with our AIAssistantWidget

Copy link
Contributor

Choose a reason for hiding this comment

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

If we don’t want to mention anything about the AI Assistant, should we update the other description as well?

Your component is responsible for interacting with the editor state (if needed) using
Redux, `window.tinymce`, CodeMirror, or other utilities provided by `frontend-app-authoring`.
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be nice to add an example of how exactly it is possible to interact with the editor state.

Copy link
Author

Choose a reason for hiding this comment

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

Example added


#### Interacting with Editor State (Reading State from Redux)
Copy link
Contributor

Choose a reason for hiding this comment

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

We will need to mark this plugin slot as ⚠️ unstable, because we have an ongoing project to refactor the editors and remove redux completely - #2088 .


```jsx
import { useSelector } from 'react-redux';
import { selectors } from 'CourseAuthoring/editors/data/redux';

const MyComponent = ({ blockType }) => {
// Read problem state
const problemState = useSelector(selectors.problem.completeState);
const learningContextId = useSelector(selectors.app.learningContextId);
const showRawEditor = useSelector(selectors.app.showRawEditor);

// Access problem data
const question = problemState?.question || '';
const answers = problemState?.answers || [];

return <div>Question: {question}</div>;
};
```
## Examples
### Default content
![Problem editor with default content](./images/screenshot_default.png)
### Replaced with custom component
The following `env.config.tsx` will add a centered `h1` tag im Problem editor.
![🦶 in Problem editor slot](./images/screenshot_custom.png)
```tsx
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';

const config = {
pluginSlots: {
'org.openedx.frontend.authoring.problem_editor_plugin.v1': {
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'my-problem-editor-helper',
type: DIRECT_PLUGIN,
RenderWidget: () => (
<h1 style={{ textAlign: 'center' }}>🦶</h1>
),
},
},
]
}
},
}

export default config;
```
### Custom component with plugin props
![Paragon Alert component in Problem editor slot](./images/screenshot_with_alert.png)
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:
```jsx
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
import { Alert } from '@openedx/paragon';

const config = {
pluginSlots: {
'org.openedx.frontend.authoring.problem_editor_plugin.v1': {
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom-problem-editor-assistant',
priority: 1,
type: DIRECT_PLUGIN,
RenderWidget: ({ blockType }) => {
return (
<Alert variant="success">
<Alert.Heading>Custom component for {blockType} problem editor 🤗🤗🤗</Alert.Heading>
</Alert>
);
},
},
op: PLUGIN_OPERATIONS.Insert,
},
]
}
},
}

export default config;
```
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be good if, in the screenshots for all plugin slots, the modal dialog were centered. As far as I know, these modals are always displayed in the center anyway.
Alternatively, you could capture screenshots of just the modal itself. My main point is that all screenshots added to the documentation should look clean and presentable.

Copy link
Author

Choose a reason for hiding this comment

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

Yes, replaced with screenshots of just the modal itself

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions src/plugin-slots/ProblemEditorPluginSlot/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { PluginSlot } from '@openedx/frontend-plugin-framework';

interface ProblemEditorPluginSlotProps {
blockType: string | null;
}

export const ProblemEditorPluginSlot = ({
blockType,
}: ProblemEditorPluginSlotProps) => (
<PluginSlot
id="org.openedx.frontend.authoring.problem_editor_plugin.v1"
idAliases={['problem_editor_plugin_slot']}
pluginProps={{
blockType,
}}
/>
);
2 changes: 2 additions & 0 deletions src/plugin-slots/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
## Course Unit page
* [`org.openedx.frontend.authoring.course_unit_header_actions.v1`](./CourseUnitHeaderActionsSlot/)
* [`org.openedx.frontend.authoring.course_unit_sidebar.v1`](./CourseAuthoringUnitSidebarSlot/)
* [`org.openedx.frontend.authoring.text_editor_plugin.v1`](./TextEditorPluginSlot/)
* [`org.openedx.frontend.authoring.problem_editor_plugin.v1`](./ProblemEditorPluginSlot/)

## Other Slots
* [`org.openedx.frontend.authoring.additional_course_content_plugin.v1`](./AdditionalCourseContentPluginSlot/)
Expand Down
125 changes: 125 additions & 0 deletions src/plugin-slots/TextEditorPluginSlot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# TextEditorPluginSlot
Copy link
Contributor

Choose a reason for hiding this comment

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

Please update the documentation for this plugin slot as well, following the same approach I used for ProblemEditorPluginSlot.

Copy link
Author

Choose a reason for hiding this comment

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

Updated in accordance to ProblemEditorPluginSlot


### Slot ID: `org.openedx.frontend.authoring.text_editor_plugin.v1`

### Slot ID Aliases
* `text_editor_plugin_slot`

### Plugin Props:

* `blockType` - String. The type of block being edited (e.g., `html`).

## Description

The `TextEditorPluginSlot` is rendered inside the Text Editor modal window for HTML XBlocks.
By default, the slot is **empty**.
It is intended as a generic extension point that can host **any React component** – for example:

- **Contextual helpers** (tips, validation messages, writing guides)
- **Content utilities** (templates, reusable snippets, glossary insert tools)
- **Integrations** (linking to external systems, analytics, metadata editors)

Your component is responsible for interacting with the editor (if needed) using Redux state,
DOM APIs, or other utilities provided by `frontend-app-authoring`.
Comment on lines +22 to +23
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be nice to add an example of how exactly it is possible to interact with the editor state.

Copy link
Author

Choose a reason for hiding this comment

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

example added


#### Interacting with Editor State

```jsx
import { useSelector } from 'react-redux';
import { selectors } from 'CourseAuthoring/editors/data/redux';

const MyComponent = ({ blockType }) => {
// Read editor state
const showRawEditor = useSelector(selectors.app.showRawEditor);
const blockValue = useSelector(selectors.app.blockValue);

// Update CodeMirror (raw editor)
const updateRawContent = (content) => {
const cm = document.querySelector('.CodeMirror')?.CodeMirror;
if (cm?.dispatch) {
cm.dispatch(cm.state.update({
changes: { from: 0, to: cm.state.doc.length, insert: content }
}));
}
};

return <button onClick={() => showRawEditor ? updateRawContent('<p>New content</p>') : updateContent('<p>New content</p>')}>
Update Editor
</button>;
};
```

## Examples

### Default content

![HTML editor with default content](./images/screenshot_default.png)

### Replaced with custom component

The following `env.config.tsx` will add a centered `h1` tag im HTML editor.

![🦶 in HTML editor slot](./images/screenshot_custom.png)

```tsx
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';

const config = {
pluginSlots: {
'org.openedx.frontend.authoring.text_editor_plugin.v1': {
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'my-html-editor-helper',
type: DIRECT_PLUGIN,
RenderWidget: () => (
<h1 style={{ textAlign: 'center' }}>🦶</h1>
),
},
},
]
}
},
}

export default config;
```

### Custom component with plugin props

![Paragon Alert component in HTML editor slot](./images/screenshot_with_alert.png)

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:

```jsx
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
import { Alert } from '@openedx/paragon';

const config = {
pluginSlots: {
'org.openedx.frontend.authoring.text_editor_plugin.v1': {
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom-html-editor-assistant',
priority: 1,
type: DIRECT_PLUGIN,
RenderWidget: ({ blockType }) => {
return (
<Alert variant="success">
<Alert.Heading>Custom component for {blockType} HTML editor 🤗🤗🤗</Alert.Heading>
</Alert>
);
},
},
op: PLUGIN_OPERATIONS.Insert,
},
]
}
},
}

export default config;
```
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions src/plugin-slots/TextEditorPluginSlot/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { PluginSlot } from '@openedx/frontend-plugin-framework';

interface TextEditorPluginSlotProps {
blockType: string;
}

export const TextEditorPluginSlot = ({
blockType,
}: TextEditorPluginSlotProps) => (
<PluginSlot
id="org.openedx.frontend.authoring.text_editor_plugin.v1"
idAliases={['text_editor_plugin_slot']}
pluginProps={{
blockType,
}}
/>
);