Working on undo redo capabilities

This commit is contained in:
2025-07-24 23:41:27 +02:00
parent 1ceddfac7c
commit aa8aa8f58c
14 changed files with 203 additions and 7 deletions

View File

@@ -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 {

View File

@@ -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'),

View File

@@ -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()

View File

View File

@@ -0,0 +1,7 @@
from fastcore.basics import NotStr
# carbon Undo
icon_undo = NotStr("""<svg name="undo" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 32 32"><path d="M20 10H7.815l3.587-3.586L10 5l-6 6l6 6l1.402-1.415L7.818 12H20a6 6 0 0 1 0 12h-8v2h8a8 8 0 0 0 0-16z" fill="currentColor"></path></svg>""")
# carbon Redo
icon_redo = NotStr("""<svg name="redo" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 32 32"><path d="M12 10h12.185l-3.587-3.586L22 5l6 6l-6 6l-1.402-1.415L24.182 12H12a6 6 0 0 0 0 12h8v2h-8a8 8 0 0 1 0-16z" fill="currentColor"></path></svg>""")

View File

@@ -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}"}}',
}

View File

@@ -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

View File

@@ -0,0 +1,8 @@
UNDO_REDO_INSTANCE_ID = "__UndoRedo__"
ROUTE_ROOT = "/undo"
class Routes:
Undo = "/undo"
Redo = "/redo"

View File

@@ -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)

View File

@@ -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")

View File

@@ -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))

15
tests/test_undo_redo.py Normal file
View File

@@ -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