Adding FileUpload control and updated related components
This commit is contained in:
12
src/app.py
12
src/app.py
@@ -4,12 +4,14 @@ import yaml
|
|||||||
from fasthtml import serve
|
from fasthtml import serve
|
||||||
|
|
||||||
from myfasthtml.controls.CommandsDebugger import CommandsDebugger
|
from myfasthtml.controls.CommandsDebugger import CommandsDebugger
|
||||||
|
from myfasthtml.controls.FileUpload import FileUpload
|
||||||
from myfasthtml.controls.InstancesDebugger import InstancesDebugger
|
from myfasthtml.controls.InstancesDebugger import InstancesDebugger
|
||||||
from myfasthtml.controls.Layout import Layout
|
from myfasthtml.controls.Layout import Layout
|
||||||
from myfasthtml.controls.TabsManager import TabsManager
|
from myfasthtml.controls.TabsManager import TabsManager
|
||||||
from myfasthtml.controls.helpers import Ids, mk
|
from myfasthtml.controls.helpers import Ids, mk
|
||||||
from myfasthtml.core.instances import InstancesManager, RootInstance
|
from myfasthtml.core.instances import InstancesManager, RootInstance
|
||||||
from myfasthtml.icons.carbon import volume_object_storage
|
from myfasthtml.icons.carbon import volume_object_storage
|
||||||
|
from myfasthtml.icons.fluent_p3 import folder_open20_regular
|
||||||
from myfasthtml.myfastapp import create_app
|
from myfasthtml.myfastapp import create_app
|
||||||
|
|
||||||
with open('logging.yaml', 'r') as f:
|
with open('logging.yaml', 'r') as f:
|
||||||
@@ -49,10 +51,16 @@ def index(session):
|
|||||||
command=tabs_manager.commands.add_tab("Commands", commands_debugger),
|
command=tabs_manager.commands.add_tab("Commands", commands_debugger),
|
||||||
id=commands_debugger.get_id())
|
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_left.add(tabs_manager.add_tab_btn())
|
||||||
layout.header_right.add(btn_show_right_drawer)
|
layout.header_right.add(btn_show_right_drawer)
|
||||||
layout.left_drawer.add(btn_show_instances_debugger)
|
layout.left_drawer.add(btn_show_instances_debugger, "Debugger")
|
||||||
layout.left_drawer.add(btn_show_commands_debugger)
|
layout.left_drawer.add(btn_show_commands_debugger, "Debugger")
|
||||||
|
layout.left_drawer.add(btn_file_upload, "Test")
|
||||||
layout.set_main(tabs_manager)
|
layout.set_main(tabs_manager)
|
||||||
return layout
|
return layout
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
:root {
|
:root {
|
||||||
--color-border-primary: color-mix(in oklab, var(--color-primary) 40%, #0000);
|
--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 {
|
.mf-icon-16 {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
min-width: 16px;
|
min-width: 16px;
|
||||||
@@ -219,6 +231,7 @@
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Base resizer styles */
|
/* Base resizer styles */
|
||||||
@@ -299,6 +312,16 @@
|
|||||||
opacity: 1;
|
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 ************ */
|
/* *********** Tabs Manager Component ************ */
|
||||||
/* *********************************************** */
|
/* *********************************************** */
|
||||||
|
|||||||
49
src/myfasthtml/controls/FileUpload.py
Normal file
49
src/myfasthtml/controls/FileUpload.py
Normal 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()
|
||||||
@@ -69,20 +69,36 @@ class Layout(SingleInstance):
|
|||||||
class Content:
|
class Content:
|
||||||
def __init__(self, owner):
|
def __init__(self, owner):
|
||||||
self._owner = owner
|
self._owner = owner
|
||||||
self._content = []
|
self._content = {}
|
||||||
|
self._groups = []
|
||||||
self._ids = set()
|
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)
|
content_id = get_id(content)
|
||||||
if content_id in self._ids:
|
if content_id in self._ids:
|
||||||
return
|
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:
|
if content_id is not None:
|
||||||
self._ids.add(content_id)
|
self._ids.add(content_id)
|
||||||
|
|
||||||
def get_content(self):
|
def get_content(self):
|
||||||
return self._content
|
return self._content
|
||||||
|
|
||||||
|
def get_groups(self):
|
||||||
|
return self._groups
|
||||||
|
|
||||||
def __init__(self, session, app_name, parent=None):
|
def __init__(self, session, app_name, parent=None):
|
||||||
"""
|
"""
|
||||||
@@ -224,7 +240,14 @@ class Layout(SingleInstance):
|
|||||||
|
|
||||||
# Wrap content in scrollable container
|
# Wrap content in scrollable container
|
||||||
content_wrapper = Div(
|
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"
|
cls="mf-layout-drawer-content"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class Ids:
|
|||||||
Boundaries = "mf-boundaries"
|
Boundaries = "mf-boundaries"
|
||||||
CommandsDebugger = "mf-commands-debugger"
|
CommandsDebugger = "mf-commands-debugger"
|
||||||
DbManager = "mf-dbmanager"
|
DbManager = "mf-dbmanager"
|
||||||
|
FileUpload = "mf-file-upload"
|
||||||
InstancesDebugger = "mf-instances-debugger"
|
InstancesDebugger = "mf-instances-debugger"
|
||||||
Layout = "mf-layout"
|
Layout = "mf-layout"
|
||||||
Root = "mf-root"
|
Root = "mf-root"
|
||||||
|
|||||||
@@ -129,6 +129,14 @@ class DataConverter:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class LambdaConverter(DataConverter):
|
||||||
|
def __init__(self, func):
|
||||||
|
self.func = func
|
||||||
|
|
||||||
|
def convert(self, data):
|
||||||
|
return self.func(data)
|
||||||
|
|
||||||
|
|
||||||
class BooleanConverter(DataConverter):
|
class BooleanConverter(DataConverter):
|
||||||
def convert(self, data):
|
def convert(self, data):
|
||||||
if data is None:
|
if data is None:
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class DbObject:
|
|||||||
self._initializing = old_state
|
self._initializing = old_state
|
||||||
|
|
||||||
def __setattr__(self, name: str, value: str):
|
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)
|
super().__setattr__(name, value)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -74,7 +74,8 @@ class DbObject:
|
|||||||
self._save_self()
|
self._save_self()
|
||||||
|
|
||||||
def _save_self(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:
|
if props:
|
||||||
self._db_manager.save(self._name, props)
|
self._db_manager.save(self._name, props)
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class BaseInstance:
|
|||||||
def get_parent(self):
|
def get_parent(self):
|
||||||
return self._parent
|
return self._parent
|
||||||
|
|
||||||
|
|
||||||
class SingleInstance(BaseInstance):
|
class SingleInstance(BaseInstance):
|
||||||
"""
|
"""
|
||||||
Base class for instances that can only have one instance at a time.
|
Base class for instances that can only have one instance at a time.
|
||||||
@@ -107,7 +108,11 @@ class InstancesManager:
|
|||||||
if instance_type:
|
if instance_type:
|
||||||
if not issubclass(instance_type, SingleInstance):
|
if not issubclass(instance_type, SingleInstance):
|
||||||
assert parent is not None, "Parent instance must be provided if not 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:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from myfasthtml.controls.CommandsDebugger import CommandsDebugger
|
from myfasthtml.controls.CommandsDebugger import CommandsDebugger
|
||||||
|
from myfasthtml.controls.FileUpload import FileUpload
|
||||||
from myfasthtml.controls.InstancesDebugger import InstancesDebugger
|
from myfasthtml.controls.InstancesDebugger import InstancesDebugger
|
||||||
from myfasthtml.controls.VisNetwork import VisNetwork
|
from myfasthtml.controls.VisNetwork import VisNetwork
|
||||||
from myfasthtml.controls.helpers import Ids
|
from myfasthtml.controls.helpers import Ids
|
||||||
@@ -12,6 +13,7 @@ logger = logging.getLogger("InstancesHelper")
|
|||||||
class InstancesHelper:
|
class InstancesHelper:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def dynamic_get(parent: BaseInstance, component_type: str, instance_id: str):
|
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:
|
if component_type == Ids.VisNetwork:
|
||||||
return InstancesManager.get(parent.get_session(), instance_id,
|
return InstancesManager.get(parent.get_session(), instance_id,
|
||||||
VisNetwork, parent=parent, _id=instance_id)
|
VisNetwork, parent=parent, _id=instance_id)
|
||||||
@@ -21,6 +23,7 @@ class InstancesHelper:
|
|||||||
elif component_type == Ids.CommandsDebugger:
|
elif component_type == Ids.CommandsDebugger:
|
||||||
return InstancesManager.get(parent.get_session(), instance_id,
|
return InstancesManager.get(parent.get_session(), instance_id,
|
||||||
CommandsDebugger, parent.get_session(), parent, 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}")
|
logger.warning(f"Unknown component type: {component_type}")
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -266,6 +266,7 @@ def post(session, b_id: str, values: dict):
|
|||||||
from myfasthtml.core.bindings import BindingsManager
|
from myfasthtml.core.bindings import BindingsManager
|
||||||
binding = BindingsManager.get_binding(b_id)
|
binding = BindingsManager.get_binding(b_id)
|
||||||
if binding:
|
if binding:
|
||||||
return binding.update(values)
|
res = binding.update(values)
|
||||||
|
return res
|
||||||
|
|
||||||
raise ValueError(f"Binding with ID '{b_id}' not found.")
|
raise ValueError(f"Binding with ID '{b_id}' not found.")
|
||||||
|
|||||||
@@ -106,6 +106,52 @@ def test_i_can_init_from_db_with_dataclass(session, db_manager):
|
|||||||
assert dummy.number == 34
|
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):
|
def test_db_is_updated_when_attribute_is_modified(session, db_manager):
|
||||||
@dataclass
|
@dataclass
|
||||||
class DummyObject(DbObject):
|
class DummyObject(DbObject):
|
||||||
|
|||||||
Reference in New Issue
Block a user