diff --git a/README.md b/README.md
index e06553e..4664d32 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ python main.py
```shell
docker-compose up -d
```
-The application will be accessible on port 8000 (or whatever port you configured).
+The application will be accessible on port 8001 (if the docker compose file was not changed !).
2. **Initialize the Mistral model** (first run):
```shell
diff --git a/src/components/undo_redo/components/UndoRedo.py b/src/components/undo_redo/components/UndoRedo.py
index 7d0fcdb..56af2bd 100644
--- a/src/components/undo_redo/components/UndoRedo.py
+++ b/src/components/undo_redo/components/UndoRedo.py
@@ -1,30 +1,24 @@
import logging
-from abc import ABC, abstractmethod
+from dataclasses import dataclass
from fasthtml.components import *
from components.BaseComponent import BaseComponentSingleton
from components.undo_redo.assets.icons import icon_redo, icon_undo
from components.undo_redo.commands import UndoRedoCommandManager
-from components.undo_redo.constants import UNDO_REDO_INSTANCE_ID
+from components.undo_redo.constants import UNDO_REDO_INSTANCE_ID, UndoRedoAttrs
from components_helpers import mk_icon, mk_tooltip
logger = logging.getLogger("UndoRedoApp")
-class CommandHistory(ABC):
- def __init__(self, name, desc, owner):
- self.name = name
- self.desc = desc
- self.owner = owner
-
- @abstractmethod
- def undo(self):
- pass
-
- @abstractmethod
- def redo(self):
- pass
+@dataclass
+class CommandHistory:
+ attrs: UndoRedoAttrs
+ digest: str | None # digest to remember
+ entry: str # digest to remember
+ key: str # key
+ path: str # path within the key if only on subitem needs to be updated
class UndoRedo(BaseComponentSingleton):
@@ -35,35 +29,73 @@ class UndoRedo(BaseComponentSingleton):
self.index = -1
self.history = []
self._commands = UndoRedoCommandManager(self)
+ self._db_engine = settings_manager.get_db_engine()
- def push(self, command: CommandHistory):
- self.history = self.history[:self.index + 1]
+ def snapshot(self, undo_redo_attrs: UndoRedoAttrs, entry, key, path=None):
+ digest = self._settings_manager.get_digest(self._session, entry) # get the current digest (the last one)
+
+ # init the history if this is the first call
+ if len(self.history) == 0:
+ digest_history = self._settings_manager.history(self._session, entry, digest, 2)
+ command = CommandHistory(undo_redo_attrs,
+ digest_history[1] if len(digest_history) > 1 else None,
+ entry,
+ key,
+ path)
+ self.history.append(command)
+ self.index = 0
+
+ command = CommandHistory(undo_redo_attrs, digest, entry, key, path)
+
+ self.history = self.history[:self.index + 1] #
self.history.append(command)
- self.index += 1
+ self.index = len(self.history) - 1
def undo(self):
logger.debug(f"Undo command")
- if self.index < 0 :
+ if self.index < 1:
logger.debug(f" No command to undo.")
return self
- command = self.history[self.index]
- logger.debug(f" Undoing command {command.name} ({command.desc})")
- res = command.undo()
+ current = self.history[self.index]
+ current_state = self._settings_manager.load(self._session, None, digest=current.digest)
+
+ previous = self.history[self.index - 1]
+ previous_state = self._settings_manager.load(self._session, None, digest=previous.digest)
+
+ # reapply the state
+ current_state[current.key] = previous_state[current.key]
+ self._settings_manager.save(self._session, current.entry, current_state)
+
self.index -= 1
- return self, res
+
+ if current.attrs.on_undo is not None:
+ return self, current.attrs.on_undo()
+ else:
+ return self
def redo(self):
- logger.debug("Redo command")
- if self.index == len(self.history) - 1:
- logger.debug(f" No command to redo.")
+ logger.debug(f"Redo command")
+ if self.index >= len(self.history) - 1:
+ logger.debug(f" No command to undo.")
return self
+ current = self.history[self.index]
+ current_state = self._settings_manager.load(self._session, None, digest=current.digest)
+
+ next_ = self.history[self.index + 1]
+ next_state = self._settings_manager.load(self._session, None, digest=next_.digest)
+
+ # reapply the state
+ current_state[current.key] = next_state[current.key]
+ self._settings_manager.save(self._session, current.entry, current_state)
+
self.index += 1
- command = self.history[self.index]
- logger.debug(f" Redoing command {command.name} ({command.desc})")
- res = command.redo()
- return self, res
+
+ if current.attrs.on_undo is not None:
+ return self, current.attrs.on_redo()
+ else:
+ return self
def refresh(self):
return self.__ft__(oob=True)
@@ -83,7 +115,7 @@ class UndoRedo(BaseComponentSingleton):
return mk_tooltip(mk_icon(icon_undo,
size=24,
**self._commands.undo()),
- f"Undo '{command.name}'.")
+ f"Undo '{command.attrs.name}'.")
else:
return mk_tooltip(mk_icon(icon_undo,
size=24,
@@ -97,7 +129,7 @@ class UndoRedo(BaseComponentSingleton):
return mk_tooltip(mk_icon(icon_redo,
size=24,
**self._commands.redo()),
- f"Redo '{command.name}'.")
+ f"Redo '{command.attrs.name}'.")
else:
return mk_tooltip(mk_icon(icon_redo,
size=24,
@@ -106,7 +138,7 @@ class UndoRedo(BaseComponentSingleton):
"Nothing to redo.")
def _can_undo(self):
- return self.index >= 0
+ return self.index >= 1
def _can_redo(self):
return self.index < len(self.history) - 1
diff --git a/src/components/undo_redo/constants.py b/src/components/undo_redo/constants.py
index 52e7d9b..552143a 100644
--- a/src/components/undo_redo/constants.py
+++ b/src/components/undo_redo/constants.py
@@ -1,3 +1,6 @@
+from dataclasses import dataclass
+from typing import Callable
+
UNDO_REDO_INSTANCE_ID = "__UndoRedo__"
ROUTE_ROOT = "/undo"
@@ -5,4 +8,16 @@ ROUTE_ROOT = "/undo"
class Routes:
Undo = "/undo"
- Redo = "/redo"
\ No newline at end of file
+ Redo = "/redo"
+
+
+@dataclass
+class UndoRedoAttrs:
+ name: str
+ desc: str = None
+ on_undo: Callable = None
+ on_redo: Callable = None
+
+ def __post_init__(self):
+ if self.on_redo is None:
+ self.on_redo = self.on_undo
\ No newline at end of file
diff --git a/src/components/workflows/WorkflowsApp.py b/src/components/workflows/WorkflowsApp.py
index a77342f..5fed577 100644
--- a/src/components/workflows/WorkflowsApp.py
+++ b/src/components/workflows/WorkflowsApp.py
@@ -129,14 +129,20 @@ def post(session, _id: str, component_id: str, event_name: str, details: dict):
@rt(Routes.PlayWorkflow)
def post(session, _id: str, tab_boundaries: str):
- logger.debug(
- f"Entering {Routes.PlayWorkflow} with args {debug_session(session)}, {_id=}")
+ logger.debug(f"Entering {Routes.PlayWorkflow} with args {debug_session(session)}, {_id=}")
instance = InstanceManager.get(session, _id)
return instance.play_workflow(json.loads(tab_boundaries))
+
@rt(Routes.StopWorkflow)
def post(session, _id: str):
- logger.debug(
- f"Entering {Routes.StopWorkflow} with args {debug_session(session)}, {_id=}")
+ logger.debug(f"Entering {Routes.StopWorkflow} with args {debug_session(session)}, {_id=}")
instance = InstanceManager.get(session, _id)
- return instance.stop_workflow()
\ No newline at end of file
+ return instance.stop_workflow()
+
+
+@rt(Routes.Refresh)
+def post(session, _id: str):
+ logger.debug(f"Entering {Routes.Refresh} with args {debug_session(session)}, {_id=}")
+ instance = InstanceManager.get(session, _id)
+ return instance.refresh()
diff --git a/src/components/workflows/assets/icons.py b/src/components/workflows/assets/icons.py
index da6157d..87ad77f 100644
--- a/src/components/workflows/assets/icons.py
+++ b/src/components/workflows/assets/icons.py
@@ -23,3 +23,6 @@ icon_pause_circle = NotStr(
# fluent RecordStop20Regular
icon_stop_circle = NotStr(
"""""")
+
+# fluent ArrowClockwise20Regular
+icon_refresh = NotStr("""""")
\ No newline at end of file
diff --git a/src/components/workflows/commands.py b/src/components/workflows/commands.py
index fa4e2d3..ca596ca 100644
--- a/src/components/workflows/commands.py
+++ b/src/components/workflows/commands.py
@@ -1,23 +1,6 @@
from components.BaseCommandManager import BaseCommandManager
-from components.undo_redo.components.UndoRedo import CommandHistory
from components.workflows.constants import Routes, ROUTE_ROOT
-class AddConnectorCommand(CommandHistory):
-
- def __init__(self, owner, connector):
- super().__init__("Add connector", "Add connector", owner)
- self.connector = connector
-
- def undo(self):
- del self.owner.get_state().components[self.connector.id]
- self.owner.get_db().save_state(self.owner.get_key(), self.owner.get_state()) # update db
- return self.owner.refresh_designer(True)
-
- def redo(self, oob=True):
- self.owner.get_state().components[self.connector.id] = self.connector
- self.owner.get_db().save_state(self.owner.get_key(), self.owner.get_state()) # update db
- return self.owner.refresh_designer(oob)
-
class WorkflowsCommandManager(BaseCommandManager):
def __init__(self, owner):
@@ -108,6 +91,13 @@ class WorkflowDesignerCommandManager(BaseCommandManager):
"hx-swap": "outerHTML",
"hx-vals": f'js:{{"_id": "{self._id}"}}',
}
+
+ def refresh(self):
+ return {
+ "hx_post": f"{ROUTE_ROOT}{Routes.Refresh}",
+ "hx-swap": "none",
+ "hx-vals": f'js:{{"_id": "{self._id}"}}',
+ }
class WorkflowPlayerCommandManager(BaseCommandManager):
diff --git a/src/components/workflows/components/WorkflowDesigner.py b/src/components/workflows/components/WorkflowDesigner.py
index 33e6ba8..d89de47 100644
--- a/src/components/workflows/components/WorkflowDesigner.py
+++ b/src/components/workflows/components/WorkflowDesigner.py
@@ -6,8 +6,9 @@ from fasthtml.xtend import Script
from assets.icons import icon_error
from components.BaseComponent import BaseComponent
-from components.workflows.assets.icons import icon_play, icon_pause, icon_stop
-from components.workflows.commands import WorkflowDesignerCommandManager, AddConnectorCommand
+from components.undo_redo.constants import UndoRedoAttrs
+from components.workflows.assets.icons import icon_play, icon_pause, icon_stop, icon_refresh
+from components.workflows.commands import WorkflowDesignerCommandManager
from components.workflows.components.WorkflowPlayer import WorkflowPlayer
from components.workflows.constants import WORKFLOW_DESIGNER_INSTANCE_ID, ProcessorTypes
from components.workflows.db_management import WorkflowsDesignerSettings, WorkflowComponent, \
@@ -63,6 +64,7 @@ class WorkflowDesigner(BaseComponent):
self._key = key
self._designer_settings = designer_settings
self._db = WorkflowsDesignerDbManager(session, settings_manager)
+ self._undo_redo = ComponentsInstancesHelper.get_undo_redo(session)
self._state = self._db.load_state(key)
self._boundaries = boundaries
self.commands = WorkflowDesignerCommandManager(self)
@@ -96,6 +98,13 @@ class WorkflowDesigner(BaseComponent):
def refresh_properties(self, oob=False):
return self._mk_properties(oob)
+ def refresh(self):
+ return self.__ft__(oob=True)
+
+ def refresh_state(self):
+ self._state = self._db.load_state(self._key)
+ return self.__ft__(oob=True)
+
def add_component(self, component_type, x, y):
self._state.component_counter += 1
@@ -111,24 +120,19 @@ class WorkflowDesigner(BaseComponent):
description=info["description"],
properties={"processor_name": PROCESSOR_TYPES[component_type][0]}
)
-
- command = AddConnectorCommand(self, component)
- undo_redo = ComponentsInstancesHelper.get_undo_redo(self._session)
- #undo_redo.push(command)
self._state.components[component_id] = component
- self._db.save_state(self._key, self._state) # update db
- undo_redo.snapshot("add_component")
- return command.redo(), undo_redo.refresh()
- # self._state.components[component_id] = component
- # self._db.save_state(self._key, self._state) # update db
- # return self.refresh_designer()
+
+ undo_redo_attrs = UndoRedoAttrs(f"Add Component '{component_type}'", on_undo=lambda: self.refresh_state())
+ self._db.save_state(self._key, self._state, undo_redo_attrs) # update db
+
+ return self.refresh_designer(), self._undo_redo.refresh()
def move_component(self, component_id, x, y):
if component_id in self._state.components:
self._state.selected_component_id = component_id
self._state.components[component_id].x = int(x)
self._state.components[component_id].y = int(y)
- self._db.save_state(self._key, self._state) # update db
+ self._db.save_state(self._key, self._state, ) # update db
return self.refresh_designer(), self.refresh_properties(True)
@@ -247,12 +251,13 @@ class WorkflowDesigner(BaseComponent):
def get_workflow_connections(self):
return self._state.connections
- def __ft__(self):
+ def __ft__(self, oob=False):
return Div(
H1(f"{self._designer_settings.workflow_name}", cls="text-xl font-bold"),
P("Drag components from the toolbox to the canvas to create your workflow.", cls="text-sm mb-6"),
Div(
self._mk_media(),
+ #self._mk_refresh_button(),
self._mk_error_message(),
cls="flex mb-2",
id=f"t_{self._id}"
@@ -263,6 +268,7 @@ class WorkflowDesigner(BaseComponent):
Script(f"bindWorkflowDesigner('{self._id}');"),
**apply_boundaries(self._boundaries),
id=f"{self._id}",
+ hx_swap_oob='true' if oob else None,
)
def _mk_connection_svg(self, conn: Connection):
@@ -384,6 +390,9 @@ class WorkflowDesigner(BaseComponent):
cls=f"media-controls flex m-2"
)
+ def _mk_refresh_button(self):
+ return mk_icon(icon_refresh, **self.commands.refresh())
+
def _mk_error_message(self):
if not self._error_message:
return Div()
diff --git a/src/components/workflows/constants.py b/src/components/workflows/constants.py
index e8c56f7..d4473a5 100644
--- a/src/components/workflows/constants.py
+++ b/src/components/workflows/constants.py
@@ -32,4 +32,5 @@ class Routes:
PlayWorkflow = "/play-workflow"
PauseWorkflow = "/pause-workflow"
StopWorkflow = "/stop-workflow"
+ Refresh = "/refresh"
\ No newline at end of file
diff --git a/src/components/workflows/db_management.py b/src/components/workflows/db_management.py
index 1938ce9..ad2b104 100644
--- a/src/components/workflows/db_management.py
+++ b/src/components/workflows/db_management.py
@@ -2,10 +2,12 @@ import enum
import logging
from dataclasses import dataclass, field
+from components.undo_redo.constants import UndoRedoAttrs
from components.workflows.constants import WORKFLOWS_DB_ENTRY, WORKFLOW_DESIGNER_DB_ENTRY, \
WORKFLOW_DESIGNER_DB_SETTINGS_ENTRY, WORKFLOW_DESIGNER_DB_STATE_ENTRY
from core.settings_management import SettingsManager
from core.utils import make_safe_id
+from utils.ComponentsInstancesHelper import ComponentsInstancesHelper
logger = logging.getLogger("WorkflowsSettings")
@@ -158,6 +160,7 @@ class WorkflowsDesignerDbManager:
def __init__(self, session: dict, settings_manager: SettingsManager):
self._session = session
self._settings_manager = settings_manager
+ self._undo_redo = ComponentsInstancesHelper.get_undo_redo(session)
@staticmethod
def _get_db_entry(key):
@@ -169,11 +172,17 @@ class WorkflowsDesignerDbManager:
WORKFLOW_DESIGNER_DB_SETTINGS_ENTRY,
settings)
- def save_state(self, key: str, state: WorkflowsDesignerState):
+ def save_state(self, key: str, state: WorkflowsDesignerState, undo_redo_attrs: UndoRedoAttrs = None):
+ db_entry = self._get_db_entry(key)
self._settings_manager.put(self._session,
- self._get_db_entry(key),
+ db_entry,
WORKFLOW_DESIGNER_DB_STATE_ENTRY,
state)
+
+ if undo_redo_attrs is not None:
+ self._undo_redo.snapshot(undo_redo_attrs,
+ db_entry,
+ WORKFLOW_DESIGNER_DB_STATE_ENTRY)
def save_all(self, key: str, settings: WorkflowsDesignerSettings = None, state: WorkflowsDesignerState = None):
items = {}
diff --git a/src/core/dbengine.py b/src/core/dbengine.py
index cb2e951..4a459ef 100644
--- a/src/core/dbengine.py
+++ b/src/core/dbengine.py
@@ -271,6 +271,42 @@ class DbEngine:
except KeyError:
raise DbException(f"Key '{key}' not found in entry '{entry}'")
+ def history(self, user_id, entry, digest=None, max_items=1000):
+ """
+ Gives the current digest and all its ancestors
+ :param user_id:
+ :param entry:
+ :param digest:
+ :param max_items:
+ :return:
+ """
+ with self.lock:
+ logger.info(f"History for {user_id=}, {entry=}, {digest=}")
+
+ digest_to_use = digest or self._get_entry_digest(user_id, entry)
+ logger.debug(f"Using digest {digest_to_use}.")
+
+ count = 0
+ history = []
+
+ while True:
+ if count >= max_items or digest_to_use is None:
+ break
+
+ history.append(digest_to_use)
+ count += 1
+
+ try:
+ target_file = self._get_obj_path(user_id, digest_to_use)
+ with open(target_file, 'r', encoding='utf-8') as file:
+ as_dict = json.load(file)
+
+ digest_to_use = as_dict[TAG_PARENT][0]
+ except FileNotFoundError:
+ break
+
+ return history
+
def debug_root(self):
"""
Lists all folders in the root directory
@@ -312,7 +348,7 @@ class DbEngine:
return []
return [f for f in os.listdir(self.root) if os.path.isdir(os.path.join(self.root, f)) and f != 'refs']
- def debug_get_digest(self, user_id, entry):
+ def get_digest(self, user_id, entry):
return self._get_entry_digest(user_id, entry)
def _serialize(self, obj):
diff --git a/src/core/settings_management.py b/src/core/settings_management.py
index 58dd7e3..943043c 100644
--- a/src/core/settings_management.py
+++ b/src/core/settings_management.py
@@ -98,10 +98,10 @@ class SettingsManager:
user_id, user_email = self._get_user(session)
return self._db_engine.save(user_id, user_email, entry, obj)
- def load(self, session: dict, entry: str, default=NoDefault):
+ def load(self, session: dict, entry: str, digest=None, default=NoDefault):
user_id, _ = self._get_user(session)
try:
- return self._db_engine.load(user_id, entry)
+ return self._db_engine.load(user_id, entry, digest)
except DbException:
return default
@@ -128,6 +128,14 @@ class SettingsManager:
return self._db_engine.exists(user_id, entry)
+ def get_digest(self, session: dict, entry: str):
+ user_id, _ = self._get_user(session)
+ return self._db_engine.get_digest(user_id, entry)
+
+ def history(self, session, entry, digest=None, max_items=1000):
+ user_id, _ = self._get_user(session)
+ return self._db_engine.history(user_id, entry, digest, max_items)
+
def get_db_engine(self):
return self._db_engine
@@ -177,7 +185,7 @@ class GenericDbManager:
if key.startswith("_"):
super().__setattr__(key, value)
- settings = self._settings_manager.load(self._session, self._obj_entry, self._obj_type())
+ settings = self._settings_manager.load(self._session, self._obj_entry, default=self._obj_type())
if not (hasattr(settings, key)):
raise AttributeError(f"Settings '{self._obj_entry}' has no attribute '{key}'.")
@@ -188,7 +196,7 @@ class GenericDbManager:
if item.startswith("_"):
return super().__getattribute__(item)
- settings = self._settings_manager.load(self._session, self._obj_entry, self._obj_type())
+ settings = self._settings_manager.load(self._session, self._obj_entry, default=self._obj_type())
if not (hasattr(settings, item)):
raise AttributeError(f"Settings '{self._obj_entry}' has no attribute '{item}'.")
@@ -250,7 +258,7 @@ class NestedSettingsManager:
self._settings_manager.save(self._session, self._obj_entry, settings)
def _get_settings_and_object(self):
- settings = self._settings_manager.load(self._session, self._obj_entry, self._obj_type())
+ settings = self._settings_manager.load(self._session, self._obj_entry, default=self._obj_type())
if not hasattr(settings, self._obj_attribute):
raise AttributeError(f"Settings '{self._obj_entry}' has no attribute '{self._obj_attribute}'.")
diff --git a/tests/test_dbengine.py b/tests/test_dbengine.py
index d6c529b..ccf0d1e 100644
--- a/tests/test_dbengine.py
+++ b/tests/test_dbengine.py
@@ -235,3 +235,40 @@ def test_put_many_save_only_if_necessary(engine):
entry_content = engine.load(FAKE_USER_ID, "MyEntry")
assert entry_content[TAG_PARENT] == [None] # Still None, nothing was save
+
+
+def test_i_can_retrieve_history_using_put(engine):
+ engine.put(FAKE_USER_ID, FAKE_USER_EMAIL, "MyEntry", "key1", DummyObj(1, "a", False))
+ engine.put(FAKE_USER_ID, FAKE_USER_EMAIL, "MyEntry", "key1", DummyObj(2, "a", False))
+ engine.put(FAKE_USER_ID, FAKE_USER_EMAIL, "MyEntry", "key1", DummyObj(3, "a", False))
+
+ history = engine.history(FAKE_USER_ID, "MyEntry")
+ assert len(history) == 3
+
+ v0 = engine.load(FAKE_USER_ID, "MyEntry", history[0])
+ v1 = engine.load(FAKE_USER_ID, "MyEntry", history[1])
+ v2 = engine.load(FAKE_USER_ID, "MyEntry", history[2])
+
+ assert v0["key1"] == DummyObj(3, "a", False)
+ assert v1["key1"] == DummyObj(2, "a", False)
+ assert v2["key1"] == DummyObj(1, "a", False)
+
+ assert v2[TAG_PARENT] == [None]
+
+def test_i_can_retrieve_history_using_save(engine):
+ engine.save(FAKE_USER_ID, FAKE_USER_EMAIL, "MyEntry", {"key1" : DummyObj(1, "a", False)})
+ engine.save(FAKE_USER_ID, FAKE_USER_EMAIL, "MyEntry", {"key1" : DummyObj(2, "a", False)})
+ engine.save(FAKE_USER_ID, FAKE_USER_EMAIL, "MyEntry", {"key1" : DummyObj(3, "a", False)})
+
+ history = engine.history(FAKE_USER_ID, "MyEntry")
+ assert len(history) == 3
+
+ v0 = engine.load(FAKE_USER_ID, "MyEntry", history[0])
+ v1 = engine.load(FAKE_USER_ID, "MyEntry", history[1])
+ v2 = engine.load(FAKE_USER_ID, "MyEntry", history[2])
+
+ assert v0["key1"] == DummyObj(3, "a", False)
+ assert v1["key1"] == DummyObj(2, "a", False)
+ assert v2["key1"] == DummyObj(1, "a", False)
+
+ assert v2[TAG_PARENT] == [None]
\ No newline at end of file
diff --git a/tests/test_undo_redo.py b/tests/test_undo_redo.py
index b097a2d..9082d62 100644
--- a/tests/test_undo_redo.py
+++ b/tests/test_undo_redo.py
@@ -1,25 +1,12 @@
import pytest
from fasthtml.components import Div
-from components.undo_redo.components.UndoRedo import UndoRedo, CommandHistory
+from components.undo_redo.components.UndoRedo import UndoRedo
from core.settings_management import SettingsManager, MemoryDbEngine
from helpers import matches, div_icon, Contains, DoesNotContain
from my_mocks import tabs_manager
-class UndoableCommand(CommandHistory):
- def __init__(self, old_value=0, new_value=0):
- super().__init__("Set new value", lambda value: f"Setting new value to {value}", None)
- self.old_value = old_value
- self.new_value = new_value
-
- def undo(self):
- return Div(self.old_value, hx_swap_oob="true")
-
- def redo(self):
- return Div(self.new_value, hx_swap_oob="true")
-
-
@pytest.fixture
def undo_redo(session, tabs_manager):
return UndoRedo(session,
@@ -100,6 +87,7 @@ def test_i_can_undo_and_redo(undo_redo):
expected = Div(2, hx_swap_oob="true")
assert matches(res, expected)
+
def test_history_is_rewritten_when_pushing_a_command(undo_redo):
undo_redo.push(UndoableCommand(0, 1))
undo_redo.push(UndoableCommand(1, 2))
@@ -109,4 +97,4 @@ def test_history_is_rewritten_when_pushing_a_command(undo_redo):
undo_redo.undo()
undo_redo.push(UndoableCommand(1, 5))
- assert len(undo_redo.history) == 2
\ No newline at end of file
+ assert len(undo_redo.history) == 2