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

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