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} />
<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
122 changes: 122 additions & 0 deletions src/plugin-slots/ProblemEditorPluginSlot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# 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
Comment on lines +17 to +21
Copy link
Contributor

Choose a reason for hiding this comment

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

According to https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md004.md

Suggested change
- single-select
- multi-select
- dropdown
- numerical-input
- text-input
* 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?

By default, the slot is **empty**. Widgets are attached via `env.config.jsx` using the
`@openedx/frontend-plugin-framework`.
Comment on lines +29 to +30
Copy link
Contributor

Choose a reason for hiding this comment

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

Interaction with the slot is not limited to env.config.jsx. Let’s remove this sentence.


The only prop your component receives from the slot is:

- `blockType` – the current problem block type.
Comment on lines +32 to +34
Copy link
Contributor

Choose a reason for hiding this comment

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

[clarify]: Given that there is already a “Plugin Props” section, do we need to explicitly list the plugin props for this plugin slot again?


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

The slot is available in the **visual editor** mode. Advanced / raw editing is handled
by the raw editor, and your component can still interact with the underlying state
if it chooses to.
Comment on lines +36 to +41
Copy link
Contributor

Choose a reason for hiding this comment

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

To me, this looks like unnecessary information that can be removed


## Example: Adding a component into `ProblemEditorPluginSlot`

The following example configuration shows how to add a custom widget to the slot:

```jsx
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
import { MyProblemEditorHelper } from '@example/my-problem-editor-helper';

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,
priority: 1,
RenderWidget: MyProblemEditorHelper,
},
},
]
}
},
}

export default config;
Comment on lines +47 to +69
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we requiring users to create MyProblemEditorHelper just to see how the plugin slot works?
Can we instead use a more generic, commonly used example to demonstrate where this plugin slot is integrated and how it behaves?

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;

A complete example might look like this:

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

```

## Example: Custom Implementation
Copy link
Contributor

Choose a reason for hiding this comment

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

In this example, it seems to me that the Paragon Card component is not the best choice for this. Let’s do it the following way instead:

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


The following example shows a minimal helper component that uses `blockType`:

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

const CustomProblemAssistant = ({ blockType }) => {
// Your custom implementation (example)
return (
<Card>
<Card.Body>
Custom component for {blockType} problem editor 🤗🤗🤗
</Card.Body>
</Card>
);
};

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: CustomProblemAssistant,
},
op: PLUGIN_OPERATIONS.Insert,
},
]
}
},
}

export default config;
```

### Example: Screenshots
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe I already mentioned that the images should be placed under the heading/description of the plugin slot use cases. Let’s remove this.


**Default problem editor without a widget**

![Screenshot with default Problem Editor](./images/default_problem_editor.png)

**With a widget rendered in the slot**

![Screenshot with component in ProblemEditorPluginSlot](./images/problem_editor_slot.png)

Copy link
Contributor

Choose a reason for hiding this comment

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

So, after the refactoring, the documentation for this plugin slot could look like the following

# 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
## Examples

### Default content

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

### Replaced with custom component

The following `env.config.jsx` will add a centered `h1` tag im Problem editor.

```jsx
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.jsx` 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.
18 changes: 18 additions & 0 deletions src/plugin-slots/ProblemEditorPluginSlot/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { PluginSlot } from '@openedx/frontend-plugin-framework/dist';
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
import { PluginSlot } from '@openedx/frontend-plugin-framework/dist';
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,
}}
/>
);

Check failure on line 18 in src/plugin-slots/ProblemEditorPluginSlot/index.tsx

View workflow job for this annotation

GitHub Actions / tests

Too many blank lines at the end of file. Max of 0 allowed
Copy link
Contributor

Choose a reason for hiding this comment

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

Too many blank lines at the end of file. Max of 0 allowed

108 changes: 108 additions & 0 deletions src/plugin-slots/TextEditorPluginSlot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# 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.


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

By default, the slot is **empty**. Widgets are attached via `env.config.jsx` using the
`@openedx/frontend-plugin-framework`.

The only prop your component receives from the slot is:

- `blockType` – the current editor block type (for this slot it will typically be `html`).

Your component is responsible for interacting with the editor (if needed) using Redux state,
DOM APIs, or other utilities provided by `frontend-app-authoring`.

## Example: Adding a component into `TextEditorPluginSlot`

The following example configuration shows how to add a custom widget to the slot:

```jsx
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
import { MyTextEditorHelper } from '@example/my-text-editor-helper';

const config = {
pluginSlots: {
'org.openedx.frontend.authoring.text_editor_plugin.v1': {
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'my-text-editor-helper',
type: DIRECT_PLUGIN,
priority: 1,
RenderWidget: MyTextEditorHelper,
},
},
]
}
},
}

export default config;
```

## Example: Custom Implementation

The following example shows a minimal helper component that uses `blockType`:

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

const CustomTextEditorWidget = ({ blockType }) => {
// Your custom implementation (example)
return (
<Card>
<Card.Body>
Custom widget for {blockType} editor 🤗🤗🤗
</Card.Body>
</Card>
);
};

const config = {
pluginSlots: {
'org.openedx.frontend.authoring.text_editor_plugin.v1': {
plugins: [
{
widget: {
id: 'custom-text-editor-widget',
priority: 1,
type: DIRECT_PLUGIN,
RenderWidget: CustomTextEditorWidget,
},
op: PLUGIN_OPERATIONS.Insert,
},
]
}
},
}

export default config;
```

### Example: Screenshots

**With a widget rendered in the slot**

![Screenshot with component in TextEditorPluginSlot](./images/html_editor_slot.png)

**Default HTML editor without a widget**

![Screenshot with default HTML editor](./images/default_html_editor.png)
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.
19 changes: 19 additions & 0 deletions src/plugin-slots/TextEditorPluginSlot/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { PluginSlot } from '@openedx/frontend-plugin-framework/dist';
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
import { PluginSlot } from '@openedx/frontend-plugin-framework/dist';
import { PluginSlot } from '@openedx/frontend-plugin-framework';


interface TextEditorPluginSlotProps {
blockType: string;
}


Check failure on line 7 in src/plugin-slots/TextEditorPluginSlot/index.tsx

View workflow job for this annotation

GitHub Actions / tests

More than 1 blank line not allowed
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need this extra space?

Copy link
Author

Choose a reason for hiding this comment

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

removed

Copy link
Contributor

Choose a reason for hiding this comment

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

More than 1 blank line not allowed

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

Check failure on line 19 in src/plugin-slots/TextEditorPluginSlot/index.tsx

View workflow job for this annotation

GitHub Actions / tests

Too many blank lines at the end of file. Max of 0 allowed
Copy link
Contributor

Choose a reason for hiding this comment

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

Too many blank lines at the end of file. Max of 0 allowed

Loading