Adding FileUpload control and updated related components

This commit is contained in:
2025-11-18 21:54:49 +01:00
parent 3de9aff15c
commit 4199427c71
11 changed files with 179 additions and 11 deletions

View File

@@ -4,12 +4,14 @@ import yaml
from fasthtml import serve
from myfasthtml.controls.CommandsDebugger import CommandsDebugger
from myfasthtml.controls.FileUpload import FileUpload
from myfasthtml.controls.InstancesDebugger import InstancesDebugger
from myfasthtml.controls.Layout import Layout
from myfasthtml.controls.TabsManager import TabsManager
from myfasthtml.controls.helpers import Ids, mk
from myfasthtml.core.instances import InstancesManager, RootInstance
from myfasthtml.icons.carbon import volume_object_storage
from myfasthtml.icons.fluent_p3 import folder_open20_regular
from myfasthtml.myfastapp import create_app
with open('logging.yaml', 'r') as f:
@@ -49,10 +51,16 @@ def index(session):
command=tabs_manager.commands.add_tab("Commands", commands_debugger),
id=commands_debugger.get_id())
btn_file_upload = mk.label("Upload",
icon=folder_open20_regular,
command=tabs_manager.commands.add_tab("File Open", FileUpload(layout)),
id="file_upload_id")
layout.header_left.add(tabs_manager.add_tab_btn())
layout.header_right.add(btn_show_right_drawer)
layout.left_drawer.add(btn_show_instances_debugger)
layout.left_drawer.add(btn_show_commands_debugger)
layout.left_drawer.add(btn_show_instances_debugger, "Debugger")
layout.left_drawer.add(btn_show_commands_debugger, "Debugger")
layout.left_drawer.add(btn_file_upload, "Test")
layout.set_main(tabs_manager)
return layout

View File

@@ -1,8 +1,20 @@
:root {
--color-border-primary: color-mix(in oklab, var(--color-primary) 40%, #0000);
--font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
--spacing: 0.25rem;
--text-sm: 0.875rem;
--text-sm--line-height: calc(1.25 / 0.875);
--text-xl: 1.25rem;
--text-xl--line-height: calc(1.75 / 1.25);
--font-weight-medium: 500;
--radius-md: 0.375rem;
--default-font-family: var(--font-sans);
--default-mono-font-family: var(--font-mono);
}
.mf-icon-16 {
width: 16px;
min-width: 16px;
@@ -219,6 +231,7 @@
bottom: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 1rem;
}
/* Base resizer styles */
@@ -299,6 +312,16 @@
opacity: 1;
}
.mf-layout-group {
font-weight: bold;
/*font-size: var(--text-sm);*/
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 0.2rem;
}
/* *********************************************** */
/* *********** Tabs Manager Component ************ */
/* *********************************************** */

View File

@@ -0,0 +1,49 @@
from dataclasses import dataclass
from fasthtml.components import *
from myfasthtml.controls.helpers import Ids, mk
from myfasthtml.core.bindings import Binding, LambdaConverter
from myfasthtml.core.dbmanager import DbObject
from myfasthtml.core.instances import MultipleInstance
class FileUploadState(DbObject):
def __init__(self, owner):
super().__init__(owner.get_session(), owner.get_id())
with self.initializing():
# persisted in DB
# must not be persisted in DB (prefix ns_ = no_saving_)
self.ns_file_name: str = ""
class FileUpload(MultipleInstance):
@dataclass
class BindingData:
filename: str = ""
def __init__(self, parent, _id=None):
super().__init__(Ids.FileUpload, parent, _id=_id)
self._binding = self.BindingData()
self._state = FileUploadState(self)
def render(self):
return Div(
mk.mk(Input(type='file',
name='file',
id=f"fn_{self._id}", # fn stands for 'file name'
value=self._state.ns_file_name,
hx_preserve="true",
hx_encoding='multipart/form-data',
cls="file-input file-input-bordered file-input-sm w-full",
),
binding=Binding(self._binding, "filename")
),
mk.mk(Label(), binding=Binding(self._binding, "filename",
LambdaConverter(lambda x: x.filename if hasattr(x, "filename") else x))),
cls="flex"
)
def __ft__(self):
return self.render()

View File

