Working in DataGridFormattingManager
This commit is contained in:
13
src/app.py
13
src/app.py
@@ -6,6 +6,7 @@ from fasthtml import serve
|
||||
from fasthtml.components import Div
|
||||
|
||||
from myfasthtml.controls.CommandsDebugger import CommandsDebugger
|
||||
from myfasthtml.controls.DataGridFormattingManager import DataGridFormattingManager
|
||||
from myfasthtml.controls.DataGridsManager import DataGridsManager
|
||||
from myfasthtml.controls.Dropdown import Dropdown
|
||||
from myfasthtml.controls.FileUpload import FileUpload
|
||||
@@ -18,7 +19,7 @@ from myfasthtml.core.dbengine_utils import DataFrameHandler
|
||||
from myfasthtml.core.instances import UniqueInstance
|
||||
from myfasthtml.icons.carbon import volume_object_storage
|
||||
from myfasthtml.icons.fluent_p2 import key_command16_regular
|
||||
from myfasthtml.icons.fluent_p3 import folder_open20_regular
|
||||
from myfasthtml.icons.fluent_p3 import folder_open20_regular, text_edit_style20_regular
|
||||
from myfasthtml.myfastapp import create_app
|
||||
|
||||
with open('logging.yaml', 'r') as f:
|
||||
@@ -74,6 +75,15 @@ def index(session):
|
||||
layout.header_right.add(btn_show_right_drawer)
|
||||
layout.left_drawer.add(btn_show_instances_debugger, "Debugger")
|
||||
layout.left_drawer.add(btn_show_commands_debugger, "Debugger")
|
||||
|
||||
# Parameters
|
||||
formatting_manager = DataGridFormattingManager(layout)
|
||||
btn_show_formatting_manager = mk.label("Formatting",
|
||||
icon=text_edit_style20_regular,
|
||||
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_file_upload, "Test")
|
||||
layout.left_drawer.add(btn_popup, "Test")
|
||||
|
||||
@@ -83,6 +93,7 @@ def index(session):
|
||||
dgs_manager.mk_main_icons(),
|
||||
cls="mf-layout-group flex gap-3"))
|
||||
layout.left_drawer.add(dgs_manager, "Documents")
|
||||
|
||||
layout.set_main(tabs_manager)
|
||||
|
||||
# keyboard shortcuts
|
||||
|
||||
391
src/myfasthtml/controls/DataGridFormattingManager.py
Normal file
391
src/myfasthtml/controls/DataGridFormattingManager.py
Normal file
@@ -0,0 +1,391 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fasthtml.common import Form, Fieldset, Label, Input, Span
|
||||
from fasthtml.components import Div
|
||||
|
||||
from myfasthtml.controls.BaseCommands import BaseCommands
|
||||
from myfasthtml.controls.DslEditor import DslEditor, DslEditorConf
|
||||
from myfasthtml.controls.IconsHelper import IconsHelper
|
||||
from myfasthtml.controls.Menu import Menu, MenuConf
|
||||
from myfasthtml.controls.Panel import Panel, PanelConf
|
||||
from myfasthtml.controls.Search import Search
|
||||
from myfasthtml.controls.helpers import mk
|
||||
from myfasthtml.core.commands import Command
|
||||
from myfasthtml.core.dbmanager import DbObject
|
||||
from myfasthtml.core.formatting.dataclasses import RulePreset
|
||||
from myfasthtml.core.formatting.dsl import parse_dsl
|
||||
from myfasthtml.core.formatting.dsl.definition import FormattingDSL
|
||||
from myfasthtml.core.formatting.presets import DEFAULT_RULE_PRESETS
|
||||
from myfasthtml.core.instances import SingleInstance
|
||||
|
||||
logger = logging.getLogger("DataGridFormattingManager")
|
||||
|
||||
_DSL_PLACEHOLDER = 'tables:\n style("error") if value < 0\n style("success") if value > 0'
|
||||
|
||||
|
||||
class DataGridFormattingManagerState(DbObject):
|
||||
def __init__(self, owner):
|
||||
with self.initializing():
|
||||
super().__init__(owner)
|
||||
self.presets: list = []
|
||||
self.selected_name: Optional[str] = None
|
||||
self.ns_mode: str = "view"
|
||||
|
||||
|
||||
class Commands(BaseCommands):
|
||||
def new_preset(self):
|
||||
return Command(
|
||||
"NewPreset",
|
||||
"New preset",
|
||||
self._owner,
|
||||
self._owner.on_new_preset,
|
||||
icon=IconsHelper.get("Add20Regular"),
|
||||
).htmx(target=f"#{self._id}", swap="outerHTML")
|
||||
|
||||
def save_preset(self):
|
||||
return Command(
|
||||
"SavePreset",
|
||||
"Save preset",
|
||||
self._owner,
|
||||
self._owner.on_save_preset,
|
||||
icon=IconsHelper.get("Save20Regular"),
|
||||
).htmx(target=f"#{self._id}", swap="outerHTML")
|
||||
|
||||
def rename_preset(self):
|
||||
return Command(
|
||||
"RenamePreset",
|
||||
"Rename preset",
|
||||
self._owner,
|
||||
self._owner.on_rename_preset,
|
||||
icon=IconsHelper.get("Rename20Regular"),
|
||||
).htmx(target=f"#{self._id}", swap="outerHTML")
|
||||
|
||||
def delete_preset(self):
|
||||
return Command(
|
||||
"DeletePreset",
|
||||
"Delete preset",
|
||||
self._owner,
|
||||
self._owner.on_delete_preset,
|
||||
icon=IconsHelper.get("Delete20Regular"),
|
||||
).htmx(target=f"#{self._id}", swap="outerHTML")
|
||||
|
||||
def confirm_new(self):
|
||||
return Command(
|
||||
"ConfirmNew",
|
||||
"Confirm new preset",
|
||||
self._owner,
|
||||
self._owner.on_confirm_new,
|
||||
).htmx(target=f"#{self._id}", swap="outerHTML")
|
||||
|
||||
def confirm_rename(self):
|
||||
return Command(
|
||||
"ConfirmRename",
|
||||
"Confirm rename",
|
||||
self._owner,
|
||||
self._owner.on_confirm_rename,
|
||||
).htmx(target=f"#{self._id}", swap="outerHTML")
|
||||
|
||||
def cancel(self):
|
||||
return Command(
|
||||
"Cancel",
|
||||
"Cancel",
|
||||
self._owner,
|
||||
self._owner.on_cancel,
|
||||
).htmx(target=f"#{self._id}", swap="outerHTML")
|
||||
|
||||
|
||||
class DataGridFormattingManager(SingleInstance):
|
||||
def __init__(self, parent, _id=None):
|
||||
super().__init__(parent, _id=_id)
|
||||
self._state = DataGridFormattingManagerState(self)
|
||||
self.commands = Commands(self)
|
||||
|
||||
self._panel = Panel(
|
||||
self,
|
||||
conf=PanelConf(left=True, right=False, left_title="Presets"),
|
||||
_id="-panel",
|
||||
)
|
||||
self._menu = Menu(
|
||||
self,
|
||||
conf=MenuConf(fixed_items=["NewPreset", "SavePreset", "RenamePreset", "DeletePreset"]),
|
||||
save_state=False,
|
||||
_id="-menu",
|
||||
)
|
||||
self._search = Search(
|
||||
self,
|
||||
items_names="Presets",
|
||||
template=self._mk_preset_item,
|
||||
_id="-search",
|
||||
)
|
||||
self._editor = DslEditor(
|
||||
self,
|
||||
dsl=FormattingDSL(),
|
||||
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}")
|
||||
|
||||
# === Helpers ===
|
||||
|
||||
def _get_all_presets(self) -> list:
|
||||
"""Returns builtin presets followed by user presets."""
|
||||
return list(DEFAULT_RULE_PRESETS.values()) + self._state.presets
|
||||
|
||||
def _is_builtin(self, name: str) -> bool:
|
||||
return name in DEFAULT_RULE_PRESETS
|
||||
|
||||
def _get_selected_preset(self) -> Optional[RulePreset]:
|
||||
if not self._state.selected_name:
|
||||
return None
|
||||
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]:
|
||||
for p in self._state.presets:
|
||||
if p.name == name:
|
||||
return p
|
||||
return None
|
||||
|
||||
def _parse_dsl_to_rules(self, dsl_text: str) -> list:
|
||||
"""Parse DSL text and extract FormatRule objects, ignoring scopes."""
|
||||
try:
|
||||
scoped_rules = parse_dsl(dsl_text)
|
||||
return [sr.rule for sr in scoped_rules]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
# === Command handlers ===
|
||||
|
||||
def on_new_preset(self):
|
||||
self._state.ns_mode = "new"
|
||||
return self.render()
|
||||
|
||||
def on_save_preset(self):
|
||||
if not self._state.selected_name or self._is_builtin(self._state.selected_name):
|
||||
return self.render()
|
||||
preset = self._get_user_preset(self._state.selected_name)
|
||||
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):
|
||||
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):
|
||||
if not self._state.selected_name or self._is_builtin(self._state.selected_name):
|
||||
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):
|
||||
preset = None
|
||||
for p in self._get_all_presets():
|
||||
if p.name == name:
|
||||
preset = p
|
||||
break
|
||||
if preset is None:
|
||||
return self.render()
|
||||
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()
|
||||
|
||||
def on_confirm_new(self, client_response):
|
||||
name = (client_response.get("name") or "").strip()
|
||||
description = (client_response.get("description") or "").strip()
|
||||
|
||||
if not name:
|
||||
self._state.ns_mode = "view"
|
||||
return self.render()
|
||||
|
||||
all_names = {p.name for p in self._get_all_presets()}
|
||||
if name in all_names:
|
||||
logger.debug(f"Cannot create preset '{name}': name already exists")
|
||||
self._state.ns_mode = "view"
|
||||
return self.render()
|
||||
|
||||
new_preset = RulePreset(name=name, description=description, rules=[], dsl="")
|
||||
self._state.presets.append(new_preset)
|
||||
self._state.selected_name = name
|
||||
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):
|
||||
name = (client_response.get("name") or "").strip()
|
||||
description = (client_response.get("description") or "").strip()
|
||||
|
||||
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"
|
||||
return self.render()
|
||||
|
||||
old_name = preset.name
|
||||
preset.name = name
|
||||
preset.description = description
|
||||
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):
|
||||
self._state.ns_mode = "view"
|
||||
return self.render()
|
||||
|
||||
# === Rendering ===
|
||||
|
||||
def _mk_preset_item(self, preset: RulePreset):
|
||||
is_active = self._state.selected_name == preset.name
|
||||
is_builtin = self._is_builtin(preset.name)
|
||||
|
||||
badges = []
|
||||
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"
|
||||
if is_active:
|
||||
item_cls += " mf-fmgr-preset-item-active"
|
||||
|
||||
select_cmd = Command(
|
||||
"SelectPreset",
|
||||
f"Select {preset.name}",
|
||||
self,
|
||||
self.on_select_preset,
|
||||
args=[preset.name],
|
||||
).htmx(target=f"#{self._id}", swap="outerHTML")
|
||||
|
||||
return mk.mk(
|
||||
Div(
|
||||
Div(preset.name, cls="mf-fmgr-preset-name"),
|
||||
Div(preset.description, cls="mf-fmgr-preset-desc") if preset.description else None,
|
||||
Div(*badges, cls="mf-fmgr-preset-badges") if badges else None,
|
||||
cls=item_cls,
|
||||
),
|
||||
command=select_cmd,
|
||||
)
|
||||
|
||||
def _mk_preset_header(self, preset: RulePreset, is_builtin: bool):
|
||||
badges = []
|
||||
if preset.has_formatter():
|
||||
badges.append(Span("format()", cls="badge badge-secondary"))
|
||||
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(
|
||||
Div(
|
||||
Div(preset.name, cls="text-sm font-semibold"),
|
||||
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):
|
||||
preset = self._get_selected_preset()
|
||||
if preset is None:
|
||||
return Div("Select a preset to edit", cls="mf-fmgr-placeholder p-4 text-sm opacity-50")
|
||||
is_builtin = self._is_builtin(preset.name)
|
||||
return Div(
|
||||
self._mk_preset_header(preset, is_builtin),
|
||||
self._editor,
|
||||
cls="mf-fmgr-editor-view",
|
||||
)
|
||||
|
||||
def _mk_new_form(self):
|
||||
return Div(
|
||||
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):
|
||||
preset = self._get_selected_preset()
|
||||
return Div(
|
||||
Form(
|
||||
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-4",
|
||||
)
|
||||
|
||||
def _mk_main_content(self):
|
||||
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,6 +1,7 @@
|
||||
from fastcore.basics import NotStr
|
||||
|
||||
from myfasthtml.core.constants import ColumnType
|
||||
from myfasthtml.core.utils import pascal_to_snake
|
||||
from myfasthtml.icons.fluent import question20_regular, brain_circuit20_regular, number_symbol20_regular, \
|
||||
number_row20_regular
|
||||
from myfasthtml.icons.fluent_p1 import checkbox_checked20_regular, checkbox_unchecked20_regular, \
|
||||
@@ -63,6 +64,9 @@ class IconsHelper:
|
||||
|
||||
_UTILITY_MODULES = {'manage_icons', 'update_icons'}
|
||||
|
||||
if name[0].isupper():
|
||||
name = pascal_to_snake(name)
|
||||
|
||||
if package:
|
||||
module = importlib.import_module(f"myfasthtml.icons.{package}")
|
||||
icon = getattr(module, name, None)
|
||||
|
||||
@@ -190,6 +190,7 @@ class RulePreset:
|
||||
name: str
|
||||
description: str
|
||||
rules: list[FormatRule] = field(default_factory=list)
|
||||
dsl: str = ""
|
||||
|
||||
def has_formatter(self) -> bool:
|
||||
"""Returns True if at least one rule defines a formatter."""
|
||||
|
||||
@@ -45,6 +45,9 @@ DEFAULT_RULE_PRESETS: dict[str, RulePreset] = {
|
||||
formatter=NumberFormatter(precision=0, thousands_sep=" "),
|
||||
),
|
||||
],
|
||||
dsl='tables:\n'
|
||||
' format.number(precision=0, prefix="(", suffix=")", absolute=True, thousands_sep=" ") if value < 0\n'
|
||||
' format.number(precision=0, thousands_sep=" ") if value > 0\n',
|
||||
),
|
||||
"traffic_light": RulePreset(
|
||||
name="traffic_light",
|
||||
@@ -54,6 +57,10 @@ DEFAULT_RULE_PRESETS: dict[str, RulePreset] = {
|
||||
FormatRule(condition=Condition(operator="==", value=0), style=Style(preset="warning")),
|
||||
FormatRule(condition=Condition(operator=">", value=0), style=Style(preset="success")),
|
||||
],
|
||||
dsl='tables:\n'
|
||||
' style("error") if value < 0\n'
|
||||
' style("warning") if value == 0\n'
|
||||
' style("success") if value > 0\n',
|
||||
),
|
||||
"budget_variance": RulePreset(
|
||||
name="budget_variance",
|
||||
@@ -73,6 +80,10 @@ DEFAULT_RULE_PRESETS: dict[str, RulePreset] = {
|
||||
formatter=NumberFormatter(precision=1, suffix="%", multiplier=100),
|
||||
),
|
||||
],
|
||||
dsl='tables:\n'
|
||||
' format.number(precision=1, suffix="%", multiplier=100) style("error") if value < 0\n'
|
||||
' format.number(precision=1, suffix="%", multiplier=100) style("warning") if value > 0.1\n'
|
||||
' format.number(precision=1, suffix="%", multiplier=100)\n',
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user