Skip to content
Closed
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} />
<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)

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`.

#### Interacting with Editor State (Reading State from Redux)

```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;
```
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

### 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`.

#### 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,
}}
/>
);