From cb4d6a0ea4b3401c6d5ffa5940f8097c95988aff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Gagn=C3=A9?= <152034892+DisciplinedSoftware@users.noreply.github.com> Date: Tue, 10 Mar 2026 00:59:44 -0400 Subject: [PATCH 1/6] feat: expose clearHistory() method on MindElixir instance Adds a clearHistory() method to the operationHistory plugin that resets the undo/redo stack to an empty state with a fresh baseline snapshot. This is needed by hosts that load new data into an existing MindElixir instance (e.g. via refresh()) and want to prevent users from undoing back into the previous diagram's state. --- src/plugin/operationHistory.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/plugin/operationHistory.ts b/src/plugin/operationHistory.ts index 83366fb3..d5450fe7 100644 --- a/src/plugin/operationHistory.ts +++ b/src/plugin/operationHistory.ts @@ -88,6 +88,11 @@ export default function (mei: MindElixirInstance) { } } } + mei.clearHistory = function () { + history = [] + currentIndex = -1 + current = mei.getData() + } const handleOperation = function (operation: Operation) { if (operation.name === 'beginEdit') return history = history.slice(0, currentIndex + 1) From e6e688b8172ddde0a3bce042bc8fefb7c4e8d6fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Gagn=C3=A9?= <152034892+DisciplinedSoftware@users.noreply.github.com> Date: Tue, 10 Mar 2026 01:00:22 -0400 Subject: [PATCH 2/6] feat: add clearHistory() to MindElixirInstance type declaration --- src/types/index.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/types/index.ts b/src/types/index.ts index ef130cf7..c477f31d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -121,6 +121,13 @@ export interface MindElixirInstance extends Omit, 'markdown' | history: Operation[] undo: () => void redo: () => void + /** + * Reset the undo/redo stack and update the internal baseline snapshot to the + * current diagram state. Call this after loading new data into an existing + * instance (e.g. after `refresh()`) to prevent users from undoing back into + * a previously loaded diagram. + */ + clearHistory: () => void selection: SelectionArea dragMoveHelper: ReturnType From 601dc4b45ffe02ea74c2c156072c367760b596f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Gagn=C3=A9?= <152034892+DisciplinedSoftware@users.noreply.github.com> Date: Tue, 10 Mar 2026 01:09:22 -0400 Subject: [PATCH 3/6] fix: reset currentSelectedNodes in clearHistory() and add missing review fixes - Reset currentSelectedNodes to [] so stale selection IDs from the previous diagram cannot corrupt undo/redo selection restoration after a refresh + clearHistory() call. - (types updated in next commit to mark clearHistory as optional) --- src/plugin/operationHistory.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugin/operationHistory.ts b/src/plugin/operationHistory.ts index d5450fe7..e2cec086 100644 --- a/src/plugin/operationHistory.ts +++ b/src/plugin/operationHistory.ts @@ -92,6 +92,7 @@ export default function (mei: MindElixirInstance) { history = [] currentIndex = -1 current = mei.getData() + currentSelectedNodes = [] } const handleOperation = function (operation: Operation) { if (operation.name === 'beginEdit') return From 15a42c9e7a7e6c9d575d7878bcf4fdeeef60d6da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Gagn=C3=A9?= <152034892+DisciplinedSoftware@users.noreply.github.com> Date: Tue, 10 Mar 2026 01:10:02 -0400 Subject: [PATCH 4/6] fix: mark clearHistory as optional in MindElixirInstance clearHistory is only registered by the operationHistory plugin, which is skipped when allowUndo is false. Declaring it as a required method would be a type-runtime mismatch. Mark it optional and update the JSDoc to mention the allowUndo prerequisite. --- src/types/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/types/index.ts b/src/types/index.ts index c477f31d..4d7f57a9 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -126,8 +126,10 @@ export interface MindElixirInstance extends Omit, 'markdown' | * current diagram state. Call this after loading new data into an existing * instance (e.g. after `refresh()`) to prevent users from undoing back into * a previously loaded diagram. + * + * Only available when `allowUndo` is `true` (the default). */ - clearHistory: () => void + clearHistory?: () => void selection: SelectionArea dragMoveHelper: ReturnType From 1e50bf0177f8c501556d534982a849ba4b23594e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Gagn=C3=A9?= <152034892+DisciplinedSoftware@users.noreply.github.com> Date: Tue, 10 Mar 2026 01:20:26 -0400 Subject: [PATCH 5/6] test: add Playwright tests for clearHistory() Tests verify: - Undo cannot revert into a pre-refresh diagram after clearHistory() - Operations performed after clearHistory() are themselves undoable/redoable - The first undo baseline is the refreshed diagram state, not a prior one --- tests/clear-history.spec.ts | 107 ++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 tests/clear-history.spec.ts diff --git a/tests/clear-history.spec.ts b/tests/clear-history.spec.ts new file mode 100644 index 00000000..68c108d5 --- /dev/null +++ b/tests/clear-history.spec.ts @@ -0,0 +1,107 @@ +import { test, expect } from './mind-elixir-test' + +const diagramA = { + nodeData: { + id: 'root-a', + topic: 'Diagram A', + children: [ + { id: 'child-a', topic: 'Child A' }, + ], + }, +} + +const diagramB = { + nodeData: { + id: 'root-b', + topic: 'Diagram B', + children: [ + { id: 'child-b', topic: 'Child B' }, + ], + }, +} + +test.beforeEach(async ({ me }) => { + await me.init(diagramA) +}) + +test('clearHistory - undo cannot revert into the pre-refresh diagram', async ({ page, me }) => { + // Perform an operation in Diagram A + await me.click('Child A') + await page.keyboard.press('Tab') + await page.keyboard.press('Enter') + await expect(me.getByText('New Node')).toBeVisible() + + // Load a new diagram and clear the history + await page.evaluate(data => { + const mind = (window as any)['#map'] + mind.refresh(data) + mind.clearHistory() + }, diagramB) + + // Diagram B should now be shown + await expect(me.getByText('Diagram B')).toBeVisible() + await expect(me.getByText('Diagram A')).toBeHidden() + + // Undo should NOT travel back into Diagram A + await page.keyboard.press('Control+z') + await expect(me.getByText('Diagram B')).toBeVisible() + await expect(me.getByText('Diagram A')).toBeHidden() + + // Redo should also be a no-op + await page.keyboard.press('Control+y') + await expect(me.getByText('Diagram B')).toBeVisible() + await expect(me.getByText('Diagram A')).toBeHidden() +}) + +test('clearHistory - operations after clearHistory are undoable normally', async ({ page, me }) => { + // Perform an operation in Diagram A then load Diagram B and clear history + await me.click('Child A') + await page.keyboard.press('Delete') + await expect(me.getByText('Child A')).toBeHidden() + + await page.evaluate(data => { + const mind = (window as any)['#map'] + mind.refresh(data) + mind.clearHistory() + }, diagramB) + + // Now add a node to Diagram B + await me.click('Child B') + await page.keyboard.press('Tab') + await page.keyboard.press('Enter') + await expect(me.getByText('New Node')).toBeVisible() + + // Undo the add — should work + await page.keyboard.press('Control+z') + await expect(me.getByText('New Node')).toBeHidden() + + // Redo the add — should also work + await page.keyboard.press('Control+y') + await expect(me.getByText('New Node')).toBeVisible() + + // One more undo should be a no-op (no history before the cleared point) + await page.keyboard.press('Control+z') // undo add + await page.keyboard.press('Control+z') // should be no-op, not reach Diagram A + await expect(me.getByText('Diagram B')).toBeVisible() + await expect(me.getByText('Diagram A')).toBeHidden() +}) + +test('clearHistory - first undo baseline is the refreshed diagram state', async ({ page, me }) => { + // Load Diagram B, clear history, then add a node and undo + await page.evaluate(data => { + const mind = (window as any)['#map'] + mind.refresh(data) + mind.clearHistory() + }, diagramB) + + await me.click('Child B') + await page.keyboard.press('Tab') + await page.keyboard.press('Enter') + await expect(me.getByText('New Node')).toBeVisible() + + // Undo restores exactly to the state right after refresh (Diagram B with just Child B) + await page.keyboard.press('Control+z') + await expect(me.getByText('New Node')).toBeHidden() + await expect(me.getByText('Child B')).toBeVisible() + await expect(me.getByText('Diagram B')).toBeVisible() +}) From 90b0e0ebe93d8288c6c245b602951139937c5737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Gagn=C3=A9?= Date: Wed, 11 Mar 2026 01:12:02 -0400 Subject: [PATCH 6/6] fix: use mei.clearSelection() in clearHistory and fix tests --- src/plugin/operationHistory.ts | 2 +- tests/clear-history.spec.ts | 46 ++++++++++++++++------------------ 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/src/plugin/operationHistory.ts b/src/plugin/operationHistory.ts index e2cec086..0c5ce3a6 100644 --- a/src/plugin/operationHistory.ts +++ b/src/plugin/operationHistory.ts @@ -92,7 +92,7 @@ export default function (mei: MindElixirInstance) { history = [] currentIndex = -1 current = mei.getData() - currentSelectedNodes = [] + mei.clearSelection() } const handleOperation = function (operation: Operation) { if (operation.name === 'beginEdit') return diff --git a/tests/clear-history.spec.ts b/tests/clear-history.spec.ts index 68c108d5..ca738df5 100644 --- a/tests/clear-history.spec.ts +++ b/tests/clear-history.spec.ts @@ -4,9 +4,7 @@ const diagramA = { nodeData: { id: 'root-a', topic: 'Diagram A', - children: [ - { id: 'child-a', topic: 'Child A' }, - ], + children: [{ id: 'child-a', topic: 'Child A' }], }, } @@ -14,9 +12,7 @@ const diagramB = { nodeData: { id: 'root-b', topic: 'Diagram B', - children: [ - { id: 'child-b', topic: 'Child B' }, - ], + children: [{ id: 'child-b', topic: 'Child B' }], }, } @@ -24,25 +20,24 @@ test.beforeEach(async ({ me }) => { await me.init(diagramA) }) -test('clearHistory - undo cannot revert into the pre-refresh diagram', async ({ page, me }) => { +test('clearHistory - undo cannot revert into pre-refresh diagram', async ({ page, me }) => { // Perform an operation in Diagram A await me.click('Child A') await page.keyboard.press('Tab') await page.keyboard.press('Enter') await expect(me.getByText('New Node')).toBeVisible() - // Load a new diagram and clear the history - await page.evaluate(data => { + // Load Diagram B and clear the history stack + await page.evaluate((data: typeof diagramB) => { const mind = (window as any)['#map'] mind.refresh(data) mind.clearHistory() }, diagramB) - // Diagram B should now be shown await expect(me.getByText('Diagram B')).toBeVisible() await expect(me.getByText('Diagram A')).toBeHidden() - // Undo should NOT travel back into Diagram A + // Undo should be a no-op — must not travel back into Diagram A await page.keyboard.press('Control+z') await expect(me.getByText('Diagram B')).toBeVisible() await expect(me.getByText('Diagram A')).toBeHidden() @@ -54,18 +49,20 @@ test('clearHistory - undo cannot revert into the pre-refresh diagram', async ({ }) test('clearHistory - operations after clearHistory are undoable normally', async ({ page, me }) => { - // Perform an operation in Diagram A then load Diagram B and clear history + // Perform an operation in Diagram A, then switch to Diagram B and clear history await me.click('Child A') await page.keyboard.press('Delete') await expect(me.getByText('Child A')).toBeHidden() - await page.evaluate(data => { + await page.evaluate((data: typeof diagramB) => { const mind = (window as any)['#map'] mind.refresh(data) mind.clearHistory() }, diagramB) - // Now add a node to Diagram B + await expect(me.getByText('Diagram B')).toBeVisible() + + // Add a node to Diagram B await me.click('Child B') await page.keyboard.press('Tab') await page.keyboard.press('Enter') @@ -74,32 +71,33 @@ test('clearHistory - operations after clearHistory are undoable normally', async // Undo the add — should work await page.keyboard.press('Control+z') await expect(me.getByText('New Node')).toBeHidden() + await expect(me.getByText('Diagram B')).toBeVisible() - // Redo the add — should also work - await page.keyboard.press('Control+y') - await expect(me.getByText('New Node')).toBeVisible() - - // One more undo should be a no-op (no history before the cleared point) - await page.keyboard.press('Control+z') // undo add - await page.keyboard.press('Control+z') // should be no-op, not reach Diagram A + // Another undo should be a no-op — must not reach Diagram A + await page.keyboard.press('Control+z') await expect(me.getByText('Diagram B')).toBeVisible() await expect(me.getByText('Diagram A')).toBeHidden() + + // Redo restores the added node + await page.keyboard.press('Control+y') + await expect(me.getByText('New Node')).toBeVisible() }) test('clearHistory - first undo baseline is the refreshed diagram state', async ({ page, me }) => { - // Load Diagram B, clear history, then add a node and undo - await page.evaluate(data => { + // Switch to Diagram B and clear history + await page.evaluate((data: typeof diagramB) => { const mind = (window as any)['#map'] mind.refresh(data) mind.clearHistory() }, diagramB) + // Add a node to Diagram B await me.click('Child B') await page.keyboard.press('Tab') await page.keyboard.press('Enter') await expect(me.getByText('New Node')).toBeVisible() - // Undo restores exactly to the state right after refresh (Diagram B with just Child B) + // Undo should restore exactly to the post-refresh state of Diagram B await page.keyboard.press('Control+z') await expect(me.getByText('New Node')).toBeHidden() await expect(me.getByText('Child B')).toBeVisible()