Fixed bugs in DataGridFormattingManager

This commit is contained in:
2026-03-14 22:01:44 +01:00
parent 56fb3cf021
commit a4ebd6d61b
8 changed files with 488 additions and 363 deletions

View File

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

View 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;
}

View File

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

View File

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

View File

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

View File

@@ -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,15 +14,17 @@ 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):
@@ -39,8 +41,8 @@ class Commands(BaseCommands):
"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):
@@ -48,7 +50,7 @@ class Commands(BaseCommands):
"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")
@@ -57,8 +59,8 @@ class Commands(BaseCommands):
"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):
@@ -66,7 +68,7 @@ class Commands(BaseCommands):
"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")
@@ -75,7 +77,7 @@ class Commands(BaseCommands):
"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):
@@ -83,7 +85,7 @@ class Commands(BaseCommands):
"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):
@@ -91,7 +93,16 @@ class Commands(BaseCommands):
"Cancel", "Cancel",
"Cancel", "Cancel",
self._owner, self._owner,
self._owner.on_cancel, self._owner.handle_cancel,
).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") ).htmx(target=f"#{self._id}", swap="outerHTML")
@@ -118,21 +129,30 @@ class DataGridFormattingManager(SingleInstance):
template=self._mk_preset_item, template=self._mk_preset_item,
_id="-search", _id="-search",
) )
provider = DatagridMetadataProvider(self)
completion_engine = FormattingCompletionEngine(provider, "")
DslsManager.register(completion_engine, DSLParser())
self._editor = DslEditor( self._editor = DslEditor(
self, self,
dsl=FormattingDSL(), dsl=FormattingDSL(),
conf=DslEditorConf( conf=DslEditorConf(
save_button=False, save_button=False,
autocompletion=False, autocompletion=True,
linting=False, linting=True,
placeholder=_DSL_PLACEHOLDER, engine_id=completion_engine.get_id(),
), ),
save_state=False, save_state=False,
_id="-editor", _id="-editor",
) )
self.handle_select_preset(self._state.selected_name)
self._sync_provider()
logger.debug(f"DataGridFormattingManager created with id={self._id}") logger.debug(f"DataGridFormattingManager created with id={self._id}")
def get_main_content_id(self):
return self._panel.get_ids().main
# === Helpers === # === Helpers ===
def _get_all_presets(self) -> list: def _get_all_presets(self) -> list:
@@ -164,13 +184,18 @@ class DataGridFormattingManager(SingleInstance):
except Exception: except Exception:
return [] return []
def _sync_provider(self):
"""Sync all presets (builtin + user) into the session-scoped metadata provider."""
provider = DatagridMetadataProvider(self)
provider.rule_presets = {p.name: p for p in self._get_all_presets()}
# === Command handlers === # === Command handlers ===
def on_new_preset(self): def handle_new_preset(self):
self._state.ns_mode = "new" self._state.ns_mode = "new"
return self.render() return self.render()
def on_save_preset(self): def handle_save_preset(self):
if not self._state.selected_name or self._is_builtin(self._state.selected_name): if not self._state.selected_name or self._is_builtin(self._state.selected_name):
return self.render() return self.render()
preset = self._get_user_preset(self._state.selected_name) preset = self._get_user_preset(self._state.selected_name)
@@ -179,41 +204,45 @@ class DataGridFormattingManager(SingleInstance):
dsl = self._editor.get_content() dsl = self._editor.get_content()
preset.dsl = dsl preset.dsl = dsl
preset.rules = self._parse_dsl_to_rules(dsl) preset.rules = self._parse_dsl_to_rules(dsl)
self._state.save()
self._sync_provider()
logger.debug(f"Saved preset '{preset.name}' with {len(preset.rules)} rules") logger.debug(f"Saved preset '{preset.name}' with {len(preset.rules)} rules")
return self.render() return self.render()
def on_rename_preset(self): def handle_rename_preset(self):
if not self._state.selected_name or self._is_builtin(self._state.selected_name): if not self._state.selected_name or self._is_builtin(self._state.selected_name):
return self.render() return self.render()
self._state.ns_mode = "rename" self._state.ns_mode = "rename"
return self.render() return self.render()
def on_delete_preset(self): def handle_delete_preset(self):
if not self._state.selected_name or self._is_builtin(self._state.selected_name): if not self._state.selected_name or self._is_builtin(self._state.selected_name):
return self.render() return self.render()
self._state.presets = [p for p in self._state.presets if p.name != self._state.selected_name] 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._state.selected_name = None
self._editor.set_content("") self._editor.set_content("")
self._editor.conf.readonly = False self._editor.conf.readonly = False
logger.debug(f"Deleted preset '{self._state.selected_name}'") self._sync_provider()
logger.debug(f"Deleted preset '{deleted_name}'")
return self.render() return self.render()
def on_select_preset(self, name: str): def handle_select_preset(self, name: str):
preset = None preset = None
for p in self._get_all_presets(): for p in self._get_all_presets():
if p.name == name: if p.name == name:
preset = p preset = p
break break
if preset is None: if preset is None:
return self.render() return None
self._state.selected_name = name self._state.selected_name = name
self._state.ns_mode = "view" self._state.ns_mode = "view"
self._editor.set_content(preset.dsl) self._editor.set_content(preset.dsl)
self._editor.conf.readonly = self._is_builtin(name) self._editor.conf.readonly = self._is_builtin(name)
logger.debug(f"Selected preset '{name}', readonly={self._editor.conf.readonly}") logger.debug(f"Selected preset '{name}', readonly={self._editor.conf.readonly}")
return self.render() return self.render() # to also update the selected item in the search
def on_confirm_new(self, client_response): def handle_confirm_new(self, client_response):
name = (client_response.get("name") or "").strip() name = (client_response.get("name") or "").strip()
description = (client_response.get("description") or "").strip() description = (client_response.get("description") or "").strip()
@@ -233,10 +262,11 @@ class DataGridFormattingManager(SingleInstance):
self._state.ns_mode = "view" self._state.ns_mode = "view"
self._editor.set_content("") self._editor.set_content("")
self._editor.conf.readonly = False self._editor.conf.readonly = False
self._sync_provider()
logger.debug(f"Created preset '{name}'") logger.debug(f"Created preset '{name}'")
return self.render() return self.render()
def on_confirm_rename(self, client_response): def handle_confirm_rename(self, client_response):
name = (client_response.get("name") or "").strip() name = (client_response.get("name") or "").strip()
description = (client_response.get("description") or "").strip() description = (client_response.get("description") or "").strip()
@@ -257,39 +287,43 @@ class DataGridFormattingManager(SingleInstance):
preset.description = description preset.description = description
self._state.selected_name = name self._state.selected_name = name
self._state.ns_mode = "view" self._state.ns_mode = "view"
self._sync_provider()
logger.debug(f"Renamed preset '{old_name}''{name}'") logger.debug(f"Renamed preset '{old_name}''{name}'")
return self.render() return self.render()
def on_cancel(self): def handle_cancel(self):
self._state.ns_mode = "view" self._state.ns_mode = "view"
return self.render() return self.render()
# === Rendering === # === Rendering ===
def _get_badges(self, preset: RulePreset, is_builtin: bool):
badges = []
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"))
return badges
def _mk_badge(self, text: Literal["format", "style", "built-in"]):
if text == "built-in":
return Span("built-in", cls="badge badge-xs badge-ghost")
if text == "style":
return Span("style", cls="badge badge-xs badge-primary")
return Span("format", cls="badge badge-xs badge-secondary")
def _mk_preset_item(self, preset: RulePreset): def _mk_preset_item(self, preset: RulePreset):
is_active = self._state.selected_name == preset.name is_active = self._state.selected_name == preset.name
is_builtin = self._is_builtin(preset.name) is_builtin = self._is_builtin(preset.name)
badges = self._get_badges(preset, is_builtin)
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" item_cls = "mf-fmgr-preset-item"
if is_active: if is_active:
item_cls += " mf-fmgr-preset-item-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( return mk.mk(
Div( Div(
Div(preset.name, cls="mf-fmgr-preset-name"), Div(preset.name, cls="mf-fmgr-preset-name"),
@@ -297,17 +331,11 @@ class DataGridFormattingManager(SingleInstance):
Div(*badges, cls="mf-fmgr-preset-badges") if badges else None, Div(*badges, cls="mf-fmgr-preset-badges") if badges else None,
cls=item_cls, cls=item_cls,
), ),
command=select_cmd, command=self.commands.select_preset(preset.name),
) )
def _mk_preset_header(self, preset: RulePreset, is_builtin: bool): def _mk_preset_header(self, preset: RulePreset, is_builtin: bool):
badges = [] badges = self._get_badges(preset, is_builtin)
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( return Div(
Div( Div(
@@ -315,7 +343,7 @@ class DataGridFormattingManager(SingleInstance):
Div(preset.description, cls="text-xs opacity-50") if preset.description else None, Div(preset.description, cls="text-xs opacity-50") if preset.description else None,
), ),
Div(*badges, cls="flex gap-1") if badges else None, Div(*badges, cls="flex gap-1") if badges else None,
cls="mf-fmgr-editor-meta", cls="mf-fmgr-editor-meta mb-2",
) )
def _mk_editor_view(self): def _mk_editor_view(self):
@@ -326,7 +354,7 @@ class DataGridFormattingManager(SingleInstance):
return Div( return Div(
self._mk_preset_header(preset, is_builtin), self._mk_preset_header(preset, is_builtin),
self._editor, self._editor,
cls="mf-fmgr-editor-view", cls="mf-fmgr-editor-view p-2",
) )
def _mk_new_form(self): def _mk_new_form(self):
@@ -338,14 +366,14 @@ class DataGridFormattingManager(SingleInstance):
Label("Description"), Label("Description"),
Input(name="description", cls="input input-sm w-full"), Input(name="description", cls="input input-sm w-full"),
legend="New preset", legend="New preset",
cls="fieldset border-base-300 rounded-box p-4", cls="fieldset border-base-300 rounded-box p-2",
), ),
mk.dialog_buttons( mk.dialog_buttons(
on_ok=self.commands.confirm_new(), on_ok=self.commands.confirm_new(),
on_cancel=self.commands.cancel(), on_cancel=self.commands.cancel(),
), ),
), ),
cls="mf-fmgr-form p-4", cls="mf-fmgr-form p-2",
) )
def _mk_rename_form(self): def _mk_rename_form(self):
@@ -365,7 +393,7 @@ class DataGridFormattingManager(SingleInstance):
on_cancel=self.commands.cancel(), on_cancel=self.commands.cancel(),
), ),
), ),
cls="mf-fmgr-form p-4", cls="mf-fmgr-form p-2",
) )
def _mk_main_content(self): def _mk_main_content(self):

View File

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

View File

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