-
Notifications
You must be signed in to change notification settings - Fork 178
feat: Add HTML and Problem editors plugin slots #2749
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
2df1ba6
e3745e9
8d74dd7
9271857
4b782e7
1ea6e4c
a31aa94
108d744
749dd21
e0df73b
d8900b3
b12439d
ea61e5b
9bc95b9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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) | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should there be screenshots after the example title?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ihor-romaniuk tell me to remove all screenshots with our
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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`. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Example added |
||
|
|
||
| #### Interacting with Editor State (Reading State from Redux) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We will need to mark this plugin slot as |
||
|
|
||
| ```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 | ||
|  | ||
| ### Replaced with custom component | ||
| The following `env.config.tsx` will add a centered `h1` tag im Problem editor. | ||
|  | ||
| ```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 | ||
|  | ||
| 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; | ||
| ``` | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, replaced with screenshots of just the modal itself |
| 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, | ||
| }} | ||
| /> | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| # TextEditorPluginSlot | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated in accordance to |
||
|
|
||
| ### 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
||
|  | ||
|
|
||
| ### Replaced with custom component | ||
|
|
||
| The following `env.config.tsx` will add a centered `h1` tag im HTML editor. | ||
|
|
||
|  | ||
|
|
||
| ```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 | ||
|
|
||
|  | ||
|
|
||
| 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; | ||
| ``` | ||
| 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, | ||
| }} | ||
| /> | ||
| ); |
There was a problem hiding this comment.
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
ProblemEditorPluginSlotwrap 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.