Repository: 12rambau/sepal_ui

Issue:
Restrict maximum parent level from InputFile
I have some apps where I’m interested on only search up to certain level, i.e., module_downloads, and I think that in the most of them, the user doesn’t need to go upper from sepal_user, once they start clicking, they could easily get lost over multiple folders.

what if we implement a parameter called: max_depth? We could use int values to this parameter, what do you think?

Discussion:

Patch:
diff --git a/sepal_ui/sepalwidgets/inputs.py b/sepal_ui/sepalwidgets/inputs.py
index 04e69553..cd561bb5 100644
--- a/sepal_ui/sepalwidgets/inputs.py
+++ b/sepal_ui/sepalwidgets/inputs.py
@@ -205,6 +205,9 @@ class FileInput(v.Flex, SepalWidget):
     clear: Optional[v.Btn] = None
     "clear btn to remove everything and set back to the ini folder"
 
+    root: t.Unicode = t.Unicode("").tag(sync=True)
+    "the root folder from which you cannot go higher in the tree."
+
     v_model: t.Unicode = t.Unicode(None, allow_none=True).tag(sync=True)
     "the v_model of the input"
 
@@ -218,6 +221,7 @@ class FileInput(v.Flex, SepalWidget):
         label: str = ms.widgets.fileinput.label,
         v_model: Union[str, None] = "",
         clearable: bool = False,
+        root: Union[str, Path] = "",
         **kwargs,
     ) -> None:
         """
@@ -229,10 +233,12 @@ class FileInput(v.Flex, SepalWidget):
             label: the label of the input
             v_model: the default value
             clearable: wether or not to make the widget clearable. default to False
+            root: the root folder from which you cannot go higher in the tree.
             kwargs: any parameter from a v.Flex abject. If set, 'children' will be overwritten.
         """
         self.extentions = extentions
         self.folder = Path(folder)
+        self.root = str(root) if isinstance(root, Path) else root
 
         self.selected_file = v.TextField(
             readonly=True,
@@ -441,7 +447,10 @@ class FileInput(v.Flex, SepalWidget):
 
         folder_list = humansorted(folder_list, key=lambda x: x.value)
         file_list = humansorted(file_list, key=lambda x: x.value)
+        folder_list.extend(file_list)
 
+        # add the parent item if root is set and is not reached yet
+        # if root is not set then we always display it
         parent_item = v.ListItem(
             value=str(folder.parent),
             children=[
@@ -458,9 +467,11 @@ class FileInput(v.Flex, SepalWidget):
                 ),
             ],
         )
-
-        folder_list.extend(file_list)
-        folder_list.insert(0, parent_item)
+        root_folder = Path(self.root)
+        if self.root == "":
+            folder_list.insert(0, parent_item)
+        elif root_folder in folder.parents:
+            folder_list.insert(0, parent_item)
 
         return folder_list
 
diff --git a/sepal_ui/sepalwidgets/sepalwidget.py b/sepal_ui/sepalwidgets/sepalwidget.py
index 79428748..d9d10451 100644
--- a/sepal_ui/sepalwidgets/sepalwidget.py
+++ b/sepal_ui/sepalwidgets/sepalwidget.py
@@ -13,7 +13,7 @@ Example:
 """
 
 import warnings
-from typing import Optional, Union
+from typing import List, Optional, Union
 
 import ipyvuetify as v
 import traitlets as t
@@ -125,7 +125,7 @@ class SepalWidget(v.VuetifyWidget):
         value: str = "",
         id_: str = "",
         elements: Optional[list] = None,
-    ) -> list:
+    ) -> List[v.VuetifyWidget]:
         r"""
         Recursively search for every element matching the specifications.
 


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index f8b2c217..bac6feee 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,6 +1,7 @@
 import uuid
 from itertools import product
 from pathlib import Path
+from typing import Optional
 
 import ee
 import geopandas as gpd
@@ -20,7 +21,7 @@ except Exception:
 
 
 @pytest.fixture(scope="session")
-def root_dir():
+def root_dir() -> Path:
     """
     Path to the root dir of the librairy.
     """
@@ -28,7 +29,7 @@ def root_dir():
 
 
 @pytest.fixture(scope="session")
-def tmp_dir():
+def tmp_dir() -> Path:
     """
     Creates a temporary local directory to store data.
     """
@@ -39,7 +40,7 @@ def tmp_dir():
 
 
 @pytest.fixture(scope="session")
-def _hash():
+def _hash() -> str:
     """
     Create a hash for each test instance.
     """
@@ -47,7 +48,7 @@ def _hash():
 
 
 @pytest.fixture(scope="session")
-def gee_dir(_hash):
+def gee_dir(_hash: str) -> Optional[Path]:
     """
     Create a test dir based on earthengine initialization
     populate it with fake super small assets:
@@ -131,6 +132,6 @@ def gee_dir(_hash):
 
 
 @pytest.fixture
-def alert():
+def alert() -> sw.Alert:
     """return a dummy alert that can be used everywhere to display informations."""
     return sw.Alert()
diff --git a/tests/test_FileInput.py b/tests/test_FileInput.py
index 6fbba7a8..089f519b 100644
--- a/tests/test_FileInput.py
+++ b/tests/test_FileInput.py
@@ -1,3 +1,7 @@
+from pathlib import Path
+from typing import List
+
+import ipyvuetify as v
 import pytest
 from traitlets import Any
 
@@ -119,20 +123,32 @@ class TestFileInput:
 
         return
 
+    def test_root(self, file_input: sw.FileInput, root_dir: Path) -> None:
+        """Add a root folder to a file_input and check that you can't go higher"""
+
+        # set the root to the current folder and reload
+        file_input.root = str(root_dir)
+        file_input._on_reload()
+        first_title_item = file_input.get_children(klass=v.ListItemTitle)[0]
+
+        assert ".. /" not in first_title_item.children[0]
+
+        return
+
     @pytest.fixture
-    def file_input(self, root_dir):
+    def file_input(self, root_dir: Path) -> sw.FileInput:
         """create a default file_input in the root_dir."""
         return sw.FileInput(folder=root_dir)
 
     @pytest.fixture
-    def readme(self, root_dir):
+    def readme(self, root_dir: Path) -> Path:
         """return the readme file path."""
         return root_dir / "README.rst"
 
     @staticmethod
-    def get_names(widget):
+    def get_names(file_input: sw.FileInput) -> List[str]:
         """get the list name of a fileinput object."""
-        item_list = widget.file_list.children[0].children
+        item_list = file_input.file_list.children[0].children
 
         def get_name(item):
             return item.children[1].children[0].children[0]