From f27b87dd3dbee51e034bfbf9f16c82bc75f9cc7b Mon Sep 17 00:00:00 2001 From: Lakatos Andrei Date: Mon, 23 Mar 2026 18:00:35 +0200 Subject: [PATCH] fix: made sure ecr fields are properly saved, adapted the response area toolbar callback for inline-dropdown [PD-5787] --- .../src/__tests__/ecr-toolbar.test.jsx | 66 +++++++++++++++++-- .../configure/src/ecr-toolbar.jsx | 9 ++- .../configure/src/main.jsx | 4 +- .../inline-dropdown-toolbar.test.jsx | 4 ++ .../configure/src/inline-dropdown-toolbar.jsx | 1 + .../inline-dropdown/configure/src/main.jsx | 4 +- 6 files changed, 79 insertions(+), 9 deletions(-) diff --git a/packages/explicit-constructed-response/configure/src/__tests__/ecr-toolbar.test.jsx b/packages/explicit-constructed-response/configure/src/__tests__/ecr-toolbar.test.jsx index 7181743d33..cc4b9fcc2c 100644 --- a/packages/explicit-constructed-response/configure/src/__tests__/ecr-toolbar.test.jsx +++ b/packages/explicit-constructed-response/configure/src/__tests__/ecr-toolbar.test.jsx @@ -25,7 +25,33 @@ describe('ECRToolbar', () => { editor = { commands: { updateAttributes: jest.fn() - } + }, + state: { + selection: { + from: 0, + }, + tr: { + setNodeMarkup: jest.fn(), + }, + }, + view: { + dispatch: jest.fn(), + nodeDOM: jest.fn().mockReturnValue({ + nodeType: 1, + getBoundingClientRect: jest.fn().mockReturnValue({ + top: 100, + left: 50, + width: 200, + height: 30, + }), + closest: jest.fn().mockReturnValue({ + getBoundingClientRect: jest.fn().mockReturnValue({ + top: 0, + left: 0, + }), + }), + }), + }, }; }); @@ -36,6 +62,10 @@ describe('ECRToolbar', () => { classes: {}, node: { key: 1, + attrs: { + index: '2', + value: 'moon', + }, data: { get: (prop) => { if (prop === 'index') { @@ -47,6 +77,7 @@ describe('ECRToolbar', () => { toJSON: jest.fn(), }, }, + pos: 5, editor, correctChoice: { value: '0', label: 'moon' }, }; @@ -62,12 +93,26 @@ describe('ECRToolbar', () => { describe('logic', () => { it('onDone: calls onToolbarDone and onChangeResponse', () => { // Create an instance to test the internal method + const mockTr = { + setNodeMarkup: jest.fn(), + }; + const testEditor = { + ...editor, + state: { + ...editor.state, + tr: mockTr, + }, + }; const testInstance = new ECRToolbar({ onChangeResponse, onToolbarDone, classes: {}, node: { key: 1, + attrs: { + index: '2', + value: 'moon', + }, data: { get: (prop) => { if (prop === 'index') { @@ -78,7 +123,8 @@ describe('ECRToolbar', () => { toJSON: jest.fn(), }, }, - editor, + pos: 5, + editor: testEditor, value: { change: jest.fn().mockReturnValue({ setNodeByKey: jest.fn().mockReturnValue({ @@ -96,10 +142,15 @@ describe('ECRToolbar', () => { correctChoice: { value: '0', label: 'moon' }, }); - testInstance.onDone(); + testInstance.onDone('test markup'); - expect(onToolbarDone).toBeCalled(); - expect(onChangeResponse).toBeCalled(); + expect(mockTr.setNodeMarkup).toHaveBeenCalledWith(5, undefined, { + index: '2', + value: 'test markup', + }); + expect(testEditor.view.dispatch).toHaveBeenCalledWith(mockTr); + expect(onToolbarDone).toHaveBeenCalledWith(true); + expect(onChangeResponse).toHaveBeenCalledWith('test markup'); }); it('onRespAreaChange updates state', () => { @@ -109,6 +160,10 @@ describe('ECRToolbar', () => { classes: {}, node: { key: 1, + attrs: { + index: '2', + value: 'moon', + }, data: { get: (prop) => { if (prop === 'index') { @@ -119,6 +174,7 @@ describe('ECRToolbar', () => { toJSON: jest.fn(), }, }, + pos: 5, editor, value: { change: jest.fn().mockReturnValue({ diff --git a/packages/explicit-constructed-response/configure/src/ecr-toolbar.jsx b/packages/explicit-constructed-response/configure/src/ecr-toolbar.jsx index 510d4099ac..6fc30f687f 100644 --- a/packages/explicit-constructed-response/configure/src/ecr-toolbar.jsx +++ b/packages/explicit-constructed-response/configure/src/ecr-toolbar.jsx @@ -18,6 +18,7 @@ export class ECRToolbar extends React.Component { static propTypes = { correctChoice: PropTypes.object, node: PropTypes.object, + pos: PropTypes.number, onDone: PropTypes.func, onChangeResponse: PropTypes.func.isRequired, onToolbarDone: PropTypes.func.isRequired, @@ -67,11 +68,15 @@ export class ECRToolbar extends React.Component { } onDone = (markup) => { - const { node, editor, onToolbarDone, onChangeResponse } = this.props; + const { editor, node, onToolbarDone, onChangeResponse, pos } = this.props; const sanitizedMarkup = stripHtmlTags(markup); this.setState({ markup: sanitizedMarkup }); - editor.commands.updateAttributes('explicit_constructed_response', { value: sanitizedMarkup }); + const { tr } = editor.state; + + // Merge old and new attributes + tr.setNodeMarkup(pos, undefined, { ...node.attrs, value: sanitizedMarkup }); + editor.view.dispatch(tr); onToolbarDone(true); onChangeResponse(sanitizedMarkup); diff --git a/packages/explicit-constructed-response/configure/src/main.jsx b/packages/explicit-constructed-response/configure/src/main.jsx index 37658cd13c..fa1d05f6e8 100644 --- a/packages/explicit-constructed-response/configure/src/main.jsx +++ b/packages/explicit-constructed-response/configure/src/main.jsx @@ -408,13 +408,15 @@ export class Main extends React.Component { duplicates: true, }, maxResponseAreas: maxResponseAreas, - respAreaToolbar: (node, editor, onToolbarDone) => { + respAreaToolbar: (nodeInfo, editor, onToolbarDone) => { + const [node, pos] = nodeInfo; const { model } = this.props; const correctChoice = (model.choices[node.attrs.index] || [])[0]; return () => ( this.onChangeResponse(node.attrs.index, newVal)} node={node} + pos={pos} editor={editor} onToolbarDone={onToolbarDone} correctChoice={correctChoice} diff --git a/packages/inline-dropdown/configure/src/__tests__/inline-dropdown-toolbar.test.jsx b/packages/inline-dropdown/configure/src/__tests__/inline-dropdown-toolbar.test.jsx index fca689d831..ee1e5af328 100644 --- a/packages/inline-dropdown/configure/src/__tests__/inline-dropdown-toolbar.test.jsx +++ b/packages/inline-dropdown/configure/src/__tests__/inline-dropdown-toolbar.test.jsx @@ -101,6 +101,7 @@ describe('RespAreaToolbar', () => { value: 'cow', }, }, + pos: 0, editor, choices: [ { @@ -1195,6 +1196,7 @@ describe('MenuItem Integration Tests', () => { key: '1', attrs: { index: '0', value: 'cow' }, }, + pos: 0, editor, choices, }; @@ -1284,6 +1286,7 @@ describe('MenuItem Integration Tests', () => { key: '1', attrs: { index: '0', value: 'cow' }, }, + pos: 0, editor: localEditor, choices, }; @@ -1522,6 +1525,7 @@ describe('MenuItem Integration Tests', () => { key: '1', attrs: { index: '0', value: 'cow' }, }, + pos: 0, editor: localEditor, choices, }; diff --git a/packages/inline-dropdown/configure/src/inline-dropdown-toolbar.jsx b/packages/inline-dropdown/configure/src/inline-dropdown-toolbar.jsx index 46cae49820..819568a3b9 100644 --- a/packages/inline-dropdown/configure/src/inline-dropdown-toolbar.jsx +++ b/packages/inline-dropdown/configure/src/inline-dropdown-toolbar.jsx @@ -176,6 +176,7 @@ const ItemBuilder = styled('div')(({ theme }) => ({ class RespAreaToolbar extends React.Component { static propTypes = { node: PropTypes.object, + pos: PropTypes.number, uploadSoundSupport: PropTypes.object, onDone: PropTypes.func, choices: PropTypes.array, diff --git a/packages/inline-dropdown/configure/src/main.jsx b/packages/inline-dropdown/configure/src/main.jsx index 953f9132cf..da457f6270 100644 --- a/packages/inline-dropdown/configure/src/main.jsx +++ b/packages/inline-dropdown/configure/src/main.jsx @@ -520,7 +520,8 @@ export class Main extends React.Component { duplicates: true, }, maxResponseAreas: maxResponseAreas, - respAreaToolbar: (node, editor, onToolbarDone) => { + respAreaToolbar: (nodeInfo, editor, onToolbarDone) => { + const [node, pos] = nodeInfo; const { respAreaChoices } = this.state; return props => ( @@ -531,6 +532,7 @@ export class Main extends React.Component { onRemoveChoice={(index) => this.onRemoveChoice(node.attrs.index, index)} onSelectChoice={(index) => this.onSelectChoice(node.attrs.index, index)} node={node} + pos={pos} editor={editor} onToolbarDone={onToolbarDone} choices={respAreaChoices[node.attrs.index]}