diff --git a/src/assets/main.css b/src/assets/main.css
index 592d9f1..d609fc4 100644
--- a/src/assets/main.css
+++ b/src/assets/main.css
@@ -8,6 +8,7 @@
--datagrid-resize-zindex: 1;
--color-splitter: color-mix(in oklab, var(--color-base-content) 50%, #0000);
--color-splitter-active: color-mix(in oklab, var(--color-base-content) 50%, #ffff);
+ --color-btn-hover: color-mix(in oklab, var(--btn-color, var(--color-base-200)), #000 7%);
}
.mmt-tooltip-container {
@@ -36,6 +37,19 @@
transition: opacity 0.2s ease, visibility 0s linear 0.2s;
}
+.mmt-btn {
+ user-select: none;
+ border-style: solid;
+}
+
+.mmt-btn:hover {
+ background-color: var(--color-btn-hover);
+}
+
+.mmt-btn-disabled {
+ opacity: 0.5;
+ /*cursor: not-allowed;*/
+}
/* When parent is hovered, show the child elements with this class */
*:hover > .mmt-visible-on-hover {
@@ -63,6 +77,8 @@
width: 24px;
min-width: 24px;
height: 24px;
+ margin-top: auto;
+ margin-bottom: auto;
}
.icon-24 svg {
diff --git a/src/components/drawerlayout/components/DrawerLayout.py b/src/components/drawerlayout/components/DrawerLayout.py
index 1dc10f4..8e373f6 100644
--- a/src/components/drawerlayout/components/DrawerLayout.py
+++ b/src/components/drawerlayout/components/DrawerLayout.py
@@ -11,6 +11,7 @@ from components.drawerlayout.assets.icons import icon_panel_contract_regular, ic
from components.drawerlayout.constants import DRAWER_LAYOUT_INSTANCE_ID
from components.repositories.components.Repositories import Repositories
from components.tabs.components.MyTabs import MyTabs
+from components.undo_redo.components.UndoRedo import UndoRedo
from components.workflows.components.Workflows import Workflows
from core.instance_manager import InstanceManager
from core.settings_management import SettingsManager
@@ -31,6 +32,7 @@ class DrawerLayout(BaseComponent):
self._ai_buddy = self._create_component(AIBuddy)
self._admin = self._create_component(Admin)
self._applications = self._create_component(Applications)
+ self._undo_redo = self._create_component(UndoRedo)
self.top_components = self._get_sub_components("TOP", [self._ai_buddy])
self.bottom_components = self._get_sub_components("BOTTOM", [self._ai_buddy])
@@ -53,12 +55,16 @@ class DrawerLayout(BaseComponent):
name="sidebar"
),
Div(
- Label(
- Input(type="checkbox",
- onclick=f"document.getElementById('sidebar_{self._id}').classList.toggle('collapsed');"),
- icon_panel_contract_regular,
- icon_panel_expand_regular,
- cls="swap",
+ Div(
+ Label(
+ Input(type="checkbox",
+ onclick=f"document.getElementById('sidebar_{self._id}').classList.toggle('collapsed');"),
+ icon_panel_contract_regular,
+ icon_panel_expand_regular,
+ cls="swap mr-4",
+ ),
+ self._undo_redo,
+ cls="flex"
),
Div(*[component for component in self.top_components], name="top", cls='dl-top'),
Div(self._tabs, id=f"page_{self._id}", name="page", cls='dl-page'),
diff --git a/src/components/undo_redo/UndoRedoApp.py b/src/components/undo_redo/UndoRedoApp.py
new file mode 100644
index 0000000..f8f6ef3
--- /dev/null
+++ b/src/components/undo_redo/UndoRedoApp.py
@@ -0,0 +1,23 @@
+import logging
+
+from fasthtml.fastapp import fast_app
+
+from components.undo_redo.constants import Routes
+from core.instance_manager import debug_session, InstanceManager
+
+logger = logging.getLogger("UndoRedoApp")
+
+undo_redo_app, rt = fast_app()
+
+
+@rt(Routes.Undo)
+def post(session, _id: str):
+ logger.debug(f"Entering {Routes.Undo} with args {debug_session(session)}, {_id=}")
+ instance = InstanceManager.get(session, _id)
+ return instance.undo()
+
+@rt(Routes.Redo)
+def post(session, _id: str):
+ logger.debug(f"Entering {Routes.Redo} with args {debug_session(session)}, {_id=}")
+ instance = InstanceManager.get(session, _id)
+ return instance.redo()
\ No newline at end of file
diff --git a/src/components/undo_redo/__init__.py b/src/components/undo_redo/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/components/undo_redo/assets/__init__.py b/src/components/undo_redo/assets/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/components/undo_redo/assets/icons.py b/src/components/undo_redo/assets/icons.py
new file mode 100644
index 0000000..e35de68
--- /dev/null
+++ b/src/components/undo_redo/assets/icons.py
@@ -0,0 +1,7 @@
+from fastcore.basics import NotStr
+
+# carbon Undo
+icon_undo = NotStr("""""")
+
+# carbon Redo
+icon_redo = NotStr("""""")
\ No newline at end of file
diff --git a/src/components/undo_redo/commands.py b/src/components/undo_redo/commands.py
new file mode 100644
index 0000000..ffc75d2
--- /dev/null
+++ b/src/components/undo_redo/commands.py
@@ -0,0 +1,25 @@
+from components.BaseCommandManager import BaseCommandManager
+from components.undo_redo.constants import ROUTE_ROOT, Routes
+
+
+class UndoRedoCommandManager(BaseCommandManager):
+ def __init__(self, owner):
+ super().__init__(owner)
+
+ def undo(self):
+ return {
+ "hx-post": f"{ROUTE_ROOT}{Routes.Undo}",
+ "hx-trigger": "click, keyup[ctrlKey&&key=='z'] from:body",
+ "hx-target": f"#{self._id}",
+ "hx-swap": "innerHTML",
+ "hx-vals": f'{{"_id": "{self._id}"}}',
+ }
+
+ def redo(self):
+ return {
+ "hx-post": f"{ROUTE_ROOT}{Routes.Redo}",
+ "hx_trigger": "click, keyup[ctrlKey&&key=='y'] from:body",
+ "hx-target": f"#{self._id}",
+ "hx-swap": "innerHTML",
+ "hx-vals": f'{{"_id": "{self._id}"}}',
+ }
diff --git a/src/components/undo_redo/components/UndoRedo.py b/src/components/undo_redo/components/UndoRedo.py
new file mode 100644
index 0000000..53ef27e
--- /dev/null
+++ b/src/components/undo_redo/components/UndoRedo.py
@@ -0,0 +1,89 @@
+import logging
+from abc import ABC, abstractmethod
+
+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_helpers import mk_icon
+
+logger = logging.getLogger("UndoRedoApp")
+
+
+class CommandHistory(ABC):
+ def __init__(self, name, desc):
+ self.name = name
+ self.desc = desc
+
+ @abstractmethod
+ def undo(self):
+ pass
+
+ @abstractmethod
+ def redo(self):
+ pass
+
+
+class UndoRedo(BaseComponentSingleton):
+ COMPONENT_INSTANCE_ID = UNDO_REDO_INSTANCE_ID
+
+ def __init__(self, session, _id, settings_manager=None, tabs_manager=None):
+ super().__init__(session, _id, settings_manager, tabs_manager)
+ self.index = 0
+ self.history = []
+ self._commands = UndoRedoCommandManager(self)
+
+ def push(self, command: CommandHistory):
+ self.history.append(command)
+ self.index += 1
+
+ def undo(self):
+ logger.info(f"Undo command")
+ return self
+
+ def redo(self):
+ logger.info("Redo command")
+ if self.index > 0:
+ self.history.clear()
+ self.index = 0
+ else:
+ self.push("something")
+ return self
+
+ def __ft__(self):
+ return Div(
+ self._mk_undo(),
+ self._mk_redo(),
+ id=self._id,
+ cls="flex"
+ )
+
+ def _mk_undo(self):
+ if self._can_undo():
+ return mk_icon(icon_undo,
+ size=24,
+ **self._commands.undo())
+ else:
+ return mk_icon(icon_undo,
+ size=24,
+ can_select=False,
+ cls="mmt-btn-disabled")
+
+ def _mk_redo(self):
+ if self._can_redo():
+ return mk_icon(icon_redo,
+ size=24,
+ **self._commands.redo())
+ else:
+ return mk_icon(icon_redo,
+ size=24,
+ can_select=False,
+ cls="mmt-btn-disabled")
+
+ def _can_undo(self):
+ return self.index > 0
+
+ def _can_redo(self):
+ return self.index < len(self.history) - 1
diff --git a/src/components/undo_redo/components/__init__.py b/src/components/undo_redo/components/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/components/undo_redo/constants.py b/src/components/undo_redo/constants.py
new file mode 100644
index 0000000..52e7d9b
--- /dev/null
+++ b/src/components/undo_redo/constants.py
@@ -0,0 +1,8 @@
+UNDO_REDO_INSTANCE_ID = "__UndoRedo__"
+
+ROUTE_ROOT = "/undo"
+
+
+class Routes:
+ Undo = "/undo"
+ Redo = "/redo"
\ No newline at end of file
diff --git a/src/components_helpers.py b/src/components_helpers.py
index 38bcdfd..857ba9a 100644
--- a/src/components_helpers.py
+++ b/src/components_helpers.py
@@ -3,9 +3,10 @@ from fasthtml.components import *
from core.utils import merge_classes
-def mk_icon(icon, size=20, can_select=True, cls='', tooltip=None, **kwargs):
+def mk_icon(icon, size=20, can_select=True, can_hover=False, cls='', tooltip=None, **kwargs):
merged_cls = merge_classes(f"icon-{size}",
'icon-btn' if can_select else '',
+ 'mmt-btn' if can_hover else '',
cls,
kwargs)
return mk_tooltip(icon, tooltip, cls=merged_cls, **kwargs) if tooltip else Div(icon, cls=merged_cls, **kwargs)
diff --git a/src/main.py b/src/main.py
index a88a769..f7df448 100644
--- a/src/main.py
+++ b/src/main.py
@@ -145,6 +145,7 @@ register_component("login", "components.login", "LoginApp")
register_component("register", "components.register", "RegisterApp")
register_component("theme_controller", "components.themecontroller", "ThemeControllerApp")
register_component("main_layout", "components.drawerlayout", "DrawerLayoutApp")
+register_component("undo_redo", "components.undo_redo", "UndoRedoApp")
register_component("tabs", "components.tabs", "TabsApp") # before repositories
register_component("applications", "components.applications", "ApplicationsApp")
register_component("repositories", "components.repositories", "RepositoriesApp")
diff --git a/src/utils/ComponentsInstancesHelper.py b/src/utils/ComponentsInstancesHelper.py
index 01baf1f..c8dfe73 100644
--- a/src/utils/ComponentsInstancesHelper.py
+++ b/src/utils/ComponentsInstancesHelper.py
@@ -1,4 +1,5 @@
from components.repositories.components.Repositories import Repositories
+from components.undo_redo.components.UndoRedo import UndoRedo
from core.instance_manager import InstanceManager
@@ -6,4 +7,8 @@ class ComponentsInstancesHelper:
@staticmethod
def get_repositories(session):
return InstanceManager.get(session, Repositories.create_component_id(session))
+
+ @staticmethod
+ def get_undo_redo(session):
+ return InstanceManager.get(session, UndoRedo.create_component_id(session))
\ No newline at end of file
diff --git a/tests/test_undo_redo.py b/tests/test_undo_redo.py
new file mode 100644
index 0000000..62dc5fa
--- /dev/null
+++ b/tests/test_undo_redo.py
@@ -0,0 +1,15 @@
+from components.undo_redo.components.UndoRedo import UndoRedo
+from core.settings_management import SettingsManager, MemoryDbEngine
+
+TEST_UNDO_REDO_INSTANCE_ID = "test_undo_redo_instance_id"
+
+@pytest.fixture
+def undo_redo(session, tabs_manager):
+ return UndoRedo(session,
+ UndoRedo.create_component_id(session),
+ settings_manager=SettingsManager(engine=MemoryDbEngine()),
+ tabs_manager=tabs_manager)
+
+
+def test_i_can_undo_and_redo(undo_redo):
+ pass