@@ -69,20 +69,36 @@ class Layout(SingleInstance):
class Content:
def __init__(self, owner):
self._owner = owner
self._content = []
self._content = {}
self._groups = []
self._ids = set()
def add(self, content):
def add_group(self, group, group_ft=None):
group_ft = group_ft or Div(group, cls="mf-layout-group")
if not group:
group_ft = None
self._groups.append((group, group_ft))
self._content[group] = []
def add(self, content, group=None):
content_id = get_id(content)
if content_id in self._ids:
return
self._content.append(content)
if group not in self._content:
self.add_group(group)
self._content[group] = []
self._content[group].append(content)
if content_id is not None:
self._ids.add(content_id)
def get_content(self):
return self._content
def get_groups(self):
return self._groups
def __init__(self, session, app_name, parent=None):
"""
@@ -224,7 +240,14 @@ class Layout(SingleInstance):
# Wrap content in scrollable container
content_wrapper = Div(
*self.left_drawer.get_content(),
*[
(
Div(cls="divider") if index > 0 else None,
group_ft,
*[item for item in self.left_drawer.get_content()[group_name]]
)
for index, (group_name, group_ft) in enumerate(self.left_drawer.get_groups())
],
cls="mf-layout-drawer-content"
)

View File

@@ -11,6 +11,7 @@ class Ids:
Boundaries = "mf-boundaries"
CommandsDebugger = "mf-commands-debugger"
DbManager = "mf-dbmanager"
FileUpload = "mf-file-upload"
InstancesDebugger = "mf-instances-debugger"
Layout = "mf-layout"
Root = "mf-root"

View File

@@ -129,6 +129,14 @@ class DataConverter:
pass
class LambdaConverter(DataConverter):
def __init__(self, func):
self.func = func
def convert(self, data):
return self.func(data)
class BooleanConverter(DataConverter):
def convert(self, data):
if data is None:

View File

@@ -55,7 +55,7 @@ class DbObject:
self._initializing = old_state
def __setattr__(self, name: str, value: str):
if name.startswith("_") or getattr(self, "_initializing", False):
if name.startswith("_") or name.startswith("ns") or getattr(self, "_initializing", False):
super().__setattr__(name, value)
return
@@ -74,7 +74,8 @@ class DbObject:
self._save_self()
def _save_self(self):
props = {k: getattr(self, k) for k, v in self._get_properties().items() if not k.startswith("_")}
props = {k: getattr(self, k) for k, v in self._get_properties().items() if
not k.startswith("_") and not k.startswith("ns")}
if props:
self._db_manager.save(self._name, props)

View File

@@ -38,6 +38,7 @@ class BaseInstance:
def get_parent(self):
return self._parent
class SingleInstance(BaseInstance):
"""
Base class for instances that can only have one instance at a time.
@@ -107,7 +108,11 @@ class InstancesManager:
if instance_type:
if not issubclass(instance_type, SingleInstance):
assert parent is not None, "Parent instance must be provided if not SingleInstance"
return instance_type(session, parent=parent, *args, **kwargs) # it will be automatically registered
if isinstance(parent, MultipleInstance):
return instance_type(parent, _id=instance_id, *args, **kwargs)
else:
return instance_type(session, parent=parent, *args, **kwargs) # it will be automatically registered
else:
raise

View File

@@ -1,6 +1,7 @@
import logging
from myfasthtml.controls.CommandsDebugger import CommandsDebugger
from myfasthtml.controls.FileUpload import FileUpload
from myfasthtml.controls.InstancesDebugger import InstancesDebugger
from myfasthtml.controls.VisNetwork import VisNetwork
from myfasthtml.controls.helpers import Ids
@@ -12,6 +13,7 @@ logger = logging.getLogger("InstancesHelper")
class InstancesHelper:
@staticmethod
def dynamic_get(parent: BaseInstance, component_type: str, instance_id: str):
logger.debug(f"Dynamic get: {component_type} {instance_id}")
if component_type == Ids.VisNetwork:
return InstancesManager.get(parent.get_session(), instance_id,
VisNetwork, parent=parent, _id=instance_id)
@@ -21,6 +23,7 @@ class InstancesHelper:
elif component_type == Ids.CommandsDebugger:
return InstancesManager.get(parent.get_session(), instance_id,
CommandsDebugger, parent.get_session(), parent, instance_id)
elif component_type == Ids.FileUpload:
return InstancesManager.get(parent.get_session(), instance_id, FileUpload, parent)
logger.warning(f"Unknown component type: {component_type}")
return None

View File

@@ -266,6 +266,7 @@ def post(session, b_id: str, values: dict):
from myfasthtml.core.bindings import BindingsManager
binding = BindingsManager.get_binding(b_id)
if binding:
return binding.update(values)
res = binding.update(values)
return res
raise ValueError(f"Binding with ID '{b_id}' not found.")

View File

@@ -106,6 +106,52 @@ def test_i_can_init_from_db_with_dataclass(session, db_manager):
assert dummy.number == 34
def test_i_do_not_save_when_prefixed_by_underscore_or_ns(session, db_manager):
class DummyObject(DbObject):
def __init__(self, sess: dict):
super().__init__(sess, "DummyObject", db_manager)
with self.initializing():
self.to_save: str = "value"
self._not_to_save: str = "value"
self.ns_not_to_save: str = "value"
to_save: str = "value"
_not_to_save: str = "value"
ns_not_to_save: str = "value"
dummy = DummyObject(session)
dummy.to_save = "other_value"
dummy.ns_not_to_save = "other_value"
dummy._not_to_save = "other_value"
in_db = db_manager.load("DummyObject")
assert in_db["to_save"] == "other_value"
assert "_not_to_save" not in in_db
assert "ns_not_to_save" not in in_db
def test_i_do_not_save_when_prefixed_by_underscore_or_ns_with_dataclass(session, db_manager):
@dataclass
class DummyObject(DbObject):
def __init__(self, sess: dict):
super().__init__(sess, "DummyObject", db_manager)
to_save: str = "value"
_not_to_save: str = "value"
ns_not_to_save: str = "value"
dummy = DummyObject(session)
dummy.to_save = "other_value"
dummy.ns_not_to_save = "other_value"
dummy._not_to_save = "other_value"
in_db = db_manager.load("DummyObject")
assert in_db["to_save"] == "other_value"
assert "_not_to_save" not in in_db
assert "ns_not_to_save" not in in_db
def test_db_is_updated_when_attribute_is_modified(session, db_manager):
@dataclass
class DummyObject(DbObject):