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