Fixed bugs in DataGridFormattingManager
This commit is contained in:
@@ -80,8 +80,7 @@ def index(session):
|
|||||||
formatting_manager = DataGridFormattingManager(layout)
|
formatting_manager = DataGridFormattingManager(layout)
|
||||||
btn_show_formatting_manager = mk.label("Formatting",
|
btn_show_formatting_manager = mk.label("Formatting",
|
||||||
icon=text_edit_style20_regular,
|
icon=text_edit_style20_regular,
|
||||||
command=add_tab("Formatting", formatting_manager),
|
command=add_tab("Formatting", formatting_manager))
|
||||||
id=formatting_manager.get_id())
|
|
||||||
layout.left_drawer.add(btn_show_formatting_manager, "Parameters")
|
layout.left_drawer.add(btn_show_formatting_manager, "Parameters")
|
||||||
|
|
||||||
layout.left_drawer.add(btn_file_upload, "Test")
|
layout.left_drawer.add(btn_file_upload, "Test")
|
||||||
|
|||||||
85
src/myfasthtml/assets/core/formatting_manager.css
Normal file
85
src/myfasthtml/assets/core/formatting_manager.css
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
/* ============================================= */
|
||||||
|
/* ======== DataGridFormattingManager ========== */
|
||||||
|
/* ============================================= */
|
||||||
|
|
||||||
|
.mf-formatting-manager {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Preset list items ---- */
|
||||||
|
|
||||||
|
.mf-fmgr-preset-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
transition: background-color 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mf-fmgr-preset-item:hover {
|
||||||
|
background-color: var(--color-button-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mf-fmgr-preset-item-active {
|
||||||
|
background-color: color-mix(in oklab, var(--color-primary) 15%, #0000);
|
||||||
|
border-color: color-mix(in oklab, var(--color-primary) 40%, #0000);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.mf-fmgr-preset-name {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mf-fmgr-preset-desc {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
opacity: 0.55;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mf-fmgr-preset-badges {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Editor panel ---- */
|
||||||
|
|
||||||
|
.mf-fmgr-editor-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mf-fmgr-editor-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mf-fmgr-placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- New / Rename form ---- */
|
||||||
|
|
||||||
|
.mf-fmgr-form {
|
||||||
|
max-width: 480px;
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
--color-border: color-mix(in oklab, var(--color-base-content) 20%, #0000);
|
--color-border: color-mix(in oklab, var(--color-base-content) 20%, #0000);
|
||||||
--color-resize: color-mix(in oklab, var(--color-base-content) 50%, #0000);
|
--color-resize: color-mix(in oklab, var(--color-base-content) 50%, #0000);
|
||||||
--color-selection: color-mix(in oklab, var(--color-primary) 20%, #0000);
|
--color-selection: color-mix(in oklab, var(--color-primary) 20%, #0000);
|
||||||
|
--color-button-hover: var(--color-base-300);
|
||||||
|
|
||||||
--datagrid-resize-zindex: 1;
|
--datagrid-resize-zindex: 1;
|
||||||
--font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
--font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||||
@@ -58,7 +59,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mf-button:hover {
|
.mf-button:hover {
|
||||||
background-color: var(--color-base-300);
|
background-color: var(--color-button-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mf-tooltip-container {
|
.mf-tooltip-container {
|
||||||
|
|||||||
@@ -108,6 +108,7 @@
|
|||||||
|
|
||||||
.mf-panel-content {
|
.mf-panel-content {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Remove padding-top when using title layout */
|
/* Remove padding-top when using title layout */
|
||||||
@@ -115,3 +116,12 @@
|
|||||||
.mf-panel-right.mf-panel-with-title {
|
.mf-panel-right.mf-panel-with-title {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mf-panel-body.mf-panel-body-right {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mf-panel-body.mf-panel-body-left {
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
|
|
||||||
.mf-treenode:hover {
|
.mf-treenode:hover {
|
||||||
background-color: var(--color-base-200);
|
background-color: var(--color-button-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mf-treenode.selected {
|
.mf-treenode.selected {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional, Literal
|
||||||
|
|
||||||
from fasthtml.common import Form, Fieldset, Label, Input, Span
|
from fasthtml.common import Form, Fieldset, Label, Input, Span
|
||||||
from fasthtml.components import Div
|
from fasthtml.components import Div
|
||||||
@@ -14,378 +14,406 @@ from myfasthtml.controls.helpers import mk
|
|||||||
from myfasthtml.core.commands import Command
|
from myfasthtml.core.commands import Command
|
||||||
from myfasthtml.core.dbmanager import DbObject
|
from myfasthtml.core.dbmanager import DbObject
|
||||||
from myfasthtml.core.formatting.dataclasses import RulePreset
|
from myfasthtml.core.formatting.dataclasses import RulePreset
|
||||||
|
from myfasthtml.core.dsls import DslsManager
|
||||||
from myfasthtml.core.formatting.dsl import parse_dsl
|
from myfasthtml.core.formatting.dsl import parse_dsl
|
||||||
|
from myfasthtml.core.formatting.dsl.completion.FormattingCompletionEngine import FormattingCompletionEngine
|
||||||
|
from myfasthtml.core.formatting.dsl.completion.provider import DatagridMetadataProvider
|
||||||
from myfasthtml.core.formatting.dsl.definition import FormattingDSL
|
from myfasthtml.core.formatting.dsl.definition import FormattingDSL
|
||||||
|
from myfasthtml.core.formatting.dsl.parser import DSLParser
|
||||||
from myfasthtml.core.formatting.presets import DEFAULT_RULE_PRESETS
|
from myfasthtml.core.formatting.presets import DEFAULT_RULE_PRESETS
|
||||||
from myfasthtml.core.instances import SingleInstance
|
from myfasthtml.core.instances import SingleInstance
|
||||||
|
|
||||||
logger = logging.getLogger("DataGridFormattingManager")
|
logger = logging.getLogger("DataGridFormattingManager")
|
||||||
|
|
||||||
_DSL_PLACEHOLDER = 'tables:\n style("error") if value < 0\n style("success") if value > 0'
|
|
||||||
|
|
||||||
|
|
||||||
class DataGridFormattingManagerState(DbObject):
|
class DataGridFormattingManagerState(DbObject):
|
||||||
def __init__(self, owner):
|
def __init__(self, owner):
|
||||||
with self.initializing():
|
with self.initializing():
|
||||||
super().__init__(owner)
|
super().__init__(owner)
|
||||||
self.presets: list = []
|
self.presets: list = []
|
||||||
self.selected_name: Optional[str] = None
|
self.selected_name: Optional[str] = None
|
||||||
self.ns_mode: str = "view"
|
self.ns_mode: str = "view"
|
||||||
|
|
||||||
|
|
||||||
class Commands(BaseCommands):
|
class Commands(BaseCommands):
|
||||||
def new_preset(self):
|
def new_preset(self):
|
||||||
return Command(
|
return Command(
|
||||||
"NewPreset",
|
"NewPreset",
|
||||||
"New preset",
|
"New preset",
|
||||||
self._owner,
|
self._owner,
|
||||||
self._owner.on_new_preset,
|
self._owner.handle_new_preset,
|
||||||
icon=IconsHelper.get("Add20Regular"),
|
icon=IconsHelper.get("add_circle20_regular"),
|
||||||
).htmx(target=f"#{self._id}", swap="outerHTML")
|
).htmx(target=f"#{self._id}", swap="outerHTML")
|
||||||
|
|
||||||
def save_preset(self):
|
def save_preset(self):
|
||||||
return Command(
|
return Command(
|
||||||
"SavePreset",
|
"SavePreset",
|
||||||
"Save preset",
|
"Save preset",
|
||||||
self._owner,
|
self._owner,
|
||||||
self._owner.on_save_preset,
|
self._owner.handle_save_preset,
|
||||||
icon=IconsHelper.get("Save20Regular"),
|
icon=IconsHelper.get("Save20Regular"),
|
||||||
).htmx(target=f"#{self._id}", swap="outerHTML")
|
).htmx(target=f"#{self._id}", swap="outerHTML")
|
||||||
|
|
||||||
def rename_preset(self):
|
def rename_preset(self):
|
||||||
return Command(
|
return Command(
|
||||||
"RenamePreset",
|
"RenamePreset",
|
||||||
"Rename preset",
|
"Rename preset",
|
||||||
self._owner,
|
self._owner,
|
||||||
self._owner.on_rename_preset,
|
self._owner.handle_rename_preset,
|
||||||
icon=IconsHelper.get("Rename20Regular"),
|
icon=IconsHelper.get("edit20_regular"),
|
||||||
).htmx(target=f"#{self._id}", swap="outerHTML")
|
).htmx(target=f"#{self._id}", swap="outerHTML")
|
||||||
|
|
||||||
def delete_preset(self):
|
def delete_preset(self):
|
||||||
return Command(
|
return Command(
|
||||||
"DeletePreset",
|
"DeletePreset",
|
||||||
"Delete preset",
|
"Delete preset",
|
||||||
self._owner,
|
self._owner,
|
||||||
self._owner.on_delete_preset,
|
self._owner.handle_delete_preset,
|
||||||
icon=IconsHelper.get("Delete20Regular"),
|
icon=IconsHelper.get("Delete20Regular"),
|
||||||
).htmx(target=f"#{self._id}", swap="outerHTML")
|
).htmx(target=f"#{self._id}", swap="outerHTML")
|
||||||
|
|
||||||
def confirm_new(self):
|
def confirm_new(self):
|
||||||
return Command(
|
return Command(
|
||||||
"ConfirmNew",
|
"ConfirmNew",
|
||||||
"Confirm new preset",
|
"Confirm new preset",
|
||||||
self._owner,
|
self._owner,
|
||||||
self._owner.on_confirm_new,
|
self._owner.handle_confirm_new,
|
||||||
).htmx(target=f"#{self._id}", swap="outerHTML")
|
).htmx(target=f"#{self._id}", swap="outerHTML")
|
||||||
|
|
||||||
def confirm_rename(self):
|
def confirm_rename(self):
|
||||||
return Command(
|
return Command(
|
||||||
"ConfirmRename",
|
"ConfirmRename",
|
||||||
"Confirm rename",
|
"Confirm rename",
|
||||||
self._owner,
|
self._owner,
|
||||||
self._owner.on_confirm_rename,
|
self._owner.handle_confirm_rename,
|
||||||
).htmx(target=f"#{self._id}", swap="outerHTML")
|
).htmx(target=f"#{self._id}", swap="outerHTML")
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self):
|
||||||
return Command(
|
return Command(
|
||||||
"Cancel",
|
"Cancel",
|
||||||
"Cancel",
|
"Cancel",
|
||||||
self._owner,
|
self._owner,
|
||||||
self._owner.on_cancel,
|
self._owner.handle_cancel,
|
||||||
).htmx(target=f"#{self._id}", swap="outerHTML")
|
).htmx(target=f"#{self._id}", swap="outerHTML")
|
||||||
|
|
||||||
|
def select_preset(self, name: str):
|
||||||
|
return Command(
|
||||||
|
"SelectPreset",
|
||||||
|
f"Select {name}",
|
||||||
|
self._owner,
|
||||||
|
self._owner.handle_select_preset,
|
||||||
|
args=[name],
|
||||||
|
).htmx(target=f"#{self._id}", swap="outerHTML")
|
||||||
|
|
||||||
|
|
||||||
class DataGridFormattingManager(SingleInstance):
|
class DataGridFormattingManager(SingleInstance):
|
||||||
def __init__(self, parent, _id=None):
|
def __init__(self, parent, _id=None):
|
||||||
super().__init__(parent, _id=_id)
|
super().__init__(parent, _id=_id)
|
||||||
self._state = DataGridFormattingManagerState(self)
|
self._state = DataGridFormattingManagerState(self)
|
||||||
self.commands = Commands(self)
|
self.commands = Commands(self)
|
||||||
|
|
||||||
self._panel = Panel(
|
self._panel = Panel(
|
||||||
self,
|
self,
|
||||||
conf=PanelConf(left=True, right=False, left_title="Presets"),
|
conf=PanelConf(left=True, right=False, left_title="Presets"),
|
||||||
_id="-panel",
|
_id="-panel",
|
||||||
)
|
)
|
||||||
self._menu = Menu(
|
self._menu = Menu(
|
||||||
self,
|
self,
|
||||||
conf=MenuConf(fixed_items=["NewPreset", "SavePreset", "RenamePreset", "DeletePreset"]),
|
conf=MenuConf(fixed_items=["NewPreset", "SavePreset", "RenamePreset", "DeletePreset"]),
|
||||||
save_state=False,
|
save_state=False,
|
||||||
_id="-menu",
|
_id="-menu",
|
||||||
)
|
)
|
||||||
self._search = Search(
|
self._search = Search(
|
||||||
self,
|
self,
|
||||||
items_names="Presets",
|
items_names="Presets",
|
||||||
template=self._mk_preset_item,
|
template=self._mk_preset_item,
|
||||||
_id="-search",
|
_id="-search",
|
||||||
)
|
)
|
||||||
self._editor = DslEditor(
|
provider = DatagridMetadataProvider(self)
|
||||||
self,
|
completion_engine = FormattingCompletionEngine(provider, "")
|
||||||
dsl=FormattingDSL(),
|
DslsManager.register(completion_engine, DSLParser())
|
||||||
conf=DslEditorConf(
|
|
||||||
save_button=False,
|
|
||||||
autocompletion=False,
|
|
||||||
linting=False,
|
|
||||||
placeholder=_DSL_PLACEHOLDER,
|
|
||||||
),
|
|
||||||
save_state=False,
|
|
||||||
_id="-editor",
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.debug(f"DataGridFormattingManager created with id={self._id}")
|
self._editor = DslEditor(
|
||||||
|
self,
|
||||||
|
dsl=FormattingDSL(),
|
||||||
|
conf=DslEditorConf(
|
||||||
|
save_button=False,
|
||||||
|
autocompletion=True,
|
||||||
|
linting=True,
|
||||||
|
engine_id=completion_engine.get_id(),
|
||||||
|
),
|
||||||
|
save_state=False,
|
||||||
|
_id="-editor",
|
||||||
|
)
|
||||||
|
|
||||||
# === Helpers ===
|
self.handle_select_preset(self._state.selected_name)
|
||||||
|
self._sync_provider()
|
||||||
|
logger.debug(f"DataGridFormattingManager created with id={self._id}")
|
||||||
|
|
||||||
def _get_all_presets(self) -> list:
|
def get_main_content_id(self):
|
||||||
"""Returns builtin presets followed by user presets."""
|
return self._panel.get_ids().main
|
||||||
return list(DEFAULT_RULE_PRESETS.values()) + self._state.presets
|
|
||||||
|
|
||||||
def _is_builtin(self, name: str) -> bool:
|
# === Helpers ===
|
||||||
return name in DEFAULT_RULE_PRESETS
|
|
||||||
|
|
||||||
def _get_selected_preset(self) -> Optional[RulePreset]:
|
def _get_all_presets(self) -> list:
|
||||||
if not self._state.selected_name:
|
"""Returns builtin presets followed by user presets."""
|
||||||
return None
|
return list(DEFAULT_RULE_PRESETS.values()) + self._state.presets
|
||||||
for p in self._get_all_presets():
|
|
||||||
if p.name == self._state.selected_name:
|
|
||||||
return p
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _get_user_preset(self, name: str) -> Optional[RulePreset]:
|
def _is_builtin(self, name: str) -> bool:
|
||||||
for p in self._state.presets:
|
return name in DEFAULT_RULE_PRESETS
|
||||||
if p.name == name:
|
|
||||||
return p
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _parse_dsl_to_rules(self, dsl_text: str) -> list:
|
def _get_selected_preset(self) -> Optional[RulePreset]:
|
||||||
"""Parse DSL text and extract FormatRule objects, ignoring scopes."""
|
if not self._state.selected_name:
|
||||||
try:
|
return None
|
||||||
scoped_rules = parse_dsl(dsl_text)
|
for p in self._get_all_presets():
|
||||||
return [sr.rule for sr in scoped_rules]
|
if p.name == self._state.selected_name:
|
||||||
except Exception:
|
return p
|
||||||
return []
|
return None
|
||||||
|
|
||||||
# === Command handlers ===
|
def _get_user_preset(self, name: str) -> Optional[RulePreset]:
|
||||||
|
for p in self._state.presets:
|
||||||
|
if p.name == name:
|
||||||
|
return p
|
||||||
|
return None
|
||||||
|
|
||||||
def on_new_preset(self):
|
def _parse_dsl_to_rules(self, dsl_text: str) -> list:
|
||||||
self._state.ns_mode = "new"
|
"""Parse DSL text and extract FormatRule objects, ignoring scopes."""
|
||||||
return self.render()
|
try:
|
||||||
|
scoped_rules = parse_dsl(dsl_text)
|
||||||
|
return [sr.rule for sr in scoped_rules]
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
def on_save_preset(self):
|
def _sync_provider(self):
|
||||||
if not self._state.selected_name or self._is_builtin(self._state.selected_name):
|
"""Sync all presets (builtin + user) into the session-scoped metadata provider."""
|
||||||
return self.render()
|
provider = DatagridMetadataProvider(self)
|
||||||
preset = self._get_user_preset(self._state.selected_name)
|
provider.rule_presets = {p.name: p for p in self._get_all_presets()}
|
||||||
if preset is None:
|
|
||||||
return self.render()
|
|
||||||
dsl = self._editor.get_content()
|
|
||||||
preset.dsl = dsl
|
|
||||||
preset.rules = self._parse_dsl_to_rules(dsl)
|
|
||||||
logger.debug(f"Saved preset '{preset.name}' with {len(preset.rules)} rules")
|
|
||||||
return self.render()
|
|
||||||
|
|
||||||
def on_rename_preset(self):
|
# === Command handlers ===
|
||||||
if not self._state.selected_name or self._is_builtin(self._state.selected_name):
|
|
||||||
return self.render()
|
|
||||||
self._state.ns_mode = "rename"
|
|
||||||
return self.render()
|
|
||||||
|
|
||||||
def on_delete_preset(self):
|
def handle_new_preset(self):
|
||||||
if not self._state.selected_name or self._is_builtin(self._state.selected_name):
|
self._state.ns_mode = "new"
|
||||||
return self.render()
|
return self.render()
|
||||||
self._state.presets = [p for p in self._state.presets if p.name != self._state.selected_name]
|
|
||||||
self._state.selected_name = None
|
|
||||||
self._editor.set_content("")
|
|
||||||
self._editor.conf.readonly = False
|
|
||||||
logger.debug(f"Deleted preset '{self._state.selected_name}'")
|
|
||||||
return self.render()
|
|
||||||
|
|
||||||
def on_select_preset(self, name: str):
|
def handle_save_preset(self):
|
||||||
preset = None
|
if not self._state.selected_name or self._is_builtin(self._state.selected_name):
|
||||||
for p in self._get_all_presets():
|
return self.render()
|
||||||
if p.name == name:
|
preset = self._get_user_preset(self._state.selected_name)
|
||||||
preset = p
|
if preset is None:
|
||||||
break
|
return self.render()
|
||||||
if preset is None:
|
dsl = self._editor.get_content()
|
||||||
return self.render()
|
preset.dsl = dsl
|
||||||
self._state.selected_name = name
|
preset.rules = self._parse_dsl_to_rules(dsl)
|
||||||
self._state.ns_mode = "view"
|
self._state.save()
|
||||||
self._editor.set_content(preset.dsl)
|
self._sync_provider()
|
||||||
self._editor.conf.readonly = self._is_builtin(name)
|
logger.debug(f"Saved preset '{preset.name}' with {len(preset.rules)} rules")
|
||||||
logger.debug(f"Selected preset '{name}', readonly={self._editor.conf.readonly}")
|
return self.render()
|
||||||
return self.render()
|
|
||||||
|
|
||||||
def on_confirm_new(self, client_response):
|
def handle_rename_preset(self):
|
||||||
name = (client_response.get("name") or "").strip()
|
if not self._state.selected_name or self._is_builtin(self._state.selected_name):
|
||||||
description = (client_response.get("description") or "").strip()
|
return self.render()
|
||||||
|
self._state.ns_mode = "rename"
|
||||||
|
return self.render()
|
||||||
|
|
||||||
if not name:
|
def handle_delete_preset(self):
|
||||||
self._state.ns_mode = "view"
|
if not self._state.selected_name or self._is_builtin(self._state.selected_name):
|
||||||
return self.render()
|
return self.render()
|
||||||
|
deleted_name = self._state.selected_name
|
||||||
|
self._state.presets = [p for p in self._state.presets if p.name != deleted_name]
|
||||||
|
self._state.selected_name = None
|
||||||
|
self._editor.set_content("")
|
||||||
|
self._editor.conf.readonly = False
|
||||||
|
self._sync_provider()
|
||||||
|
logger.debug(f"Deleted preset '{deleted_name}'")
|
||||||
|
return self.render()
|
||||||
|
|
||||||
all_names = {p.name for p in self._get_all_presets()}
|
def handle_select_preset(self, name: str):
|
||||||
if name in all_names:
|
preset = None
|
||||||
logger.debug(f"Cannot create preset '{name}': name already exists")
|
for p in self._get_all_presets():
|
||||||
self._state.ns_mode = "view"
|
if p.name == name:
|
||||||
return self.render()
|
preset = p
|
||||||
|
break
|
||||||
|
if preset is None:
|
||||||
|
return None
|
||||||
|
self._state.selected_name = name
|
||||||
|
self._state.ns_mode = "view"
|
||||||
|
self._editor.set_content(preset.dsl)
|
||||||
|
self._editor.conf.readonly = self._is_builtin(name)
|
||||||
|
logger.debug(f"Selected preset '{name}', readonly={self._editor.conf.readonly}")
|
||||||
|
return self.render() # to also update the selected item in the search
|
||||||
|
|
||||||
new_preset = RulePreset(name=name, description=description, rules=[], dsl="")
|
def handle_confirm_new(self, client_response):
|
||||||
self._state.presets.append(new_preset)
|
name = (client_response.get("name") or "").strip()
|
||||||
self._state.selected_name = name
|
description = (client_response.get("description") or "").strip()
|
||||||
self._state.ns_mode = "view"
|
|
||||||
self._editor.set_content("")
|
|
||||||
self._editor.conf.readonly = False
|
|
||||||
logger.debug(f"Created preset '{name}'")
|
|
||||||
return self.render()
|
|
||||||
|
|
||||||
def on_confirm_rename(self, client_response):
|
if not name:
|
||||||
name = (client_response.get("name") or "").strip()
|
self._state.ns_mode = "view"
|
||||||
description = (client_response.get("description") or "").strip()
|
return self.render()
|
||||||
|
|
||||||
preset = self._get_user_preset(self._state.selected_name)
|
all_names = {p.name for p in self._get_all_presets()}
|
||||||
if not name or preset is None:
|
if name in all_names:
|
||||||
self._state.ns_mode = "view"
|
logger.debug(f"Cannot create preset '{name}': name already exists")
|
||||||
return self.render()
|
self._state.ns_mode = "view"
|
||||||
|
return self.render()
|
||||||
|
|
||||||
if name != preset.name:
|
new_preset = RulePreset(name=name, description=description, rules=[], dsl="")
|
||||||
all_names = {p.name for p in self._get_all_presets()}
|
self._state.presets.append(new_preset)
|
||||||
if name in all_names:
|
self._state.selected_name = name
|
||||||
logger.debug(f"Cannot rename to '{name}': name already exists")
|
self._state.ns_mode = "view"
|
||||||
self._state.ns_mode = "view"
|
self._editor.set_content("")
|
||||||
return self.render()
|
self._editor.conf.readonly = False
|
||||||
|
self._sync_provider()
|
||||||
|
logger.debug(f"Created preset '{name}'")
|
||||||
|
return self.render()
|
||||||
|
|
||||||
old_name = preset.name
|
def handle_confirm_rename(self, client_response):
|
||||||
preset.name = name
|
name = (client_response.get("name") or "").strip()
|
||||||
preset.description = description
|
description = (client_response.get("description") or "").strip()
|
||||||
self._state.selected_name = name
|
|
||||||
self._state.ns_mode = "view"
|
|
||||||
logger.debug(f"Renamed preset '{old_name}' → '{name}'")
|
|
||||||
return self.render()
|
|
||||||
|
|
||||||
def on_cancel(self):
|
preset = self._get_user_preset(self._state.selected_name)
|
||||||
|
if not name or preset is None:
|
||||||
|
self._state.ns_mode = "view"
|
||||||
|
return self.render()
|
||||||
|
|
||||||
|
if name != preset.name:
|
||||||
|
all_names = {p.name for p in self._get_all_presets()}
|
||||||
|
if name in all_names:
|
||||||
|
logger.debug(f"Cannot rename to '{name}': name already exists")
|
||||||
self._state.ns_mode = "view"
|
self._state.ns_mode = "view"
|
||||||
return self.render()
|
return self.render()
|
||||||
|
|
||||||
# === Rendering ===
|
old_name = preset.name
|
||||||
|
preset.name = name
|
||||||
|
preset.description = description
|
||||||
|
self._state.selected_name = name
|
||||||
|
self._state.ns_mode = "view"
|
||||||
|
self._sync_provider()
|
||||||
|
logger.debug(f"Renamed preset '{old_name}' → '{name}'")
|
||||||
|
return self.render()
|
||||||
|
|
||||||
def _mk_preset_item(self, preset: RulePreset):
|
def handle_cancel(self):
|
||||||
is_active = self._state.selected_name == preset.name
|
self._state.ns_mode = "view"
|
||||||
is_builtin = self._is_builtin(preset.name)
|
return self.render()
|
||||||
|
|
||||||
badges = []
|
# === Rendering ===
|
||||||
if preset.has_formatter():
|
|
||||||
badges.append(Span("format()", cls="badge badge-xs badge-secondary"))
|
|
||||||
if preset.has_style():
|
|
||||||
badges.append(Span("style()", cls="badge badge-xs badge-primary"))
|
|
||||||
if is_builtin:
|
|
||||||
badges.append(Span("built-in", cls="badge badge-xs badge-ghost"))
|
|
||||||
|
|
||||||
item_cls = "mf-fmgr-preset-item"
|
def _get_badges(self, preset: RulePreset, is_builtin: bool):
|
||||||
if is_active:
|
badges = []
|
||||||
item_cls += " mf-fmgr-preset-item-active"
|
if preset.has_formatter():
|
||||||
|
badges.append(self._mk_badge("format"))
|
||||||
|
if preset.has_style():
|
||||||
|
badges.append(self._mk_badge("style"))
|
||||||
|
if is_builtin:
|
||||||
|
badges.append(self._mk_badge("built-in"))
|
||||||
|
|
||||||
select_cmd = Command(
|
return badges
|
||||||
"SelectPreset",
|
|
||||||
f"Select {preset.name}",
|
|
||||||
self,
|
|
||||||
self.on_select_preset,
|
|
||||||
args=[preset.name],
|
|
||||||
).htmx(target=f"#{self._id}", swap="outerHTML")
|
|
||||||
|
|
||||||
return mk.mk(
|
def _mk_badge(self, text: Literal["format", "style", "built-in"]):
|
||||||
Div(
|
if text == "built-in":
|
||||||
Div(preset.name, cls="mf-fmgr-preset-name"),
|
return Span("built-in", cls="badge badge-xs badge-ghost")
|
||||||
Div(preset.description, cls="mf-fmgr-preset-desc") if preset.description else None,
|
if text == "style":
|
||||||
Div(*badges, cls="mf-fmgr-preset-badges") if badges else None,
|
return Span("style", cls="badge badge-xs badge-primary")
|
||||||
cls=item_cls,
|
return Span("format", cls="badge badge-xs badge-secondary")
|
||||||
),
|
|
||||||
command=select_cmd,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _mk_preset_header(self, preset: RulePreset, is_builtin: bool):
|
def _mk_preset_item(self, preset: RulePreset):
|
||||||
badges = []
|
is_active = self._state.selected_name == preset.name
|
||||||
if preset.has_formatter():
|
is_builtin = self._is_builtin(preset.name)
|
||||||
badges.append(Span("format()", cls="badge badge-secondary"))
|
badges = self._get_badges(preset, is_builtin)
|
||||||
if preset.has_style():
|
|
||||||
badges.append(Span("style()", cls="badge badge-primary"))
|
|
||||||
if is_builtin:
|
|
||||||
badges.append(Span("built-in", cls="badge badge-ghost"))
|
|
||||||
|
|
||||||
return Div(
|
item_cls = "mf-fmgr-preset-item"
|
||||||
Div(
|
if is_active:
|
||||||
Div(preset.name, cls="text-sm font-semibold"),
|
item_cls += " mf-fmgr-preset-item-active"
|
||||||
Div(preset.description, cls="text-xs opacity-50") if preset.description else None,
|
|
||||||
),
|
|
||||||
Div(*badges, cls="flex gap-1") if badges else None,
|
|
||||||
cls="mf-fmgr-editor-meta",
|
|
||||||
)
|
|
||||||
|
|
||||||
def _mk_editor_view(self):
|
return mk.mk(
|
||||||
preset = self._get_selected_preset()
|
Div(
|
||||||
if preset is None:
|
Div(preset.name, cls="mf-fmgr-preset-name"),
|
||||||
return Div("Select a preset to edit", cls="mf-fmgr-placeholder p-4 text-sm opacity-50")
|
Div(preset.description, cls="mf-fmgr-preset-desc") if preset.description else None,
|
||||||
is_builtin = self._is_builtin(preset.name)
|
Div(*badges, cls="mf-fmgr-preset-badges") if badges else None,
|
||||||
return Div(
|
cls=item_cls,
|
||||||
self._mk_preset_header(preset, is_builtin),
|
),
|
||||||
self._editor,
|
command=self.commands.select_preset(preset.name),
|
||||||
cls="mf-fmgr-editor-view",
|
)
|
||||||
)
|
|
||||||
|
|
||||||
def _mk_new_form(self):
|
def _mk_preset_header(self, preset: RulePreset, is_builtin: bool):
|
||||||
return Div(
|
badges = self._get_badges(preset, is_builtin)
|
||||||
Form(
|
|
||||||
Fieldset(
|
|
||||||
Label("Name"),
|
|
||||||
Input(name="name", cls="input input-sm w-full"),
|
|
||||||
Label("Description"),
|
|
||||||
Input(name="description", cls="input input-sm w-full"),
|
|
||||||
legend="New preset",
|
|
||||||
cls="fieldset border-base-300 rounded-box p-4",
|
|
||||||
),
|
|
||||||
mk.dialog_buttons(
|
|
||||||
on_ok=self.commands.confirm_new(),
|
|
||||||
on_cancel=self.commands.cancel(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
cls="mf-fmgr-form p-4",
|
|
||||||
)
|
|
||||||
|
|
||||||
def _mk_rename_form(self):
|
return Div(
|
||||||
preset = self._get_selected_preset()
|
Div(
|
||||||
return Div(
|
Div(preset.name, cls="text-sm font-semibold"),
|
||||||
Form(
|
Div(preset.description, cls="text-xs opacity-50") if preset.description else None,
|
||||||
Fieldset(
|
),
|
||||||
Label("Name"),
|
Div(*badges, cls="flex gap-1") if badges else None,
|
||||||
Input(name="name", value=preset.name if preset else "", cls="input input-sm w-full"),
|
cls="mf-fmgr-editor-meta mb-2",
|
||||||
Label("Description"),
|
)
|
||||||
Input(name="description", value=preset.description if preset else "", cls="input input-sm w-full"),
|
|
||||||
legend="Rename preset",
|
|
||||||
cls="fieldset border-base-300 rounded-box p-4",
|
|
||||||
),
|
|
||||||
mk.dialog_buttons(
|
|
||||||
on_ok=self.commands.confirm_rename(),
|
|
||||||
on_cancel=self.commands.cancel(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
cls="mf-fmgr-form p-4",
|
|
||||||
)
|
|
||||||
|
|
||||||
def _mk_main_content(self):
|
def _mk_editor_view(self):
|
||||||
if self._state.ns_mode == "new":
|
preset = self._get_selected_preset()
|
||||||
return self._mk_new_form()
|
if preset is None:
|
||||||
elif self._state.ns_mode == "rename":
|
return Div("Select a preset to edit", cls="mf-fmgr-placeholder p-4 text-sm opacity-50")
|
||||||
return self._mk_rename_form()
|
is_builtin = self._is_builtin(preset.name)
|
||||||
return self._mk_editor_view()
|
return Div(
|
||||||
|
self._mk_preset_header(preset, is_builtin),
|
||||||
|
self._editor,
|
||||||
|
cls="mf-fmgr-editor-view p-2",
|
||||||
|
)
|
||||||
|
|
||||||
def render(self):
|
def _mk_new_form(self):
|
||||||
self._search.set_items(self._get_all_presets())
|
return Div(
|
||||||
self._panel._main = self._mk_main_content()
|
Form(
|
||||||
self._panel._left = self._search
|
Fieldset(
|
||||||
|
Label("Name"),
|
||||||
|
Input(name="name", cls="input input-sm w-full"),
|
||||||
|
Label("Description"),
|
||||||
|
Input(name="description", cls="input input-sm w-full"),
|
||||||
|
legend="New preset",
|
||||||
|
cls="fieldset border-base-300 rounded-box p-2",
|
||||||
|
),
|
||||||
|
mk.dialog_buttons(
|
||||||
|
on_ok=self.commands.confirm_new(),
|
||||||
|
on_cancel=self.commands.cancel(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
cls="mf-fmgr-form p-2",
|
||||||
|
)
|
||||||
|
|
||||||
return Div(
|
def _mk_rename_form(self):
|
||||||
self._menu,
|
preset = self._get_selected_preset()
|
||||||
self._panel,
|
return Div(
|
||||||
id=self._id,
|
Form(
|
||||||
cls="mf-formatting-manager",
|
Fieldset(
|
||||||
)
|
Label("Name"),
|
||||||
|
Input(name="name", value=preset.name if preset else "", cls="input input-sm w-full"),
|
||||||
|
Label("Description"),
|
||||||
|
Input(name="description", value=preset.description if preset else "", cls="input input-sm w-full"),
|
||||||
|
legend="Rename preset",
|
||||||
|
cls="fieldset border-base-300 rounded-box p-4",
|
||||||
|
),
|
||||||
|
mk.dialog_buttons(
|
||||||
|
on_ok=self.commands.confirm_rename(),
|
||||||
|
on_cancel=self.commands.cancel(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
cls="mf-fmgr-form p-2",
|
||||||
|
)
|
||||||
|
|
||||||
def __ft__(self):
|
def _mk_main_content(self):
|
||||||
return self.render()
|
if self._state.ns_mode == "new":
|
||||||
|
return self._mk_new_form()
|
||||||
|
elif self._state.ns_mode == "rename":
|
||||||
|
return self._mk_rename_form()
|
||||||
|
return self._mk_editor_view()
|
||||||
|
|
||||||
|
def render(self):
|
||||||
|
self._search.set_items(self._get_all_presets())
|
||||||
|
self._panel._main = self._mk_main_content()
|
||||||
|
self._panel._left = self._search
|
||||||
|
|
||||||
|
return Div(
|
||||||
|
self._menu,
|
||||||
|
self._panel,
|
||||||
|
id=self._id,
|
||||||
|
cls="mf-formatting-manager",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __ft__(self):
|
||||||
|
return self.render()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import inspect
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@@ -35,7 +36,8 @@ class Menu(MultipleInstance):
|
|||||||
name
|
name
|
||||||
for name in dir(commands_obj)
|
for name in dir(commands_obj)
|
||||||
if not name.startswith("_")
|
if not name.startswith("_")
|
||||||
and callable(getattr(commands_obj, name))
|
and callable(attr := getattr(commands_obj, name))
|
||||||
|
and len(inspect.signature(attr).parameters) == 0
|
||||||
]
|
]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -59,7 +61,7 @@ class Menu(MultipleInstance):
|
|||||||
Div("|"),
|
Div("|"),
|
||||||
*[self._mk_menu(command_name) for command_name in self._state.last_used[:3]]
|
*[self._mk_menu(command_name) for command_name in self._state.last_used[:3]]
|
||||||
) if self._state.last_used else [],
|
) if self._state.last_used else [],
|
||||||
cls="flex"
|
cls="flex mb-1"
|
||||||
),
|
),
|
||||||
id=self._id
|
id=self._id
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ class Panel(MultipleInstance):
|
|||||||
body = Div(
|
body = Div(
|
||||||
header,
|
header,
|
||||||
Div(content, id=self._ids.content(side), cls="mf-panel-content"),
|
Div(content, id=self._ids.content(side), cls="mf-panel-content"),
|
||||||
cls="mf-panel-body"
|
cls=f"mf-panel-body mf-panel-body-{side}"
|
||||||
)
|
)
|
||||||
if side == "left":
|
if side == "left":
|
||||||
return Div(
|
return Div(
|
||||||
|
|||||||
Reference in New Issue
Block a user