From 65437ce832f806da316aa074539b6263e1d8b7ac Mon Sep 17 00:00:00 2001 From: aliabid94 Date: Tue, 13 Feb 2024 16:51:47 -0600 Subject: [PATCH] Improve File Explorer performance (#7337) * changes * add changeset * changes * changes * add changeset * changes * changes * changes * changes * changes * Update gradio/components/file_explorer.py Co-authored-by: Abubakar Abid * Update gradio/components/file_explorer.py Co-authored-by: Abubakar Abid * Update demo/file_explorer_component_events/run.py Co-authored-by: Abubakar Abid * changes * changes * changes --------- Co-authored-by: Ali Abid Co-authored-by: gradio-pr-bot Co-authored-by: Abubakar Abid --- .changeset/chatty-rules-press.md | 6 + demo/file_explorer/run.ipynb | 2 +- demo/file_explorer/run.py | 6 +- .../dir3/dir3_bar.log | 0 .../dir3/dir4/dir_4_bar.log | 0 demo/file_explorer_component_events/run.ipynb | 2 +- demo/file_explorer_component_events/run.py | 18 +- gradio/components/file_explorer.py | 105 ++----- .../file_explorer_component_events.spec.ts | 38 +-- js/fileexplorer/Index.svelte | 28 +- js/fileexplorer/shared/Checkbox.svelte | 12 +- .../shared/DirectoryExplorer.svelte | 107 ++++--- js/fileexplorer/shared/FileTree.svelte | 121 +++++--- js/fileexplorer/shared/types.ts | 5 + js/fileexplorer/shared/utils.test.ts | 187 ------------ js/fileexplorer/shared/utils.ts | 273 ------------------ scripts/copy_demos.py | 1 + test/test_components.py | 72 +---- test/test_utils.py | 3 +- 19 files changed, 244 insertions(+), 742 deletions(-) create mode 100644 .changeset/chatty-rules-press.md create mode 100644 demo/file_explorer_component_events/dir3/dir3_bar.log create mode 100644 demo/file_explorer_component_events/dir3/dir4/dir_4_bar.log create mode 100644 js/fileexplorer/shared/types.ts delete mode 100644 js/fileexplorer/shared/utils.test.ts delete mode 100644 js/fileexplorer/shared/utils.ts diff --git a/.changeset/chatty-rules-press.md b/.changeset/chatty-rules-press.md new file mode 100644 index 0000000000..dcec9ee9ec --- /dev/null +++ b/.changeset/chatty-rules-press.md @@ -0,0 +1,6 @@ +--- +"@gradio/fileexplorer": patch +"gradio": patch +--- + +fix:Improve File Explorer performance diff --git a/demo/file_explorer/run.ipynb b/demo/file_explorer/run.ipynb index 34a79bc6e0..c9553314b2 100644 --- a/demo/file_explorer/run.ipynb +++ b/demo/file_explorer/run.ipynb @@ -1 +1 @@ -{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: file_explorer"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "from pathlib import Path\n", "\n", "current_file_path = Path(__file__).resolve()\n", "relative_path = \"path/to/file\"\n", "absolute_path = (current_file_path.parent / \"..\" / \"..\" / \"gradio\").resolve()\n", "\n", "\n", "def get_file_content(file):\n", " return (file,)\n", "\n", "\n", "with gr.Blocks() as demo:\n", " gr.Markdown('### `FileExplorer` to `FileExplorer` -- `file_count=\"multiple\"`')\n", " submit_btn = gr.Button(\"Select\")\n", " with gr.Row():\n", " file = gr.FileExplorer(\n", " glob=\"**/{components,themes}/*.py\",\n", " # value=[\"themes/utils\"],\n", " root=absolute_path,\n", " ignore_glob=\"**/__init__.py\",\n", " )\n", "\n", " file2 = gr.FileExplorer(\n", " glob=\"**/{components,themes}/**/*.py\",\n", " root=absolute_path,\n", " ignore_glob=\"**/__init__.py\",\n", " )\n", " submit_btn.click(lambda x: x, file, file2)\n", "\n", " gr.Markdown(\"---\")\n", " gr.Markdown('### `FileExplorer` to `Code` -- `file_count=\"single\"`')\n", " with gr.Group():\n", " with gr.Row():\n", " file_3 = gr.FileExplorer(\n", " scale=1,\n", " glob=\"**/{components,themes}/**/*.py\",\n", " value=[\"themes/utils\"],\n", " file_count=\"single\",\n", " root=absolute_path,\n", " ignore_glob=\"**/__init__.py\",\n", " elem_id=\"file\",\n", " )\n", "\n", " code = gr.Code(lines=30, scale=2, language=\"python\")\n", "\n", " file_3.change(get_file_content, file_3, code)\n", "\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file +{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: file_explorer"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "from pathlib import Path\n", "\n", "current_file_path = Path(__file__).resolve()\n", "relative_path = \"path/to/file\"\n", "absolute_path = (current_file_path.parent / \"..\" / \"..\" / \"gradio\").resolve()\n", "\n", "\n", "def get_file_content(file):\n", " return (file,)\n", "\n", "\n", "with gr.Blocks() as demo:\n", " gr.Markdown('### `FileExplorer` to `FileExplorer` -- `file_count=\"multiple\"`')\n", " submit_btn = gr.Button(\"Select\")\n", " with gr.Row():\n", " file = gr.FileExplorer(\n", " glob=\"**/components/*.py\",\n", " # value=[\"themes/utils\"],\n", " root=absolute_path,\n", " ignore_glob=\"**/__init__.py\",\n", " )\n", "\n", " file2 = gr.FileExplorer(\n", " glob=\"**/components/**/*.py\",\n", " root=absolute_path,\n", " ignore_glob=\"**/__init__.py\",\n", " )\n", " submit_btn.click(lambda x: x, file, file2)\n", "\n", " gr.Markdown(\"---\")\n", " gr.Markdown('### `FileExplorer` to `Code` -- `file_count=\"single\"`')\n", " with gr.Group():\n", " with gr.Row():\n", " file_3 = gr.FileExplorer(\n", " scale=1,\n", " glob=\"**/components/**/*.py\",\n", " value=[\"themes/utils\"],\n", " file_count=\"single\",\n", " root=absolute_path,\n", " ignore_glob=\"**/__init__.py\",\n", " elem_id=\"file\",\n", " )\n", "\n", " code = gr.Code(lines=30, scale=2, language=\"python\")\n", "\n", " file_3.change(get_file_content, file_3, code)\n", "\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file diff --git a/demo/file_explorer/run.py b/demo/file_explorer/run.py index 6db60c1888..ab4f604a30 100644 --- a/demo/file_explorer/run.py +++ b/demo/file_explorer/run.py @@ -15,14 +15,14 @@ with gr.Blocks() as demo: submit_btn = gr.Button("Select") with gr.Row(): file = gr.FileExplorer( - glob="**/{components,themes}/*.py", + glob="**/components/*.py", # value=["themes/utils"], root=absolute_path, ignore_glob="**/__init__.py", ) file2 = gr.FileExplorer( - glob="**/{components,themes}/**/*.py", + glob="**/components/**/*.py", root=absolute_path, ignore_glob="**/__init__.py", ) @@ -34,7 +34,7 @@ with gr.Blocks() as demo: with gr.Row(): file_3 = gr.FileExplorer( scale=1, - glob="**/{components,themes}/**/*.py", + glob="**/components/**/*.py", value=["themes/utils"], file_count="single", root=absolute_path, diff --git a/demo/file_explorer_component_events/dir3/dir3_bar.log b/demo/file_explorer_component_events/dir3/dir3_bar.log new file mode 100644 index 0000000000..e69de29bb2 diff --git a/demo/file_explorer_component_events/dir3/dir4/dir_4_bar.log b/demo/file_explorer_component_events/dir3/dir4/dir_4_bar.log new file mode 100644 index 0000000000..e69de29bb2 diff --git a/demo/file_explorer_component_events/run.ipynb b/demo/file_explorer_component_events/run.ipynb index 60efe8e1e6..a9f9287efc 100644 --- a/demo/file_explorer_component_events/run.ipynb +++ b/demo/file_explorer_component_events/run.ipynb @@ -1 +1 @@ -{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: file_explorer_component_events"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["# Downloading files from the demo repo\n", "import os\n", "os.mkdir('dir1')\n", "!wget -q -O dir1/bar.txt https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir1/bar.txt\n", "!wget -q -O dir1/foo.txt https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir1/foo.txt\n", "os.mkdir('dir2')\n", "!wget -q -O dir2/baz.png https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir2/baz.png\n", "!wget -q -O dir2/foo.png https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir2/foo.png\n", "os.mkdir('dir3')\n", "!wget -q -O dir3/dir3_foo.txt https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir3/dir3_foo.txt\n", "!wget -q -O dir3/dir4 https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir3/dir4"]}, {"cell_type": "code", "execution_count": null, "id": "44380577570523278879349135829904343037", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "from pathlib import Path\n", "\n", "base_root = Path(__file__).parent.resolve()\n", "\n", "with gr.Blocks() as demo:\n", " with gr.Row():\n", " dd = gr.Dropdown(label=\"Select File Explorer Root\",\n", " value=str(base_root / \"dir1\"),\n", " choices=[str(base_root / \"dir1\"), str(base_root / \"dir2\"),\n", " str(base_root / \"dir3\")])\n", " with gr.Group():\n", " dir_only_glob = gr.Checkbox(label=\"Show only directories\", value=False)\n", " ignore_dir_in_glob = gr.Checkbox(label=\"Ignore directories in glob\", value=False)\n", "\n", " fe = gr.FileExplorer(root=str(base_root / \"dir1\"),\n", " glob=\"**/*\", interactive=True)\n", " textbox = gr.Textbox(label=\"Selected Directory\")\n", " run = gr.Button(\"Run\")\n", " \n", " dir_only_glob.select(lambda s: gr.FileExplorer(glob=\"**/\" if s else \"**/*.*\",\n", " file_count=\"multiple\",\n", " root=str(base_root / \"dir3\")) ,\n", " inputs=[dir_only_glob], outputs=[fe])\n", " ignore_dir_in_glob.select(lambda s: gr.FileExplorer(glob=\"**/*\",\n", " ignore_glob=\"**/\",\n", " root=str(base_root / \"dir3\")),\n", " inputs=[ignore_dir_in_glob], outputs=[fe]) \n", "\n", " dd.select(lambda s: gr.FileExplorer(root=s), inputs=[dd], outputs=[fe])\n", " run.click(lambda s: \",\".join(s) if isinstance(s, list) else s, inputs=[fe], outputs=[textbox])\n", "\n", " with gr.Row():\n", " a = gr.Textbox(elem_id=\"input-box\")\n", " a.change(lambda x: x, inputs=[a])\n", "\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file +{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: file_explorer_component_events"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["# Downloading files from the demo repo\n", "import os\n", "os.mkdir('dir1')\n", "!wget -q -O dir1/bar.txt https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir1/bar.txt\n", "!wget -q -O dir1/foo.txt https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir1/foo.txt\n", "os.mkdir('dir2')\n", "!wget -q -O dir2/baz.png https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir2/baz.png\n", "!wget -q -O dir2/foo.png https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir2/foo.png\n", "os.mkdir('dir3')\n", "!wget -q -O dir3/dir3_bar.log https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir3/dir3_bar.log\n", "!wget -q -O dir3/dir3_foo.txt https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir3/dir3_foo.txt\n", "!wget -q -O dir3/dir4 https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir3/dir4"]}, {"cell_type": "code", "execution_count": null, "id": "44380577570523278879349135829904343037", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "from pathlib import Path\n", "\n", "base_root = Path(__file__).parent.resolve()\n", "\n", "with gr.Blocks() as demo:\n", " with gr.Row():\n", " dd = gr.Dropdown(label=\"Select File Explorer Root\",\n", " value=str(base_root / \"dir1\"),\n", " choices=[str(base_root / \"dir1\"), str(base_root / \"dir2\"),\n", " str(base_root / \"dir3\")])\n", " with gr.Group():\n", " txt_only_glob = gr.Checkbox(label=\"Show only text files\", value=False)\n", " ignore_txt_in_glob = gr.Checkbox(label=\"Ignore text files in glob\", value=False)\n", "\n", " fe = gr.FileExplorer(root_dir=str(base_root / \"dir1\"),\n", " glob=\"**/*\", interactive=True)\n", " textbox = gr.Textbox(label=\"Selected Directory\")\n", " run = gr.Button(\"Run\")\n", " \n", " txt_only_glob.select(lambda s: gr.FileExplorer(glob=\"*.txt\" if s else \"*\") ,\n", " inputs=[txt_only_glob], outputs=[fe])\n", " ignore_txt_in_glob.select(lambda s: gr.FileExplorer(ignore_glob=\"*.txt\" if s else None),\n", " inputs=[ignore_txt_in_glob], outputs=[fe]) \n", "\n", " dd.select(lambda s: gr.FileExplorer(root=s), inputs=[dd], outputs=[fe])\n", " run.click(lambda s: \",\".join(s) if isinstance(s, list) else s, inputs=[fe], outputs=[textbox])\n", "\n", " with gr.Row():\n", " a = gr.Textbox(elem_id=\"input-box\")\n", " a.change(lambda x: x, inputs=[a])\n", "\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file diff --git a/demo/file_explorer_component_events/run.py b/demo/file_explorer_component_events/run.py index 0e1022d20b..994e95c4cf 100644 --- a/demo/file_explorer_component_events/run.py +++ b/demo/file_explorer_component_events/run.py @@ -10,22 +10,18 @@ with gr.Blocks() as demo: choices=[str(base_root / "dir1"), str(base_root / "dir2"), str(base_root / "dir3")]) with gr.Group(): - dir_only_glob = gr.Checkbox(label="Show only directories", value=False) - ignore_dir_in_glob = gr.Checkbox(label="Ignore directories in glob", value=False) + txt_only_glob = gr.Checkbox(label="Show only text files", value=False) + ignore_txt_in_glob = gr.Checkbox(label="Ignore text files in glob", value=False) - fe = gr.FileExplorer(root=str(base_root / "dir1"), + fe = gr.FileExplorer(root_dir=str(base_root / "dir1"), glob="**/*", interactive=True) textbox = gr.Textbox(label="Selected Directory") run = gr.Button("Run") - dir_only_glob.select(lambda s: gr.FileExplorer(glob="**/" if s else "**/*.*", - file_count="multiple", - root=str(base_root / "dir3")) , - inputs=[dir_only_glob], outputs=[fe]) - ignore_dir_in_glob.select(lambda s: gr.FileExplorer(glob="**/*", - ignore_glob="**/", - root=str(base_root / "dir3")), - inputs=[ignore_dir_in_glob], outputs=[fe]) + txt_only_glob.select(lambda s: gr.FileExplorer(glob="*.txt" if s else "*") , + inputs=[txt_only_glob], outputs=[fe]) + ignore_txt_in_glob.select(lambda s: gr.FileExplorer(ignore_glob="*.txt" if s else None), + inputs=[ignore_txt_in_glob], outputs=[fe]) dd.select(lambda s: gr.FileExplorer(root=s), inputs=[dd], outputs=[fe]) run.click(lambda s: ",".join(s) if isinstance(s, list) else s, inputs=[fe], outputs=[textbox]) diff --git a/gradio/components/file_explorer.py b/gradio/components/file_explorer.py index fb7686b065..d2076a6b2b 100644 --- a/gradio/components/file_explorer.py +++ b/gradio/components/file_explorer.py @@ -2,9 +2,8 @@ from __future__ import annotations -import itertools +import fnmatch import os -import re import warnings from pathlib import Path from typing import Any, Callable, List, Literal @@ -33,7 +32,7 @@ class FileExplorer(Component): def __init__( self, - glob: str = "**/*.*", + glob: str = "**/*", *, value: str | list[str] | Callable | None = None, file_count: Literal["single", "multiple"] = "multiple", @@ -59,7 +58,7 @@ class FileExplorer(Component): value: The file (or list of files, depending on the `file_count` parameter) to show as "selected" when the component is first loaded. If a callable is provided, it will be called when the app loads to set the initial value of the component. If not provided, no files are shown as selected. file_count: Whether to allow single or multiple files to be selected. If "single", the component will return a single absolute file path as a string. If "multiple", the component will return a list of absolute file paths as a list of strings. root_dir: Path to root directory to select files from. If not provided, defaults to current working directory. - ignore_glob: The glob-tyle pattern that will be used to exclude files from the list. For example, "*.py" will exclude all .py files from the list. See the Python glob documentation at https://docs.python.org/3/library/glob.html for more information. + ignore_glob: The glob-style, case-sensitive pattern that will be used to exclude files from the list. For example, "*.py" will exclude all .py files from the list. See the Python glob documentation at https://docs.python.org/3/library/glob.html for more information. label: The label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to. every: If `value` is a callable, run the function 'every' number of seconds while the client connection is open. Has no effect otherwise.sed (e.g. to cancel it) via this component's .load_event attribute. show_label: if True, will display label. @@ -82,10 +81,10 @@ class FileExplorer(Component): self.root_dir = os.path.abspath(root_dir) self.glob = glob self.ignore_glob = ignore_glob - valid_file_count = ["single", "multiple", "directory"] + valid_file_count = ["single", "multiple"] if file_count not in valid_file_count: raise ValueError( - f"Invalid value for parameter `type`: {type}. Please choose from one of: {valid_file_count}" + f"Invalid value for parameter `file_count`: {file_count}. Please choose from one of: {valid_file_count}" ) self.file_count = file_count self.height = height @@ -156,80 +155,40 @@ class FileExplorer(Component): return FileExplorerData(root=root) @server - def ls(self, _=None) -> list[dict[str, str]] | None: + def ls(self, subdirectory: list | None = None) -> list[dict[str, str]] | None: """ Returns: - tuple of list of files in directory, then list of folders in directory + a list of dictionaries, where each dictionary represents a file or subdirectory in the given subdirectory """ + if subdirectory is None: + subdirectory = [] - def expand_braces(text, seen=None): - if seen is None: - seen = set() + full_subdir_path = self._safe_join(subdirectory) - spans = [m.span() for m in re.finditer("{[^{}]*}", text)][::-1] - alts = [text[start + 1 : stop - 1].split(",") for start, stop in spans] + try: + subdir_items = sorted(os.listdir(full_subdir_path)) + except FileNotFoundError: + return [] - if len(spans) == 0: - if text not in seen: - yield text - seen.add(text) + files, folders = [], [] + for item in subdir_items: + full_path = os.path.join(full_subdir_path, item) + is_file = not os.path.isdir(full_path) + valid_by_glob = fnmatch.fnmatch(full_path, self.glob) + if is_file and not valid_by_glob: + continue + if self.ignore_glob and fnmatch.fnmatch(full_path, self.ignore_glob): + continue + target = files if is_file else folders + target.append( + { + "name": item, + "type": "file" if is_file else "folder", + "valid": valid_by_glob, + } + ) - else: - for combo in itertools.product(*alts): - replaced = list(text) - for (start, stop), replacement in zip(spans, combo): - replaced[start:stop] = replacement - - yield from expand_braces("".join(replaced), seen) - - def make_tree(files): - tree = [] - for file in files: - parts = file.split(os.path.sep) - make_node(parts, tree) - return tree - - def make_node(parts, tree): - _tree = tree - for i in range(len(parts)): - if _tree is None: - continue - if i == len(parts) - 1: - type = "file" - _tree.append({"path": parts[i], "type": type, "children": None}) - continue - type = "folder" - j = next( - (index for (index, v) in enumerate(_tree) if v["path"] == parts[i]), - None, - ) - if j is not None: - _tree = _tree[j]["children"] - else: - _tree.append({"path": parts[i], "type": type, "children": []}) - _tree = _tree[-1]["children"] - - files: list[Path] = [] - for result in expand_braces(self.glob): - files += list(Path(self.root_dir).resolve().glob(result)) - - files = [f for f in files if f != Path(self.root_dir).resolve()] - - ignore_files = [] - if self.ignore_glob: - for result in expand_braces(self.ignore_glob): - ignore_files += list(Path(self.root_dir).resolve().glob(result)) - files = list(set(files) - set(ignore_files)) - - files_with_sep = [] - for f in files: - file = str(f.relative_to(self.root_dir)) - if f.is_dir(): - file += os.path.sep - files_with_sep.append(file) - - tree = make_tree(files_with_sep) - return tree + return folders + files def _safe_join(self, folders): combined_path = os.path.join(self.root_dir, *folders) diff --git a/js/app/test/file_explorer_component_events.spec.ts b/js/app/test/file_explorer_component_events.spec.ts index 99e5fb9028..bd25f97baf 100644 --- a/js/app/test/file_explorer_component_events.spec.ts +++ b/js/app/test/file_explorer_component_events.spec.ts @@ -52,9 +52,9 @@ test("File Explorer correctly displays both directories and files. Directories i await page .locator("li") - .filter({ hasText: "dir4 dir5 dir7 . dir_4_foo.txt" }) + .filter({ hasText: "dir4 . dir5 dir7 dir_4_bar.log dir_4_foo.txt" }) .getByRole("checkbox") - .nth(3) + .first() .check(); await page @@ -97,20 +97,21 @@ test("File Explorer selects all children when top level directory is selected.", const directory_paths_displayed = async () => { const value = await page.getByLabel("Selected Directory").inputValue(); const files_and_dirs = value.split(","); - expect(files_and_dirs.length).toBe(6); + expect(files_and_dirs.length).toBe(7); }; await expect(directory_paths_displayed).toPass(); }); -test("File Explorer correctly displays only directories and properly adds it to the value", async ({ - page -}) => { +test("File Explorer correctly displays only text files", async ({ page }) => { const check = page.getByRole("checkbox", { - name: "Show only directories", + name: "Show only text files", exact: true }); await check.click(); + await page.getByLabel("Select File Explorer Root").click(); + await page.getByLabel(new RegExp("/dir3$"), { exact: true }).first().click(); + await page .locator("span") .filter({ hasText: "dir4" }) @@ -118,25 +119,27 @@ test("File Explorer correctly displays only directories and properly adds it to .check(); await page.getByRole("button", { name: "Run" }).click(); - const directory_paths_displayed = async () => { + const text_files_displayed = async () => { const value = await page.getByLabel("Selected Directory").inputValue(); const dirs = value.split(","); - expect(dirs.some((f) => f.endsWith("dir4"))).toBeTruthy(); - expect(dirs.some((f) => f.endsWith("dir5"))).toBeTruthy(); - expect(dirs.some((f) => f.endsWith("dir7"))).toBeTruthy(); + expect(dirs.length).toBe(3); + expect(dirs.every((d) => d.endsWith(".txt"))).toBeTruthy(); }; - await expect(directory_paths_displayed).toPass(); + await expect(text_files_displayed).toPass(); }); -test("File Explorer correctly excludes directories when ignore_glob is '**/'.", async ({ +test("File Explorer correctly excludes text files when ignore_glob is '*.txt'.", async ({ page }) => { const check = page.getByRole("checkbox", { - name: "Ignore directories in glob", + name: "Ignore text files in glob", exact: true }); await check.click(); + await page.getByLabel("Select File Explorer Root").click(); + await page.getByLabel(new RegExp("/dir3$"), { exact: true }).first().click(); + await page .locator("span") .filter({ hasText: "dir4" }) @@ -147,10 +150,9 @@ test("File Explorer correctly excludes directories when ignore_glob is '**/'.", const only_files_displayed = async () => { const value = await page.getByLabel("Selected Directory").inputValue(); const files = value.split(","); - expect(files.length).toBe(3); - expect(files.some((f) => f.endsWith("dir_4_foo.txt"))).toBeTruthy(); - expect(files.some((f) => f.endsWith("dir5_foo.txt"))).toBeTruthy(); - expect(files.some((f) => f.endsWith("dir7_foo.txt"))).toBeTruthy(); + expect(files.length).toBe(4); + expect(files.some((f) => f.endsWith(".log"))).toBeTruthy(); + expect(files.some((f) => f.endsWith(".txt"))).toBeFalsy(); }; await expect(only_files_displayed).toPass(); }); diff --git a/js/fileexplorer/Index.svelte b/js/fileexplorer/Index.svelte index e7bb480835..a4c95ac2f5 100644 --- a/js/fileexplorer/Index.svelte +++ b/js/fileexplorer/Index.svelte @@ -3,6 +3,7 @@ - gradio.dispatch("change")} - /> + {#key rerender_key} + gradio.dispatch("change")} + /> + {/key} diff --git a/js/fileexplorer/shared/Checkbox.svelte b/js/fileexplorer/shared/Checkbox.svelte index 0a9fd0dd95..6e35e30b27 100644 --- a/js/fileexplorer/shared/Checkbox.svelte +++ b/js/fileexplorer/shared/Checkbox.svelte @@ -7,10 +7,8 @@ dispatch("change", !value)} type="checkbox" - on:click={() => dispatch("change", value)} - on:keydown={({ key }) => - (key === " " || key === "Enter") && dispatch("change", value)} {disabled} class:disabled={disabled && !value} /> @@ -47,14 +45,6 @@ } .disabled { - cursor: not-allowed; - border-color: var(--checkbox-border-color-hover); - background-color: var(--checkbox-background-color-hover); - } - - input:disabled:checked, - input:disabled:checked:hover, - .disabled:checked:focus { opacity: 0.8 !important; cursor: not-allowed; } diff --git a/js/fileexplorer/shared/DirectoryExplorer.svelte b/js/fileexplorer/shared/DirectoryExplorer.svelte index 754dbff5d7..2b4ace8d05 100644 --- a/js/fileexplorer/shared/DirectoryExplorer.svelte +++ b/js/fileexplorer/shared/DirectoryExplorer.svelte @@ -1,72 +1,65 @@ -{#if $tree && $tree.length} -
- handle_select(detail)} - {file_count} - /> -
-{:else} - -{/if} +
+ { + const { path, checked, type } = e.detail; + if (checked) { + if (file_count === "single") { + value = [path]; + } else if (type === "folder") { + if (!path_in_set(path, selected_folders)) { + selected_folders = [...selected_folders, path]; + } + } else { + if (!path_in_set(path, value)) { + value = [...value, path]; + } + } + } else { + selected_folders = selected_folders.filter( + (folder) => !path_inside(path, folder) + ); // deselect all parent folders + if (type === "folder") { + selected_folders = selected_folders.filter( + (folder) => !path_inside(folder, path) + ); // deselect all children folders + value = value.filter((file) => !path_inside(file, path)); // deselect all children files + } else { + value = value.filter((x) => !paths_equal(x, path)); + } + } + }} + /> +