Fixed bugs in DataGridFormattingManager
This commit is contained in:
@@ -80,8 +80,7 @@ def index(session):
|
||||
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())
|
||||
command=add_tab("Formatting", formatting_manager))
|
||||
layout.left_drawer.add(btn_show_formatting_manager, "Parameters")
|
||||
|
||||
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-resize: color-mix(in oklab, var(--color-base-content) 50%, #0000);
|
||||
--color-selection: color-mix(in oklab, var(--color-primary) 20%, #0000);
|
||||
--color-button-hover: var(--color-base-300);
|
||||
|
||||
--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';
|
||||
@@ -58,7 +59,7 @@
|
||||
}
|
||||
|
||||
.mf-button:hover {
|
||||
background-color: var(--color-base-300);
|
||||
background-color: var(--color-button-hover);
|
||||
}
|
||||
|
||||
.mf-tooltip-container {
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
|
||||
.mf-panel-content {
|
||||
overflow-y: auto;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* Remove padding-top when using title layout */
|
||||
@@ -115,3 +116,12 @@
|
||||
.mf-panel-right.mf-panel-with-title {
|
||||
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 {
|
||||
background-color: var(--color-base-200);
|
||||
background-color: var(--color-button-hover);
|
||||
}
|
||||
|
||||
.mf-treenode.selected {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
from typing import Optional, Literal
|
||||
|
||||
from fasthtml.common import Form, Fieldset, Label, Input, Span
|
||||
from fasthtml.components import Div
|
||||
@@ -14,15 +14,17 @@ 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.dsls import DslsManager
|
||||
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.parser import DSLParser
|
||||
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):
|
||||
@@ -39,8 +41,8 @@ class Commands(BaseCommands):
|
||||
"NewPreset",
|
||||
"New preset",
|
||||
self._owner,
|
||||
self._owner.on_new_preset,
|
||||
icon=IconsHelper.get("Add20Regular"),
|
||||
self._owner.handle_new_preset,
|
||||
icon=IconsHelper.get("add_circle20_regular"),
|
||||
).htmx(target=f"#{self._id}", swap="outerHTML")
|
||||
|
||||
def save_preset(self):
|
||||
@@ -48,7 +50,7 @@ class Commands(BaseCommands):
|
||||
"SavePreset",
|
||||
"Save preset",
|
||||
self._owner,
|
||||
self._owner.on_save_preset,
|
||||
self._owner.handle_save_preset,
|
||||
icon=IconsHelper.get("Save20Regular"),
|
||||
).htmx(target=f"#{self._id}", swap="outerHTML")
|
||||
|
||||
@@ -57,8 +59,8 @@ class Commands(BaseCommands):
|
||||
"RenamePreset",
|
||||
"Rename preset",
|
||||
self._owner,
|
||||
self._owner.on_rename_preset,
|
||||
icon=IconsHelper.get("Rename20Regular"),
|
||||
self._owner.handle_rename_preset,
|
||||
icon=IconsHelper.get("edit20_regular"),
|
||||
).htmx(target=f"#{self._id}", swap="outerHTML")
|
||||
|
||||
def delete_preset(self):
|
||||
@@ -66,7 +68,7 @@ class Commands(BaseCommands):
|
||||
"DeletePreset",
|
||||
"Delete preset",
|
||||
self._owner,
|
||||
self._owner.on_delete_preset,
|
||||
self._owner.handle_delete_preset,
|
||||
icon=IconsHelper.get("Delete20Regular"),
|
||||
).htmx(target=f"#{self._id}", swap="outerHTML")
|
||||
|
||||
@@ -75,7 +77,7 @@ class Commands(BaseCommands):
|
||||
"ConfirmNew",
|
||||
"Confirm new preset",
|
||||
self._owner,
|
||||
self._owner.on_confirm_new,
|
||||
self._owner.handle_confirm_new,
|
||||
).htmx(target=f"#{self._id}", swap="outerHTML")
|
||||
|
||||
def confirm_rename(self):
|
||||
@@ -83,7 +85,7 @@ class Commands(BaseCommands):
|
||||
"ConfirmRename",
|
||||
"Confirm rename",
|
||||
self._owner,
|
||||
self._owner.on_confirm_rename,
|
||||
self._owner.handle_confirm_rename,
|
||||
).htmx(target=f"#{self._id}", swap="outerHTML")
|
||||
|
||||
def cancel(self):
|
||||
@@ -91,7 +93,16 @@ class Commands(BaseCommands):
|
||||
"Cancel",
|
||||
"Cancel",
|
||||
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")
|
||||
|
||||
|
||||
@@ -118,21 +129,30 @@ class DataGridFormattingManager(SingleInstance):
|
||||
template=self._mk_preset_item,
|
||||
_id="-search",
|
||||
)
|
||||
provider = DatagridMetadataProvider(self)
|
||||
completion_engine = FormattingCompletionEngine(provider, "")
|
||||
DslsManager.register(completion_engine, DSLParser())
|
||||
|
||||
self._editor = DslEditor(
|
||||
self,
|
||||
dsl=FormattingDSL(),
|
||||
conf=DslEditorConf(
|
||||
save_button=False,
|
||||
autocompletion=False,
|
||||
linting=False,
|
||||
placeholder=_DSL_PLACEHOLDER,
|
||||
autocompletion=True,
|
||||
linting=True,
|
||||
engine_id=completion_engine.get_id(),
|
||||
),
|
||||
save_state=False,
|
||||
_id="-editor",
|
||||
)
|
||||
|
||||
self.handle_select_preset(self._state.selected_name)
|
||||
self._sync_provider()
|
||||
logger.debug(f"DataGridFormattingManager created with id={self._id}")
|
||||
|
||||
def get_main_content_id(self):
|
||||
return self._panel.get_ids().main
|
||||
|
||||
# === Helpers ===
|
||||
|
||||
def _get_all_presets(self) -> list:
|
||||
@@ -164,13 +184,18 @@ class DataGridFormattingManager(SingleInstance):
|
||||
except Exception:
|
||||
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 ===
|
||||
|
||||
def on_new_preset(self):
|
||||
def handle_new_preset(self):
|
||||
self._state.ns_mode = "new"
|
||||
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):
|
||||
return self.render()
|
||||
preset = self._get_user_preset(self._state.selected_name)
|
||||
@@ -179,41 +204,45 @@ class DataGridFormattingManager(SingleInstance):
|
||||
dsl = self._editor.get_content()
|
||||
preset.dsl = 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")
|
||||
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):
|
||||
return self.render()
|
||||
self._state.ns_mode = "rename"
|
||||
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):
|
||||
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._editor.set_content("")
|
||||
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()
|
||||
|
||||
def on_select_preset(self, name: str):
|
||||
def handle_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()
|
||||
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()
|
||||
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()
|
||||
description = (client_response.get("description") or "").strip()
|
||||
|
||||
@@ -233,10 +262,11 @@ class DataGridFormattingManager(SingleInstance):
|
||||
self._state.ns_mode = "view"
|
||||
self._editor.set_content("")
|
||||
self._editor.conf.readonly = False
|
||||
self._sync_provider()
|
||||
logger.debug(f"Created preset '{name}'")
|
||||
return self.render()
|
||||
|
||||
def on_confirm_rename(self, client_response):
|
||||
def handle_confirm_rename(self, client_response):
|
||||
name = (client_response.get("name") or "").strip()
|
||||
description = (client_response.get("description") or "").strip()
|
||||
|
||||
@@ -257,39 +287,43 @@ class DataGridFormattingManager(SingleInstance):
|
||||
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 on_cancel(self):
|
||||
def handle_cancel(self):
|
||||
self._state.ns_mode = "view"
|
||||
return self.render()
|
||||
|
||||
# === 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):
|
||||
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"))
|
||||
badges = self._get_badges(preset, is_builtin)
|
||||
|
||||
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"),
|
||||
@@ -297,17 +331,11 @@ class DataGridFormattingManager(SingleInstance):
|
||||
Div(*badges, cls="mf-fmgr-preset-badges") if badges else None,
|
||||
cls=item_cls,
|
||||
),
|
||||
command=select_cmd,
|
||||
command=self.commands.select_preset(preset.name),
|
||||
)
|
||||
|
||||
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"))
|
||||
badges = self._get_badges(preset, is_builtin)
|
||||
|
||||
return Div(
|
||||
Div(
|
||||
@@ -315,7 +343,7 @@ class DataGridFormattingManager(SingleInstance):
|
||||
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",
|
||||
cls="mf-fmgr-editor-meta mb-2",
|
||||
)
|
||||
|
||||
def _mk_editor_view(self):
|
||||
@@ -326,7 +354,7 @@ class DataGridFormattingManager(SingleInstance):
|
||||
return Div(
|
||||
self._mk_preset_header(preset, is_builtin),
|
||||
self._editor,
|
||||
cls="mf-fmgr-editor-view",
|
||||
cls="mf-fmgr-editor-view p-2",
|
||||
)
|
||||
|
||||
def _mk_new_form(self):
|
||||
@@ -338,14 +366,14 @@ class DataGridFormattingManager(SingleInstance):
|
||||
Label("Description"),
|
||||
Input(name="description", cls="input input-sm w-full"),
|
||||
legend="New preset",
|
||||
cls="fieldset border-base-300 rounded-box p-4",
|
||||
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-4",
|
||||
cls="mf-fmgr-form p-2",
|
||||
)
|
||||
|
||||
def _mk_rename_form(self):
|
||||
@@ -365,7 +393,7 @@ class DataGridFormattingManager(SingleInstance):
|
||||
on_cancel=self.commands.cancel(),
|
||||
),
|
||||
),
|
||||
cls="mf-fmgr-form p-4",
|
||||
cls="mf-fmgr-form p-2",
|
||||
)
|
||||
|
||||
def _mk_main_content(self):
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import inspect
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
@@ -35,7 +36,8 @@ class Menu(MultipleInstance):
|
||||
name
|
||||
for name in dir(commands_obj)
|
||||
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 {
|
||||
@@ -59,7 +61,7 @@ class Menu(MultipleInstance):
|
||||
Div("|"),
|
||||
*[self._mk_menu(command_name) for command_name in self._state.last_used[:3]]
|
||||
) if self._state.last_used else [],
|
||||
cls="flex"
|
||||
cls="flex mb-1"
|
||||
),
|
||||
id=self._id
|
||||
)
|
||||
|
||||
@@ -198,7 +198,7 @@ class Panel(MultipleInstance):
|
||||
body = Div(
|
||||
header,
|
||||
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":
|
||||
return Div(
|
||||
|
||||
Reference in New Issue
Block a user