Working on undo redo
This commit is contained in:
@@ -36,7 +36,7 @@ class DataGridDbManager:
|
|||||||
self._settings_manager.save(self._session, self._get_db_entry(), {})
|
self._settings_manager.save(self._session, self._get_db_entry(), {})
|
||||||
|
|
||||||
def _get_db_entry(self):
|
def _get_db_entry(self):
|
||||||
return f"{DATAGRID_DB_ENTRY}_{self._key}"
|
return make_safe_id(f"{DATAGRID_DB_ENTRY}_{self._key}")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _key_as_string(key):
|
def _key_as_string(key):
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ logger = logging.getLogger("UndoRedoApp")
|
|||||||
|
|
||||||
|
|
||||||
class CommandHistory(ABC):
|
class CommandHistory(ABC):
|
||||||
def __init__(self, name, desc):
|
def __init__(self, name, desc, owner):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.desc = desc
|
self.desc = desc
|
||||||
|
self.owner = owner
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def undo(self):
|
def undo(self):
|
||||||
@@ -31,38 +32,42 @@ class UndoRedo(BaseComponentSingleton):
|
|||||||
|
|
||||||
def __init__(self, session, _id, settings_manager=None, tabs_manager=None):
|
def __init__(self, session, _id, settings_manager=None, tabs_manager=None):
|
||||||
super().__init__(session, _id, settings_manager, tabs_manager)
|
super().__init__(session, _id, settings_manager, tabs_manager)
|
||||||
self.index = 0
|
self.index = -1
|
||||||
self.history = []
|
self.history = []
|
||||||
self._commands = UndoRedoCommandManager(self)
|
self._commands = UndoRedoCommandManager(self)
|
||||||
|
|
||||||
def push(self, command: CommandHistory):
|
def push(self, command: CommandHistory):
|
||||||
|
self.history = self.history[:self.index + 1]
|
||||||
self.history.append(command)
|
self.history.append(command)
|
||||||
self.index += 1
|
self.index += 1
|
||||||
|
|
||||||
def undo(self):
|
def undo(self):
|
||||||
logger.debug(f"Undo command")
|
logger.debug(f"Undo command")
|
||||||
if self.index == 0:
|
if self.index < 0 :
|
||||||
logger.debug(f" No command to undo.")
|
logger.debug(f" No command to undo.")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
self.index -= 1
|
|
||||||
command = self.history[self.index]
|
command = self.history[self.index]
|
||||||
logger.debug(f" Undoing command {command.name} ({command.desc})")
|
logger.debug(f" Undoing command {command.name} ({command.desc})")
|
||||||
res = command.undo()
|
res = command.undo()
|
||||||
|
self.index -= 1
|
||||||
return self, res
|
return self, res
|
||||||
|
|
||||||
def redo(self):
|
def redo(self):
|
||||||
logger.debug("Redo command")
|
logger.debug("Redo command")
|
||||||
if self.index >= len(self.history):
|
if self.index == len(self.history) - 1:
|
||||||
logger.debug(f" No command to redo.")
|
logger.debug(f" No command to redo.")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
self.index += 1
|
||||||
command = self.history[self.index]
|
command = self.history[self.index]
|
||||||
logger.debug(f" Redoing command {command.name} ({command.desc})")
|
logger.debug(f" Redoing command {command.name} ({command.desc})")
|
||||||
res = command.redo()
|
res = command.redo()
|
||||||
self.index += 1
|
|
||||||
return self, res
|
return self, res
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
return self.__ft__(oob=True)
|
||||||
|
|
||||||
def __ft__(self, oob=False):
|
def __ft__(self, oob=False):
|
||||||
return Div(
|
return Div(
|
||||||
self._mk_undo(),
|
self._mk_undo(),
|
||||||
@@ -74,33 +79,34 @@ class UndoRedo(BaseComponentSingleton):
|
|||||||
|
|
||||||
def _mk_undo(self):
|
def _mk_undo(self):
|
||||||
if self._can_undo():
|
if self._can_undo():
|
||||||
command = self.history[self.index - 1]
|
command = self.history[self.index]
|
||||||
return mk_tooltip(mk_icon(icon_undo,
|
return mk_tooltip(mk_icon(icon_undo,
|
||||||
size=24,
|
size=24,
|
||||||
**self._commands.undo()),
|
**self._commands.undo()),
|
||||||
"Undo")
|
f"Undo '{command.name}'.")
|
||||||
else:
|
else:
|
||||||
return mk_tooltip(mk_icon(icon_undo,
|
return mk_tooltip(mk_icon(icon_undo,
|
||||||
size=24,
|
size=24,
|
||||||
can_select=False,
|
can_select=False,
|
||||||
cls="mmt-btn-disabled"),
|
cls="mmt-btn-disabled"),
|
||||||
"Nothing to undo")
|
"Nothing to undo.")
|
||||||
|
|
||||||
def _mk_redo(self):
|
def _mk_redo(self):
|
||||||
if self._can_redo():
|
if self._can_redo():
|
||||||
|
command = self.history[self.index]
|
||||||
return mk_tooltip(mk_icon(icon_redo,
|
return mk_tooltip(mk_icon(icon_redo,
|
||||||
size=24,
|
size=24,
|
||||||
**self._commands.redo()),
|
**self._commands.redo()),
|
||||||
"Redo")
|
f"Redo '{command.name}'.")
|
||||||
else:
|
else:
|
||||||
return mk_tooltip(mk_icon(icon_redo,
|
return mk_tooltip(mk_icon(icon_redo,
|
||||||
size=24,
|
size=24,
|
||||||
can_select=False,
|
can_select=False,
|
||||||
cls="mmt-btn-disabled"),
|
cls="mmt-btn-disabled"),
|
||||||
"Nothing to redo")
|
"Nothing to redo.")
|
||||||
|
|
||||||
def _can_undo(self):
|
def _can_undo(self):
|
||||||
return self.index > 0
|
return self.index >= 0
|
||||||
|
|
||||||
def _can_redo(self):
|
def _can_redo(self):
|
||||||
return self.index < len(self.history)
|
return self.index < len(self.history) - 1
|
||||||
|
|||||||
@@ -1,6 +1,23 @@
|
|||||||
from components.BaseCommandManager import BaseCommandManager
|
from components.BaseCommandManager import BaseCommandManager
|
||||||
|
from components.undo_redo.components.UndoRedo import CommandHistory
|
||||||
from components.workflows.constants import Routes, ROUTE_ROOT
|
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):
|
class WorkflowsCommandManager(BaseCommandManager):
|
||||||
def __init__(self, owner):
|
def __init__(self, owner):
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from fasthtml.xtend import Script
|
|||||||
from assets.icons import icon_error
|
from assets.icons import icon_error
|
||||||
from components.BaseComponent import BaseComponent
|
from components.BaseComponent import BaseComponent
|
||||||
from components.workflows.assets.icons import icon_play, icon_pause, icon_stop
|
from components.workflows.assets.icons import icon_play, icon_pause, icon_stop
|
||||||
from components.workflows.commands import WorkflowDesignerCommandManager
|
from components.workflows.commands import WorkflowDesignerCommandManager, AddConnectorCommand
|
||||||
from components.workflows.components.WorkflowPlayer import WorkflowPlayer
|
from components.workflows.components.WorkflowPlayer import WorkflowPlayer
|
||||||
from components.workflows.constants import WORKFLOW_DESIGNER_INSTANCE_ID, ProcessorTypes
|
from components.workflows.constants import WORKFLOW_DESIGNER_INSTANCE_ID, ProcessorTypes
|
||||||
from components.workflows.db_management import WorkflowsDesignerSettings, WorkflowComponent, \
|
from components.workflows.db_management import WorkflowsDesignerSettings, WorkflowComponent, \
|
||||||
@@ -16,6 +16,7 @@ from components_helpers import apply_boundaries, mk_tooltip, mk_dialog_buttons,
|
|||||||
from core.instance_manager import InstanceManager
|
from core.instance_manager import InstanceManager
|
||||||
from core.jira import JiraRequestTypes, DEFAULT_SEARCH_FIELDS
|
from core.jira import JiraRequestTypes, DEFAULT_SEARCH_FIELDS
|
||||||
from core.utils import get_unique_id, make_safe_id
|
from core.utils import get_unique_id, make_safe_id
|
||||||
|
from utils.ComponentsInstancesHelper import ComponentsInstancesHelper
|
||||||
from utils.DbManagementHelper import DbManagementHelper
|
from utils.DbManagementHelper import DbManagementHelper
|
||||||
|
|
||||||
logger = logging.getLogger("WorkflowDesigner")
|
logger = logging.getLogger("WorkflowDesigner")
|
||||||
@@ -80,8 +81,17 @@ class WorkflowDesigner(BaseComponent):
|
|||||||
def set_boundaries(self, boundaries: dict):
|
def set_boundaries(self, boundaries: dict):
|
||||||
self._boundaries = boundaries
|
self._boundaries = boundaries
|
||||||
|
|
||||||
def refresh_designer(self):
|
def get_state(self):
|
||||||
return self._mk_elements()
|
return self._state
|
||||||
|
|
||||||
|
def get_db(self):
|
||||||
|
return self._db
|
||||||
|
|
||||||
|
def get_key(self):
|
||||||
|
return self._key
|
||||||
|
|
||||||
|
def refresh_designer(self, oob=False):
|
||||||
|
return self._mk_canvas(oob)
|
||||||
|
|
||||||
def refresh_properties(self, oob=False):
|
def refresh_properties(self, oob=False):
|
||||||
return self._mk_properties(oob)
|
return self._mk_properties(oob)
|
||||||
@@ -102,9 +112,16 @@ class WorkflowDesigner(BaseComponent):
|
|||||||
properties={"processor_name": PROCESSOR_TYPES[component_type][0]}
|
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._state.components[component_id] = component
|
||||||
self._db.save_state(self._key, self._state) # update db
|
self._db.save_state(self._key, self._state) # update db
|
||||||
return self.refresh_designer()
|
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()
|
||||||
|
|
||||||
def move_component(self, component_id, x, y):
|
def move_component(self, component_id, x, y):
|
||||||
if component_id in self._state.components:
|
if component_id in self._state.components:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from dataclasses import dataclass, field
|
|||||||
from components.workflows.constants import WORKFLOWS_DB_ENTRY, WORKFLOW_DESIGNER_DB_ENTRY, \
|
from components.workflows.constants import WORKFLOWS_DB_ENTRY, WORKFLOW_DESIGNER_DB_ENTRY, \
|
||||||
WORKFLOW_DESIGNER_DB_SETTINGS_ENTRY, WORKFLOW_DESIGNER_DB_STATE_ENTRY
|
WORKFLOW_DESIGNER_DB_SETTINGS_ENTRY, WORKFLOW_DESIGNER_DB_STATE_ENTRY
|
||||||
from core.settings_management import SettingsManager
|
from core.settings_management import SettingsManager
|
||||||
|
from core.utils import make_safe_id
|
||||||
|
|
||||||
logger = logging.getLogger("WorkflowsSettings")
|
logger = logging.getLogger("WorkflowsSettings")
|
||||||
|
|
||||||
@@ -160,7 +161,7 @@ class WorkflowsDesignerDbManager:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_db_entry(key):
|
def _get_db_entry(key):
|
||||||
return f"{WORKFLOW_DESIGNER_DB_ENTRY}_{key}"
|
return make_safe_id(f"{WORKFLOW_DESIGNER_DB_ENTRY}_{key}")
|
||||||
|
|
||||||
def save_settings(self, key: str, settings: WorkflowsDesignerSettings):
|
def save_settings(self, key: str, settings: WorkflowsDesignerSettings):
|
||||||
self._settings_manager.put(self._session,
|
self._settings_manager.put(self._session,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from my_mocks import tabs_manager
|
|||||||
|
|
||||||
class UndoableCommand(CommandHistory):
|
class UndoableCommand(CommandHistory):
|
||||||
def __init__(self, old_value=0, new_value=0):
|
def __init__(self, old_value=0, new_value=0):
|
||||||
super().__init__("command", f"The new value is {new_value}")
|
super().__init__("Set new value", lambda value: f"Setting new value to {value}", None)
|
||||||
self.old_value = old_value
|
self.old_value = old_value
|
||||||
self.new_value = new_value
|
self.new_value = new_value
|
||||||
|
|
||||||
@@ -31,8 +31,8 @@ def undo_redo(session, tabs_manager):
|
|||||||
def test_i_can_render(undo_redo):
|
def test_i_can_render(undo_redo):
|
||||||
actual = undo_redo.__ft__()
|
actual = undo_redo.__ft__()
|
||||||
expected = Div(
|
expected = Div(
|
||||||
Div(div_icon("undo", cls=Contains("mmt-btn-disabled")), data_tooltip="Nothing to undo"),
|
Div(div_icon("undo", cls=Contains("mmt-btn-disabled")), data_tooltip="Nothing to undo."),
|
||||||
Div(div_icon("redo", cls=Contains("mmt-btn-disabled")), data_tooltip="Nothing to redo"),
|
Div(div_icon("redo", cls=Contains("mmt-btn-disabled")), data_tooltip="Nothing to redo."),
|
||||||
id=undo_redo.get_id(),
|
id=undo_redo.get_id(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,13 +40,13 @@ def test_i_can_render(undo_redo):
|
|||||||
|
|
||||||
|
|
||||||
def test_i_can_render_when_undoing_and_redoing(undo_redo):
|
def test_i_can_render_when_undoing_and_redoing(undo_redo):
|
||||||
undo_redo.push(UndoableCommand())
|
undo_redo.push(UndoableCommand(0, 1))
|
||||||
undo_redo.push(UndoableCommand())
|
undo_redo.push(UndoableCommand(1, 2))
|
||||||
|
|
||||||
actual = undo_redo.__ft__()
|
actual = undo_redo.__ft__()
|
||||||
expected = Div(
|
expected = Div(
|
||||||
Div(div_icon("undo", cls=DoesNotContain("mmt-btn-disabled")), data_tooltip="Undo "),
|
Div(div_icon("undo", cls=DoesNotContain("mmt-btn-disabled")), data_tooltip="Undo 'Set new value'."),
|
||||||
Div(div_icon("redo", cls=Contains("mmt-btn-disabled")), data_tooltip=""),
|
Div(div_icon("redo", cls=Contains("mmt-btn-disabled")), data_tooltip="Nothing to redo."),
|
||||||
id=undo_redo.get_id(),
|
id=undo_redo.get_id(),
|
||||||
)
|
)
|
||||||
assert matches(actual, expected)
|
assert matches(actual, expected)
|
||||||
@@ -54,8 +54,8 @@ def test_i_can_render_when_undoing_and_redoing(undo_redo):
|
|||||||
undo_redo.undo() # The command is now undone. We can redo it and undo the first command.
|
undo_redo.undo() # The command is now undone. We can redo it and undo the first command.
|
||||||
actual = undo_redo.__ft__()
|
actual = undo_redo.__ft__()
|
||||||
expected = Div(
|
expected = Div(
|
||||||
div_icon("undo", cls=DoesNotContain("mmt-btn-disabled")),
|
Div(div_icon("undo", cls=DoesNotContain("mmt-btn-disabled")), data_tooltip="Undo 'Set new value'."),
|
||||||
div_icon("redo", cls=DoesNotContain("mmt-btn-disabled")),
|
Div(div_icon("redo", cls=DoesNotContain("mmt-btn-disabled")), data_tooltip="Redo 'Set new value'."),
|
||||||
id=undo_redo.get_id(),
|
id=undo_redo.get_id(),
|
||||||
)
|
)
|
||||||
assert matches(actual, expected)
|
assert matches(actual, expected)
|
||||||
@@ -63,8 +63,8 @@ def test_i_can_render_when_undoing_and_redoing(undo_redo):
|
|||||||
undo_redo.undo() # Undo again, I cannot undo anymore.
|
undo_redo.undo() # Undo again, I cannot undo anymore.
|
||||||
actual = undo_redo.__ft__()
|
actual = undo_redo.__ft__()
|
||||||
expected = Div(
|
expected = Div(
|
||||||
div_icon("undo", cls=Contains("mmt-btn-disabled")),
|
Div(div_icon("undo", cls=Contains("mmt-btn-disabled"))),
|
||||||
div_icon("redo", cls=DoesNotContain("mmt-btn-disabled")),
|
Div(div_icon("redo", cls=DoesNotContain("mmt-btn-disabled"))),
|
||||||
id=undo_redo.get_id(),
|
id=undo_redo.get_id(),
|
||||||
)
|
)
|
||||||
assert matches(actual, expected)
|
assert matches(actual, expected)
|
||||||
@@ -72,8 +72,8 @@ def test_i_can_render_when_undoing_and_redoing(undo_redo):
|
|||||||
undo_redo.redo() # Redo once.
|
undo_redo.redo() # Redo once.
|
||||||
actual = undo_redo.__ft__()
|
actual = undo_redo.__ft__()
|
||||||
expected = Div(
|
expected = Div(
|
||||||
div_icon("undo", cls=DoesNotContain("mmt-btn-disabled")),
|
Div(div_icon("undo", cls=DoesNotContain("mmt-btn-disabled"))),
|
||||||
div_icon("redo", cls=DoesNotContain("mmt-btn-disabled")),
|
Div(div_icon("redo", cls=DoesNotContain("mmt-btn-disabled"))),
|
||||||
id=undo_redo.get_id(),
|
id=undo_redo.get_id(),
|
||||||
)
|
)
|
||||||
assert matches(actual, expected)
|
assert matches(actual, expected)
|
||||||
@@ -81,8 +81,8 @@ def test_i_can_render_when_undoing_and_redoing(undo_redo):
|
|||||||
undo_redo.redo() # Redo a second time.
|
undo_redo.redo() # Redo a second time.
|
||||||
actual = undo_redo.__ft__()
|
actual = undo_redo.__ft__()
|
||||||
expected = Div(
|
expected = Div(
|
||||||
div_icon("undo", cls=DoesNotContain("mmt-btn-disabled")),
|
Div(div_icon("undo", cls=DoesNotContain("mmt-btn-disabled"))),
|
||||||
div_icon("redo", cls=Contains("mmt-btn-disabled")),
|
Div(div_icon("redo", cls=Contains("mmt-btn-disabled"))),
|
||||||
id=undo_redo.get_id(),
|
id=undo_redo.get_id(),
|
||||||
)
|
)
|
||||||
assert matches(actual, expected)
|
assert matches(actual, expected)
|
||||||
@@ -99,3 +99,14 @@ def test_i_can_undo_and_redo(undo_redo):
|
|||||||
self, res = undo_redo.redo()
|
self, res = undo_redo.redo()
|
||||||
expected = Div(2, hx_swap_oob="true")
|
expected = Div(2, hx_swap_oob="true")
|
||||||
assert matches(res, expected)
|
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))
|
||||||
|
undo_redo.push(UndoableCommand(2, 3))
|
||||||
|
|
||||||
|
undo_redo.undo()
|
||||||
|
undo_redo.undo()
|
||||||
|
undo_redo.push(UndoableCommand(1, 5))
|
||||||
|
|
||||||
|
assert len(undo_redo.history) == 2
|
||||||
Reference in New Issue
Block a user