Loading src/nova/trame/view/components/file_upload.py +27 −13 Original line number Diff line number Diff line """View implementation for FileUpload.""" from typing import Any, List, Optional from typing import Any, List, Tuple, Union from trame.app import get_server from trame.widgets import vuetify3 as vuetify from trame_server.core import State from nova.trame._internal.utils import get_state_param from .remote_file_input import RemoteFileInput Loading @@ -12,25 +16,28 @@ class FileUpload(vuetify.VBtn): def __init__( self, v_model: str, base_paths: Optional[List[str]] = None, v_model: Union[str, Tuple], base_paths: Union[List[str], Tuple, None] = None, extensions: Union[List[str], Tuple, None] = None, label: str = "", return_contents: bool = True, return_contents: Union[bool, Tuple] = True, **kwargs: Any, ) -> None: """Constructor for FileUpload. Parameters ---------- v_model : str v_model : Union[str, Tuple] The state variable to set when the user uploads their file. The state variable will contain a latin1-decoded version of the file contents. If your file is binary or requires a different string encoding, then you can call `encode('latin1')` on the file contents to get the underlying bytes. base_paths: list[str], optional base_paths: Union[List[str], Tuple], optional Passed to :ref:`RemoteFileInput <api_remotefileinput>`. extensions: Union[List[str], Tuple], optional Restricts the files shown to the user to files that end with one of the strings in the list. label : str, optional The text to display on the upload button. return_contents : bool, optional return_contents : Union[bool, Tuple], optional If true, the file contents will be stored in v_model. If false, a file path will be stored in v_model. Defaults to true. **kwargs Loading @@ -41,20 +48,26 @@ class FileUpload(vuetify.VBtn): ------- None """ self._server = get_server(None, client_type="vue3") self._v_model = v_model if base_paths: self._base_paths = base_paths else: self._base_paths = ["/"] self._base_paths = base_paths if base_paths else ["/"] self._extensions = extensions if extensions else [] self._return_contents = return_contents self._ref_name = f"nova__fileupload_{self._next_id}" super().__init__(label, **kwargs) self.create_ui() @property def state(self) -> State: return self._server.state def create_ui(self) -> None: self.local_file_input = vuetify.VFileInput( v_model=(self._v_model, None), v_model=self._v_model, __properties=["accept"], accept=self._extensions, classes="d-none", ref=self._ref_name, # Serialize the content in a way that will work with nova-mvvm and then push it to the server. Loading @@ -67,6 +80,7 @@ class FileUpload(vuetify.VBtn): self.remote_file_input = RemoteFileInput( v_model=self._v_model, base_paths=self._base_paths, extensions=self._extensions, input_props={"classes": "d-none"}, return_contents=self._return_contents, ) Loading @@ -79,7 +93,7 @@ class FileUpload(vuetify.VBtn): @self.server.controller.trigger(f"decode_blob_{self._id}") def _decode_blob(contents: bytes) -> None: self.remote_file_input.decode_file(contents, self._return_contents) self.remote_file_input.decode_file(contents, get_state_param(self.state, self._return_contents)) def select_file(self, value: str) -> None: """Programmatically set the RemoteFileInput path. Loading src/nova/trame/view/components/remote_file_input.py +20 −13 Original line number Diff line number Diff line Loading @@ -203,11 +203,11 @@ class RemoteFileInput: # don't want bindings in the internal implementation as our callbacks could compete with the developer's. def _setup_bindings(self) -> None: # If the bindings were given initial values, write these to the state. set_state_param(self.state, self.allow_files) set_state_param(self.state, self.allow_folders) set_state_param(self.state, self.base_paths) set_state_param(self.state, self.extensions) return_contents = set_state_param(self.state, self.return_contents) self._last_allow_files = set_state_param(self.state, self.allow_files) self._last_allow_folders = set_state_param(self.state, self.allow_folders) self._last_base_paths = set_state_param(self.state, self.base_paths) self._last_extensions = set_state_param(self.state, self.extensions) self._last_return_contents = set_state_param(self.state, self.return_contents) # Now we need to propagate the state to this component's view model. self.vm.set_binding_parameters( Loading @@ -216,7 +216,7 @@ class RemoteFileInput: base_paths=self.base_paths, extensions=self.extensions, ) self._setup_update_binding(return_contents) self._setup_update_binding(self._last_return_contents) # Now we set up the change listeners for all bound parameters. These are responsible for updating the component # when other portions of the application manipulate these parameters. Loading @@ -227,7 +227,10 @@ class RemoteFileInput: if isinstance(self.allow_files, bool): return allow_files = rgetdictvalue(kwargs, self.allow_files[0]) self.vm.set_binding_parameters(allow_files=set_state_param(self.state, self.allow_files, allow_files)) if allow_files != self._last_allow_files: self.vm.set_binding_parameters( allow_files=set_state_param(self.state, self.allow_files, allow_files) ) if isinstance(self.allow_folders, tuple): Loading @@ -236,6 +239,7 @@ class RemoteFileInput: if isinstance(self.allow_folders, bool): return allow_folders = rgetdictvalue(kwargs, self.allow_folders[0]) if allow_folders != self._last_allow_folders: self.vm.set_binding_parameters( allow_folders=set_state_param(self.state, self.allow_folders, allow_folders) ) Loading @@ -247,6 +251,7 @@ class RemoteFileInput: if isinstance(self.base_paths, bool): return base_paths = rgetdictvalue(kwargs, self.base_paths[0]) if base_paths != self._last_base_paths: self.vm.set_binding_parameters(base_paths=set_state_param(self.state, self.base_paths, base_paths)) if isinstance(self.extensions, tuple): Loading @@ -256,6 +261,7 @@ class RemoteFileInput: if isinstance(self.extensions, bool): return extensions = rgetdictvalue(kwargs, self.extensions[0]) if extensions != self._last_extensions: self.vm.set_binding_parameters(extensions=set_state_param(self.state, self.extensions, extensions)) if isinstance(self.return_contents, tuple): Loading @@ -265,6 +271,7 @@ class RemoteFileInput: if isinstance(self.return_contents, bool): return return_contents = rgetdictvalue(kwargs, self.return_contents[0]) if return_contents != self._last_return_contents: self._setup_update_binding(return_contents) def _setup_update_binding(self, read_file: bool) -> None: Loading tests/gallery/models/file_upload.py +4 −0 Original line number Diff line number Diff line """Model for MVVM demo of FileUpload.""" from typing import List from pydantic import BaseModel, Field class FileUploadState(BaseModel): """Model for MVVM demo of FileUpload.""" extensions: List[str] = Field(default=[".cif", ".nxs"]) file: str = Field(default="") label: str = Field(default="Upload File") tests/gallery/view_models/file_upload.py +3 −0 Original line number Diff line number Diff line Loading @@ -16,3 +16,6 @@ class FileUploadVM: def on_update(self, data: Dict[str, Any]) -> None: print("file size:", sys.getsizeof(self.model.file)) def update_view(self) -> None: self.model_bind.update_in_view(self.model) tests/gallery/views/app.py +6 −1 Original line number Diff line number Diff line Loading @@ -383,7 +383,12 @@ class App(ThemedApp): vuetify.VCardTitle("Form Inputs & Controls") with GridLayout(columns=3, valign="center"): FileUpload(v_model="file_upload.file", base_paths=["/HFIR", "/SNS"], label="Upload File") FileUpload( v_model="file_upload.file", base_paths=["/HFIR", "/SNS"], extensions=("file_upload.extensions",), label="{{ file_upload.label }}", ) with html.Div(): InputField( v_model="autoscroll", Loading Loading
src/nova/trame/view/components/file_upload.py +27 −13 Original line number Diff line number Diff line """View implementation for FileUpload.""" from typing import Any, List, Optional from typing import Any, List, Tuple, Union from trame.app import get_server from trame.widgets import vuetify3 as vuetify from trame_server.core import State from nova.trame._internal.utils import get_state_param from .remote_file_input import RemoteFileInput Loading @@ -12,25 +16,28 @@ class FileUpload(vuetify.VBtn): def __init__( self, v_model: str, base_paths: Optional[List[str]] = None, v_model: Union[str, Tuple], base_paths: Union[List[str], Tuple, None] = None, extensions: Union[List[str], Tuple, None] = None, label: str = "", return_contents: bool = True, return_contents: Union[bool, Tuple] = True, **kwargs: Any, ) -> None: """Constructor for FileUpload. Parameters ---------- v_model : str v_model : Union[str, Tuple] The state variable to set when the user uploads their file. The state variable will contain a latin1-decoded version of the file contents. If your file is binary or requires a different string encoding, then you can call `encode('latin1')` on the file contents to get the underlying bytes. base_paths: list[str], optional base_paths: Union[List[str], Tuple], optional Passed to :ref:`RemoteFileInput <api_remotefileinput>`. extensions: Union[List[str], Tuple], optional Restricts the files shown to the user to files that end with one of the strings in the list. label : str, optional The text to display on the upload button. return_contents : bool, optional return_contents : Union[bool, Tuple], optional If true, the file contents will be stored in v_model. If false, a file path will be stored in v_model. Defaults to true. **kwargs Loading @@ -41,20 +48,26 @@ class FileUpload(vuetify.VBtn): ------- None """ self._server = get_server(None, client_type="vue3") self._v_model = v_model if base_paths: self._base_paths = base_paths else: self._base_paths = ["/"] self._base_paths = base_paths if base_paths else ["/"] self._extensions = extensions if extensions else [] self._return_contents = return_contents self._ref_name = f"nova__fileupload_{self._next_id}" super().__init__(label, **kwargs) self.create_ui() @property def state(self) -> State: return self._server.state def create_ui(self) -> None: self.local_file_input = vuetify.VFileInput( v_model=(self._v_model, None), v_model=self._v_model, __properties=["accept"], accept=self._extensions, classes="d-none", ref=self._ref_name, # Serialize the content in a way that will work with nova-mvvm and then push it to the server. Loading @@ -67,6 +80,7 @@ class FileUpload(vuetify.VBtn): self.remote_file_input = RemoteFileInput( v_model=self._v_model, base_paths=self._base_paths, extensions=self._extensions, input_props={"classes": "d-none"}, return_contents=self._return_contents, ) Loading @@ -79,7 +93,7 @@ class FileUpload(vuetify.VBtn): @self.server.controller.trigger(f"decode_blob_{self._id}") def _decode_blob(contents: bytes) -> None: self.remote_file_input.decode_file(contents, self._return_contents) self.remote_file_input.decode_file(contents, get_state_param(self.state, self._return_contents)) def select_file(self, value: str) -> None: """Programmatically set the RemoteFileInput path. Loading
src/nova/trame/view/components/remote_file_input.py +20 −13 Original line number Diff line number Diff line Loading @@ -203,11 +203,11 @@ class RemoteFileInput: # don't want bindings in the internal implementation as our callbacks could compete with the developer's. def _setup_bindings(self) -> None: # If the bindings were given initial values, write these to the state. set_state_param(self.state, self.allow_files) set_state_param(self.state, self.allow_folders) set_state_param(self.state, self.base_paths) set_state_param(self.state, self.extensions) return_contents = set_state_param(self.state, self.return_contents) self._last_allow_files = set_state_param(self.state, self.allow_files) self._last_allow_folders = set_state_param(self.state, self.allow_folders) self._last_base_paths = set_state_param(self.state, self.base_paths) self._last_extensions = set_state_param(self.state, self.extensions) self._last_return_contents = set_state_param(self.state, self.return_contents) # Now we need to propagate the state to this component's view model. self.vm.set_binding_parameters( Loading @@ -216,7 +216,7 @@ class RemoteFileInput: base_paths=self.base_paths, extensions=self.extensions, ) self._setup_update_binding(return_contents) self._setup_update_binding(self._last_return_contents) # Now we set up the change listeners for all bound parameters. These are responsible for updating the component # when other portions of the application manipulate these parameters. Loading @@ -227,7 +227,10 @@ class RemoteFileInput: if isinstance(self.allow_files, bool): return allow_files = rgetdictvalue(kwargs, self.allow_files[0]) self.vm.set_binding_parameters(allow_files=set_state_param(self.state, self.allow_files, allow_files)) if allow_files != self._last_allow_files: self.vm.set_binding_parameters( allow_files=set_state_param(self.state, self.allow_files, allow_files) ) if isinstance(self.allow_folders, tuple): Loading @@ -236,6 +239,7 @@ class RemoteFileInput: if isinstance(self.allow_folders, bool): return allow_folders = rgetdictvalue(kwargs, self.allow_folders[0]) if allow_folders != self._last_allow_folders: self.vm.set_binding_parameters( allow_folders=set_state_param(self.state, self.allow_folders, allow_folders) ) Loading @@ -247,6 +251,7 @@ class RemoteFileInput: if isinstance(self.base_paths, bool): return base_paths = rgetdictvalue(kwargs, self.base_paths[0]) if base_paths != self._last_base_paths: self.vm.set_binding_parameters(base_paths=set_state_param(self.state, self.base_paths, base_paths)) if isinstance(self.extensions, tuple): Loading @@ -256,6 +261,7 @@ class RemoteFileInput: if isinstance(self.extensions, bool): return extensions = rgetdictvalue(kwargs, self.extensions[0]) if extensions != self._last_extensions: self.vm.set_binding_parameters(extensions=set_state_param(self.state, self.extensions, extensions)) if isinstance(self.return_contents, tuple): Loading @@ -265,6 +271,7 @@ class RemoteFileInput: if isinstance(self.return_contents, bool): return return_contents = rgetdictvalue(kwargs, self.return_contents[0]) if return_contents != self._last_return_contents: self._setup_update_binding(return_contents) def _setup_update_binding(self, read_file: bool) -> None: Loading
tests/gallery/models/file_upload.py +4 −0 Original line number Diff line number Diff line """Model for MVVM demo of FileUpload.""" from typing import List from pydantic import BaseModel, Field class FileUploadState(BaseModel): """Model for MVVM demo of FileUpload.""" extensions: List[str] = Field(default=[".cif", ".nxs"]) file: str = Field(default="") label: str = Field(default="Upload File")
tests/gallery/view_models/file_upload.py +3 −0 Original line number Diff line number Diff line Loading @@ -16,3 +16,6 @@ class FileUploadVM: def on_update(self, data: Dict[str, Any]) -> None: print("file size:", sys.getsizeof(self.model.file)) def update_view(self) -> None: self.model_bind.update_in_view(self.model)
tests/gallery/views/app.py +6 −1 Original line number Diff line number Diff line Loading @@ -383,7 +383,12 @@ class App(ThemedApp): vuetify.VCardTitle("Form Inputs & Controls") with GridLayout(columns=3, valign="center"): FileUpload(v_model="file_upload.file", base_paths=["/HFIR", "/SNS"], label="Upload File") FileUpload( v_model="file_upload.file", base_paths=["/HFIR", "/SNS"], extensions=("file_upload.extensions",), label="{{ file_upload.label }}", ) with html.Div(): InputField( v_model="autoscroll", Loading