mirror of
https://github.com/gradio-app/gradio.git
synced 2025-01-24 10:54:04 +08:00
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 <abubakar@huggingface.co> * Update gradio/components/file_explorer.py Co-authored-by: Abubakar Abid <abubakar@huggingface.co> * Update demo/file_explorer_component_events/run.py Co-authored-by: Abubakar Abid <abubakar@huggingface.co> * changes * changes * changes --------- Co-authored-by: Ali Abid <aliabid94@gmail.com> Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com> Co-authored-by: Abubakar Abid <abubakar@huggingface.co>
This commit is contained in:
parent
547517b74e
commit
65437ce832
6
.changeset/chatty-rules-press.md
Normal file
6
.changeset/chatty-rules-press.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
"@gradio/fileexplorer": patch
|
||||
"gradio": patch
|
||||
---
|
||||
|
||||
fix:Improve File Explorer performance
|
@ -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}
|
||||
{"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}
|
@ -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,
|
||||
|
@ -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}
|
||||
{"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}
|
@ -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])
|
||||
|
@ -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)
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -3,6 +3,7 @@
|
||||
<script lang="ts">
|
||||
import type { Gradio, SelectData } from "@gradio/utils";
|
||||
import { File } from "@gradio/icons";
|
||||
import type { FileNode } from "./shared/types";
|
||||
|
||||
import { Block, BlockLabel } from "@gradio/atoms";
|
||||
import DirectoryExplorer from "./shared/DirectoryExplorer.svelte";
|
||||
@ -20,11 +21,9 @@
|
||||
export let show_label: boolean;
|
||||
export let height: number | undefined = undefined;
|
||||
export let file_count: "single" | "multiple" = "multiple";
|
||||
|
||||
export let root_dir: string;
|
||||
export let glob: string;
|
||||
export let ignore_glob: string;
|
||||
export let root_dir: string;
|
||||
|
||||
export let loading_status: LoadingStatus;
|
||||
export let container = true;
|
||||
export let scale: number | null = null;
|
||||
@ -33,9 +32,11 @@
|
||||
change: never;
|
||||
}>;
|
||||
export let server: {
|
||||
ls: (path: string[]) => Promise<[string[], string[]]>;
|
||||
ls: (path: string[]) => Promise<FileNode[]>;
|
||||
};
|
||||
export let interactive: boolean;
|
||||
|
||||
$: rerender_key = [root_dir, glob, ignore_glob];
|
||||
</script>
|
||||
|
||||
<Block
|
||||
@ -62,14 +63,13 @@
|
||||
label={label || "FileExplorer"}
|
||||
float={false}
|
||||
/>
|
||||
<DirectoryExplorer
|
||||
bind:value
|
||||
{file_count}
|
||||
{server}
|
||||
{interactive}
|
||||
{root_dir}
|
||||
{glob}
|
||||
{ignore_glob}
|
||||
on:change={() => gradio.dispatch("change")}
|
||||
/>
|
||||
{#key rerender_key}
|
||||
<DirectoryExplorer
|
||||
bind:value
|
||||
{file_count}
|
||||
{interactive}
|
||||
ls_fn={server.ls}
|
||||
on:change={() => gradio.dispatch("change")}
|
||||
/>
|
||||
{/key}
|
||||
</Block>
|
||||
|
@ -7,10 +7,8 @@
|
||||
|
||||
<input
|
||||
bind:checked={value}
|
||||
on:input={() => 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;
|
||||
}
|
||||
|
@ -1,72 +1,65 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { dequal } from "dequal";
|
||||
import FileTree from "./FileTree.svelte";
|
||||
import { make_fs_store } from "./utils";
|
||||
import { File } from "@gradio/icons";
|
||||
import { Empty } from "@gradio/atoms";
|
||||
import type { FileNode } from "./types";
|
||||
|
||||
export let glob: string;
|
||||
export let ignore_glob: string;
|
||||
export let root_dir: string;
|
||||
export let interactive: boolean;
|
||||
export let server: any;
|
||||
export let file_count: "single" | "multiple" = "multiple";
|
||||
|
||||
export let value: string[][] = [];
|
||||
export let ls_fn: (path: string[]) => Promise<FileNode[]>;
|
||||
let selected_folders: string[][] = [];
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
change: typeof value;
|
||||
}>();
|
||||
const tree = make_fs_store();
|
||||
|
||||
const render_tree = (): void => {
|
||||
server.ls().then((v: any) => {
|
||||
tree.create_fs_graph(v);
|
||||
});
|
||||
const paths_equal = (path: string[], path_2: string[]): boolean => {
|
||||
return path.join("/") === path_2.join("/");
|
||||
};
|
||||
|
||||
$: glob, ignore_glob, root_dir, render_tree();
|
||||
const path_in_set = (path: string[], set: string[][]): boolean => {
|
||||
return set.some((x) => paths_equal(x, path));
|
||||
};
|
||||
|
||||
$: value.length && $tree && set_checked_from_paths();
|
||||
|
||||
function set_checked_from_paths(): void {
|
||||
value = file_count === "single" ? [value[0] || []] : value;
|
||||
value = tree.set_checked_from_paths(value);
|
||||
if (!dequal(value, old_value)) {
|
||||
old_value = value;
|
||||
dispatch("change", value);
|
||||
}
|
||||
}
|
||||
|
||||
let old_value: typeof value = [];
|
||||
function handle_select({
|
||||
node_indices,
|
||||
checked
|
||||
}: {
|
||||
node_indices: number[];
|
||||
checked: boolean;
|
||||
}): void {
|
||||
value = tree.set_checked(node_indices, checked, value, file_count);
|
||||
if (!dequal(value, old_value)) {
|
||||
old_value = value;
|
||||
dispatch("change", value);
|
||||
}
|
||||
}
|
||||
const path_inside = (path: string[], path_2: string[]): boolean => {
|
||||
return path.join("/").startsWith(path_2.join("/"));
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if $tree && $tree.length}
|
||||
<div class="file-wrap">
|
||||
<FileTree
|
||||
tree={$tree}
|
||||
{interactive}
|
||||
on:check={({ detail }) => handle_select(detail)}
|
||||
{file_count}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<Empty size="large"><File /></Empty>
|
||||
{/if}
|
||||
<div class="file-wrap">
|
||||
<FileTree
|
||||
path={[]}
|
||||
selected_files={value}
|
||||
{selected_folders}
|
||||
{interactive}
|
||||
{ls_fn}
|
||||
{file_count}
|
||||
valid_for_selection={false}
|
||||
on:check={(e) => {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.file-wrap {
|
||||
|
@ -1,76 +1,129 @@
|
||||
<script lang="ts">
|
||||
import type { Node } from "./utils";
|
||||
import { createEventDispatcher, tick } from "svelte";
|
||||
import type { FileNode } from "./types";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import Arrow from "./ArrowIcon.svelte";
|
||||
import Checkbox from "./Checkbox.svelte";
|
||||
import FileIcon from "../icons/light-file.svg";
|
||||
import FolderIcon from "../icons/light-folder.svg";
|
||||
|
||||
export let path: string[] = [];
|
||||
export let selected_files: string[][] = [];
|
||||
export let selected_folders: string[][] = [];
|
||||
export let is_selected_entirely = false;
|
||||
export let interactive: boolean;
|
||||
export let tree: Node[] = [];
|
||||
export let icons: any = {};
|
||||
export let node_indices: number[] = [];
|
||||
export let ls_fn: (path: string[]) => Promise<FileNode[]>;
|
||||
export let file_count: "single" | "multiple" = "multiple";
|
||||
export let valid_for_selection: boolean;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
check: { node_indices: number[]; checked: boolean };
|
||||
}>();
|
||||
let content: FileNode[] = [];
|
||||
let opened_folders: number[] = [];
|
||||
|
||||
async function dispatch_change(i: number): Promise<void> {
|
||||
await tick();
|
||||
const toggle_open_folder = (i: number): void => {
|
||||
if (opened_folders.includes(i)) {
|
||||
opened_folders = opened_folders.filter((x) => x !== i);
|
||||
} else {
|
||||
opened_folders = [...opened_folders, i];
|
||||
}
|
||||
};
|
||||
|
||||
dispatch("check", {
|
||||
node_indices: [...node_indices, i],
|
||||
checked: !tree[i].checked
|
||||
const open_folder = (i: number): void => {
|
||||
if (!opened_folders.includes(i)) {
|
||||
opened_folders = [...opened_folders, i];
|
||||
}
|
||||
};
|
||||
|
||||
(async () => {
|
||||
content = await ls_fn(path);
|
||||
if (valid_for_selection) {
|
||||
content = [{ name: ".", type: "file" }, ...content];
|
||||
}
|
||||
opened_folders = content
|
||||
.map((x, i) =>
|
||||
x.type === "folder" &&
|
||||
(is_selected_entirely || selected_files.some((y) => y[0] === x.name))
|
||||
? i
|
||||
: null
|
||||
)
|
||||
.filter((x): x is number => x !== null);
|
||||
})();
|
||||
|
||||
$: if (is_selected_entirely) {
|
||||
content.forEach((x) => {
|
||||
dispatch("check", {
|
||||
path: [...path, x.name],
|
||||
checked: true,
|
||||
type: x.type
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
check: { path: string[]; checked: boolean; type: "file" | "folder" };
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<ul>
|
||||
{#each tree as { type, path, children, children_visible, checked }, i}
|
||||
{#each content as { type, name, valid }, i}
|
||||
<li>
|
||||
<span class="wrap">
|
||||
<Checkbox
|
||||
disabled={!interactive ||
|
||||
(type === "folder" && file_count === "single")}
|
||||
bind:value={checked}
|
||||
on:change={() => dispatch_change(i)}
|
||||
value={(type === "file" ? selected_files : selected_folders).some(
|
||||
(x) => x[0] === name && x.length === 1
|
||||
)}
|
||||
on:change={(e) => {
|
||||
let checked = e.detail;
|
||||
dispatch("check", {
|
||||
path: [...path, name],
|
||||
checked,
|
||||
type
|
||||
});
|
||||
if (type === "folder" && checked) {
|
||||
open_folder(i);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if type === "folder"}
|
||||
<span
|
||||
class="icon"
|
||||
class:hidden={!tree[i].children_visible}
|
||||
on:click|stopPropagation={() =>
|
||||
(tree[i].children_visible = !tree[i].children_visible)}
|
||||
class:hidden={!opened_folders.includes(i)}
|
||||
on:click|stopPropagation={() => toggle_open_folder(i)}
|
||||
role="button"
|
||||
aria-label="expand directory"
|
||||
tabindex="0"
|
||||
on:keydown={({ key }) =>
|
||||
(key === " " || key === "Enter") &&
|
||||
(tree[i].children_visible = !tree[i].children_visible)}
|
||||
><Arrow /></span
|
||||
on:keydown={({ key }) => {
|
||||
if (key === " " || key === "Enter") {
|
||||
toggle_open_folder(i);
|
||||
}
|
||||
}}><Arrow /></span
|
||||
>
|
||||
{:else if path === ""}
|
||||
<span class="file-icon">
|
||||
<img src={FolderIcon} alt="folder icon" />
|
||||
</span>
|
||||
{:else}
|
||||
<span class="file-icon">
|
||||
<img src={FileIcon} alt="file icon" />
|
||||
<img src={name === "." ? FolderIcon : FileIcon} alt="file icon" />
|
||||
</span>
|
||||
{/if}
|
||||
{path ? path : "."}
|
||||
{name}
|
||||
</span>
|
||||
{#if children && children_visible}
|
||||
{#if type === "folder" && opened_folders.includes(i)}
|
||||
<svelte:self
|
||||
tree={children}
|
||||
{icons}
|
||||
on:check
|
||||
node_indices={[...node_indices, i]}
|
||||
path={[...path, name]}
|
||||
selected_files={selected_files
|
||||
.filter((x) => x[0] === name)
|
||||
.map((x) => x.slice(1))}
|
||||
selected_folders={selected_folders
|
||||
.filter((x) => x[0] === name)
|
||||
.map((x) => x.slice(1))}
|
||||
is_selected_entirely={selected_folders.some(
|
||||
(x) => x[0] === name && x.length === 1
|
||||
)}
|
||||
{interactive}
|
||||
{ls_fn}
|
||||
{file_count}
|
||||
valid_for_selection={valid}
|
||||
on:check
|
||||
/>
|
||||
{/if}
|
||||
</li>
|
||||
|
5
js/fileexplorer/shared/types.ts
Normal file
5
js/fileexplorer/shared/types.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface FileNode {
|
||||
type: "file" | "folder";
|
||||
name: string;
|
||||
valid?: boolean;
|
||||
}
|
@ -1,187 +0,0 @@
|
||||
import { describe, test, expect, beforeAll } from "vitest";
|
||||
import { make_fs_store, type SerialisedNode } from "./utils";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
describe("fs_store", () => {
|
||||
let store: ReturnType<typeof make_fs_store>,
|
||||
n0: SerialisedNode,
|
||||
n1: SerialisedNode,
|
||||
n2: SerialisedNode,
|
||||
n3: SerialisedNode,
|
||||
n4: SerialisedNode,
|
||||
n5: SerialisedNode;
|
||||
beforeAll(() => {
|
||||
n4 = {
|
||||
type: "file",
|
||||
path: "d.txt",
|
||||
parent: null,
|
||||
children: []
|
||||
};
|
||||
n5 = {
|
||||
type: "file",
|
||||
path: "e.txt",
|
||||
parent: null,
|
||||
children: []
|
||||
};
|
||||
|
||||
n3 = {
|
||||
type: "folder",
|
||||
path: "c",
|
||||
parent: null,
|
||||
children: [n4, n5]
|
||||
};
|
||||
|
||||
n2 = {
|
||||
type: "folder",
|
||||
path: "b",
|
||||
parent: null,
|
||||
children: [n3]
|
||||
};
|
||||
|
||||
n1 = {
|
||||
type: "folder",
|
||||
path: "a",
|
||||
parent: null,
|
||||
children: [n2]
|
||||
};
|
||||
|
||||
n0 = {
|
||||
type: "file",
|
||||
path: "my-file.txt",
|
||||
parent: null,
|
||||
children: []
|
||||
};
|
||||
|
||||
store = make_fs_store();
|
||||
});
|
||||
|
||||
test("initialise store with correct references", () => {
|
||||
store.create_fs_graph([n1, n0]);
|
||||
|
||||
const items = get(store);
|
||||
|
||||
expect(items?.[0].last).toEqual(items?.[1]);
|
||||
expect(items?.[1].last).toEqual(items?.[1]);
|
||||
expect(items?.[1].previous).toEqual(items?.[0]);
|
||||
expect(items?.[0].previous).toEqual(null);
|
||||
expect(items?.[0].parent).toEqual(null);
|
||||
expect(items?.[0].children?.[0].parent).toEqual(items?.[0]);
|
||||
});
|
||||
|
||||
test("set_checked_from_paths", () => {
|
||||
const checked_paths = [
|
||||
["a", "b", "c", "d.txt"],
|
||||
["a", "b", "c", "e.txt"]
|
||||
];
|
||||
const new_checked_paths = store.set_checked_from_paths(checked_paths);
|
||||
|
||||
const items = get(store);
|
||||
|
||||
expect(new_checked_paths).toEqual(checked_paths);
|
||||
expect(items?.[0].checked).toEqual(true);
|
||||
});
|
||||
|
||||
test("set_checked_from_paths should be deterministic", () => {
|
||||
const checked_paths = [
|
||||
["a", "b", "c", "d.txt"],
|
||||
["a", "b", "c", "e.txt"]
|
||||
];
|
||||
const new_checked_paths = store.set_checked_from_paths(checked_paths);
|
||||
|
||||
const items = get(store);
|
||||
|
||||
expect(new_checked_paths).toEqual(checked_paths);
|
||||
expect(items?.[0].checked).toEqual(true);
|
||||
});
|
||||
|
||||
test("set_checked should check the appropriate index", () => {
|
||||
const checked_indices = [0, 0, 0, 0];
|
||||
store.set_checked(checked_indices, false, [], "multiple");
|
||||
|
||||
const items = get(store);
|
||||
|
||||
expect(
|
||||
items?.[0].children?.[0].children?.[0].children?.[0].checked
|
||||
).toEqual(false);
|
||||
});
|
||||
|
||||
test("if all children are set to false then all parents should also be false", () => {
|
||||
const checked_indices = [0, 0, 0, 1];
|
||||
store.set_checked(checked_indices, false, [], "multiple");
|
||||
|
||||
const items = get(store);
|
||||
|
||||
expect(
|
||||
items?.[0].children?.[0].children?.[0].children?.[0].checked
|
||||
).toEqual(false);
|
||||
expect(
|
||||
items?.[0].children?.[0].children?.[0].children?.[1].checked
|
||||
).toEqual(false);
|
||||
expect(items?.[0].children?.[0].children?.[0].checked).toEqual(false);
|
||||
expect(items?.[0].children?.[0].checked).toEqual(false);
|
||||
expect(items?.[0].checked).toEqual(false);
|
||||
});
|
||||
|
||||
test("if only one child is set to true then parent should be false", () => {
|
||||
const checked_indices = [0, 0, 0, 1];
|
||||
store.set_checked(checked_indices, true, [], "multiple");
|
||||
|
||||
const items = get(store);
|
||||
|
||||
expect(
|
||||
items?.[0].children?.[0].children?.[0].children?.[0].checked
|
||||
).toEqual(false);
|
||||
expect(
|
||||
items?.[0].children?.[0].children?.[0].children?.[1].checked
|
||||
).toEqual(true);
|
||||
expect(items?.[0].children?.[0].children?.[0].checked).toEqual(false);
|
||||
expect(items?.[0].children?.[0].checked).toEqual(false);
|
||||
expect(items?.[0].checked).toEqual(false);
|
||||
});
|
||||
|
||||
test("if all children are set to true then parents should be true", () => {
|
||||
const checked_indices = [0, 0, 0, 0];
|
||||
store.set_checked(checked_indices, true, [], "multiple");
|
||||
|
||||
const items = get(store);
|
||||
|
||||
expect(
|
||||
items?.[0].children?.[0].children?.[0].children?.[0].checked
|
||||
).toEqual(true);
|
||||
expect(
|
||||
items?.[0].children?.[0].children?.[0].children?.[1].checked
|
||||
).toEqual(true);
|
||||
expect(items?.[0].children?.[0].children?.[0].checked).toEqual(true);
|
||||
expect(items?.[0].children?.[0].checked).toEqual(true);
|
||||
expect(items?.[0].checked).toEqual(true);
|
||||
});
|
||||
|
||||
test("calling set_checked multiple times should not impact other nodes", () => {
|
||||
store.set_checked([1], true, [], "multiple");
|
||||
expect(get(store)?.[1].checked).toEqual(true);
|
||||
|
||||
store.set_checked([0], true, [], "multiple");
|
||||
expect(get(store)?.[1].checked).toEqual(true);
|
||||
|
||||
store.set_checked([0], false, [], "multiple");
|
||||
expect(get(store)?.[1].checked).toEqual(true);
|
||||
|
||||
store.set_checked([0], true, [], "multiple");
|
||||
expect(get(store)?.[1].checked).toEqual(true);
|
||||
|
||||
store.set_checked([0], false, [], "multiple");
|
||||
expect(get(store)?.[1].checked).toEqual(true);
|
||||
|
||||
const items = get(store);
|
||||
|
||||
// expect(
|
||||
// items?.[0].children?.[0].children?.[0].children?.[0].checked
|
||||
// ).toEqual(true);
|
||||
// expect(
|
||||
// items?.[0].children?.[0].children?.[0].children?.[1].checked
|
||||
// ).toEqual(true);
|
||||
// expect(items?.[0].children?.[0].children?.[0].checked).toEqual(true);
|
||||
// expect(items?.[0].children?.[0].checked).toEqual(true);
|
||||
// expect(items?.[0].checked).toEqual(true);
|
||||
});
|
||||
});
|
@ -1,273 +0,0 @@
|
||||
import { writable, type Readable } from "svelte/store";
|
||||
import { dequal } from "dequal";
|
||||
|
||||
export interface Node {
|
||||
type: "file" | "folder";
|
||||
path: string;
|
||||
children?: Node[];
|
||||
checked: boolean;
|
||||
children_visible: boolean;
|
||||
last?: Node | null;
|
||||
parent: Node | null;
|
||||
previous?: Node | null;
|
||||
}
|
||||
|
||||
export type SerialisedNode = Omit<
|
||||
Node,
|
||||
"checked" | "children_visible" | "children"
|
||||
> & { children?: SerialisedNode[] };
|
||||
|
||||
interface FSStore {
|
||||
subscribe: Readable<Node[] | null>["subscribe"];
|
||||
create_fs_graph: (serialised_node: SerialisedNode[]) => void;
|
||||
|
||||
set_checked: (
|
||||
indices: number[],
|
||||
checked: boolean,
|
||||
checked_paths: string[][],
|
||||
file_count: "single" | "multiple"
|
||||
) => string[][];
|
||||
set_checked_from_paths: (checked_paths: string[][]) => string[][];
|
||||
}
|
||||
|
||||
export const make_fs_store = (): FSStore => {
|
||||
const { subscribe, set } = writable<Node[] | null>(null);
|
||||
let root: Node = {
|
||||
type: "folder",
|
||||
path: "",
|
||||
checked: false,
|
||||
children_visible: false,
|
||||
parent: null
|
||||
};
|
||||
let tree_updated = false;
|
||||
|
||||
function create_fs_graph(serialised_node: SerialisedNode[]): void {
|
||||
root.children = process_tree(serialised_node);
|
||||
tree_updated = true;
|
||||
set(root.children);
|
||||
}
|
||||
|
||||
let old_checked_paths: string[][] = [];
|
||||
|
||||
function set_checked_from_paths(checked_paths: string[][]): string[][] {
|
||||
if (dequal(checked_paths, old_checked_paths) && !tree_updated) {
|
||||
return checked_paths;
|
||||
}
|
||||
old_checked_paths = checked_paths;
|
||||
check_node_and_children(root.children, false, []);
|
||||
const new_checked_paths: string[][] = [];
|
||||
const seen_nodes = new Set();
|
||||
for (let i = 0; i < checked_paths.length; i++) {
|
||||
let _node = root;
|
||||
let _path = [];
|
||||
for (let j = 0; j < checked_paths[i].length; j++) {
|
||||
if (!_node?.children) {
|
||||
continue;
|
||||
}
|
||||
_path.push(checked_paths[i][j]);
|
||||
_node = _node.children!.find((v) => v.path === checked_paths[i][j])!;
|
||||
}
|
||||
|
||||
if (!_node) {
|
||||
continue;
|
||||
}
|
||||
|
||||
_node.checked = true;
|
||||
ensure_visible(_node);
|
||||
const nodes = check_node_and_children(_node.children, true, [_node]);
|
||||
check_parent(_node);
|
||||
nodes.forEach((node) => {
|
||||
const path = get_full_path(node);
|
||||
const normalized_path = path.join("/");
|
||||
// let normalized_path = path.join("/");
|
||||
// normalized_path = normalized_path.endsWith("/") ? normalized_path.slice(0, -1) : normalized_path;
|
||||
if (node.type === "file") {
|
||||
if (seen_nodes.has(normalized_path)) {
|
||||
return;
|
||||
}
|
||||
new_checked_paths.push(path);
|
||||
seen_nodes.add(normalized_path);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
set(root.children!);
|
||||
|
||||
tree_updated = false;
|
||||
|
||||
return new_checked_paths;
|
||||
}
|
||||
|
||||
function set_checked(
|
||||
indices: number[],
|
||||
checked: boolean,
|
||||
checked_paths: string[][],
|
||||
file_count: "single" | "multiple"
|
||||
): string[][] {
|
||||
let _node = root;
|
||||
|
||||
if (file_count === "single") {
|
||||
check_node_and_children(root.children, false, []);
|
||||
set(root.children!);
|
||||
}
|
||||
|
||||
for (let i = 0; i < indices.length; i++) {
|
||||
_node = _node.children![indices[i]];
|
||||
}
|
||||
|
||||
_node.checked = checked;
|
||||
const nodes = check_node_and_children(_node.children, checked, [_node]);
|
||||
|
||||
let new_checked_paths = new Map(checked_paths.map((v) => [v.join("/"), v]));
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const _path = get_full_path(nodes[i]);
|
||||
if (!nodes[i].checked) {
|
||||
new_checked_paths.delete(_path.join("/"));
|
||||
} else if (nodes[i].checked) {
|
||||
if (file_count === "single") {
|
||||
new_checked_paths = new Map();
|
||||
}
|
||||
|
||||
new_checked_paths.set(_path.join("/"), _path);
|
||||
}
|
||||
}
|
||||
|
||||
check_parent(_node);
|
||||
|
||||
set(root.children!);
|
||||
old_checked_paths = Array.from(new_checked_paths).map((v) => v[1]);
|
||||
return old_checked_paths;
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
create_fs_graph,
|
||||
set_checked,
|
||||
set_checked_from_paths
|
||||
};
|
||||
};
|
||||
|
||||
function ensure_visible(node: Node): void {
|
||||
if (node.parent) {
|
||||
node.parent.children_visible = true;
|
||||
ensure_visible(node.parent);
|
||||
}
|
||||
}
|
||||
|
||||
function process_tree(
|
||||
node: SerialisedNode[],
|
||||
depth = 0,
|
||||
path_segments: string[] = [],
|
||||
parent: Node | null = null
|
||||
): Node[] {
|
||||
const folders: Node[] = [];
|
||||
const files: Node[] = [];
|
||||
|
||||
for (let i = 0; i < node.length; i++) {
|
||||
let n: (typeof node)[number] = node[i];
|
||||
|
||||
if (n.type === "file") {
|
||||
let index = files.findIndex(
|
||||
(v) => v.path.toLocaleLowerCase() >= n.path.toLocaleLowerCase()
|
||||
);
|
||||
|
||||
const _node: Node = {
|
||||
children: undefined,
|
||||
type: "file",
|
||||
path: n.path,
|
||||
checked: false,
|
||||
children_visible: false,
|
||||
parent: parent
|
||||
};
|
||||
|
||||
files.splice(index === -1 ? files.length : index, 0, _node);
|
||||
} else {
|
||||
let index = folders.findIndex(
|
||||
(v) => v.path.toLocaleLowerCase() >= n.path.toLocaleLowerCase()
|
||||
);
|
||||
|
||||
const _node: Node = {
|
||||
type: "folder",
|
||||
path: n.path,
|
||||
checked: false,
|
||||
children_visible: false,
|
||||
parent: parent
|
||||
};
|
||||
|
||||
const children = process_tree(
|
||||
n.children!,
|
||||
depth + 1,
|
||||
[...path_segments, n.path],
|
||||
_node
|
||||
);
|
||||
|
||||
_node.children = children;
|
||||
|
||||
folders.splice(index === -1 ? folders.length : index, 0, _node);
|
||||
}
|
||||
}
|
||||
|
||||
const last = files[files.length - 1] || folders[folders.length - 1];
|
||||
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
folders[i].last = last;
|
||||
folders[i].previous = folders[i - 1] || null;
|
||||
}
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
if (i === 0) {
|
||||
files[i].previous = folders[folders.length - 1] || null;
|
||||
} else {
|
||||
files[i].previous = files[i - 1] || null;
|
||||
}
|
||||
files[i].last = last;
|
||||
}
|
||||
|
||||
return Array().concat(folders, files);
|
||||
}
|
||||
|
||||
function get_full_path(node: Node, path: string[] = []): string[] {
|
||||
const new_path = [node.path, ...path];
|
||||
|
||||
if (node.parent) {
|
||||
return get_full_path(node.parent, new_path);
|
||||
}
|
||||
return new_path;
|
||||
}
|
||||
|
||||
function check_node_and_children(
|
||||
node: Node[] | null | undefined,
|
||||
checked: boolean,
|
||||
checked_nodes: Node[]
|
||||
): Node[] {
|
||||
if (node === null || node === undefined) return checked_nodes;
|
||||
for (let i = 0; i < node.length; i++) {
|
||||
node[i].checked = checked;
|
||||
checked_nodes.push(node[i]);
|
||||
if (checked) ensure_visible(node[i]);
|
||||
|
||||
checked_nodes.concat(
|
||||
check_node_and_children(node[i].children, checked, checked_nodes)
|
||||
);
|
||||
}
|
||||
|
||||
return checked_nodes;
|
||||
}
|
||||
|
||||
function check_parent(node: Node | null | undefined): void {
|
||||
if (node === null || node === undefined || !node.parent) return;
|
||||
let _node = node.last;
|
||||
let nodes_checked = [];
|
||||
while (_node) {
|
||||
nodes_checked.push(_node.checked);
|
||||
_node = _node.previous;
|
||||
}
|
||||
if (nodes_checked.every((v) => v === true)) {
|
||||
node.parent!.checked = true;
|
||||
check_parent(node?.parent);
|
||||
} else if (nodes_checked.some((v) => v === false)) {
|
||||
node.parent!.checked = false;
|
||||
check_parent(node?.parent);
|
||||
}
|
||||
}
|
@ -23,6 +23,7 @@ def copy_all_demos(source_dir: str, dest_dir: str):
|
||||
"code",
|
||||
"fake_gan",
|
||||
"fake_diffusion_with_gif",
|
||||
"file_explorer_component_events",
|
||||
"image_mod_default_image",
|
||||
"image_segmentation",
|
||||
"interface_random_slider",
|
||||
|
@ -2874,7 +2874,7 @@ class TestFileExplorer:
|
||||
file_explorer = gr.FileExplorer(file_count="single")
|
||||
|
||||
config = file_explorer.get_config()
|
||||
assert config["glob"] == "**/*.*"
|
||||
assert config["glob"] == "**/*"
|
||||
assert config["value"] is None
|
||||
assert config["file_count"] == "single"
|
||||
assert config["server_fns"] == ["ls"]
|
||||
@ -2891,7 +2891,7 @@ class TestFileExplorer:
|
||||
file_explorer = gr.FileExplorer(file_count="multiple")
|
||||
|
||||
config = file_explorer.get_config()
|
||||
assert config["glob"] == "**/*.*"
|
||||
assert config["glob"] == "**/*"
|
||||
assert config["value"] is None
|
||||
assert config["file_count"] == "multiple"
|
||||
assert config["server_fns"] == ["ls"]
|
||||
@ -2905,66 +2905,24 @@ class TestFileExplorer:
|
||||
preprocessed_data = file_explorer.preprocess(input_data)
|
||||
assert preprocessed_data == []
|
||||
|
||||
def test_file_explorer_dir_only_glob(self, tmpdir):
|
||||
def test_file_explorer_txt_only_glob(self, tmpdir):
|
||||
tmpdir.mkdir("foo")
|
||||
tmpdir.mkdir("bar")
|
||||
tmpdir.mkdir("baz")
|
||||
(Path(tmpdir) / "baz" / "qux").mkdir()
|
||||
(Path(tmpdir) / "foo" / "abc").mkdir()
|
||||
(Path(tmpdir) / "foo" / "abc" / "def").mkdir()
|
||||
(Path(tmpdir) / "foo" / "abc" / "def" / "file.txt").touch()
|
||||
(Path(tmpdir) / "foo" / "bar").mkdir()
|
||||
(Path(tmpdir) / "foo" / "file.txt").touch()
|
||||
(Path(tmpdir) / "foo" / "file2.txt").touch()
|
||||
(Path(tmpdir) / "foo" / "file3.log").touch()
|
||||
(Path(tmpdir) / "foo" / "img.png").touch()
|
||||
(Path(tmpdir) / "foo" / "bar" / "bar.txt").touch()
|
||||
|
||||
file_explorer = gr.FileExplorer(glob="**/", root=Path(tmpdir))
|
||||
tree = file_explorer.ls()
|
||||
|
||||
def sort_answer(answer):
|
||||
answer = sorted(answer, key=lambda x: x["path"])
|
||||
for item in answer:
|
||||
if item["children"]:
|
||||
item["children"] = sort_answer(item["children"])
|
||||
return answer
|
||||
file_explorer = gr.FileExplorer(glob="*.txt", root=Path(tmpdir))
|
||||
tree = file_explorer.ls(["foo"])
|
||||
|
||||
answer = [
|
||||
{
|
||||
"path": "bar",
|
||||
"type": "folder",
|
||||
"children": [{"path": "", "type": "file", "children": None}],
|
||||
},
|
||||
{
|
||||
"path": "baz",
|
||||
"type": "folder",
|
||||
"children": [
|
||||
{"path": "", "type": "file", "children": None},
|
||||
{
|
||||
"path": "qux",
|
||||
"type": "folder",
|
||||
"children": [{"path": "", "type": "file", "children": None}],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"path": "foo",
|
||||
"type": "folder",
|
||||
"children": [
|
||||
{"path": "", "type": "file", "children": None},
|
||||
{
|
||||
"path": "abc",
|
||||
"type": "folder",
|
||||
"children": [
|
||||
{"path": "", "type": "file", "children": None},
|
||||
{
|
||||
"path": "def",
|
||||
"type": "folder",
|
||||
"children": [
|
||||
{"path": "", "type": "file", "children": None}
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{"name": "bar", "type": "folder", "valid": False},
|
||||
{"name": "file.txt", "type": "file", "valid": True},
|
||||
{"name": "file2.txt", "type": "file", "valid": True},
|
||||
]
|
||||
assert sort_answer(tree) == sort_answer(answer)
|
||||
assert tree == answer
|
||||
|
||||
|
||||
def test_component_class_ids():
|
||||
|
@ -7,7 +7,6 @@ import warnings
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from typing_extensions import Literal
|
||||
|
||||
@ -20,6 +19,7 @@ from gradio.utils import (
|
||||
check_function_inputs_match,
|
||||
colab_check,
|
||||
delete_none,
|
||||
download_if_url,
|
||||
get_continuous_fn,
|
||||
get_extension_from_file_path_or_url,
|
||||
get_type_hints,
|
||||
@ -27,7 +27,6 @@ from gradio.utils import (
|
||||
is_in_or_equal,
|
||||
is_special_typed_parameter,
|
||||
kaggle_check,
|
||||
download_if_url,
|
||||
sagemaker_check,
|
||||
sanitize_list_for_csv,
|
||||
sanitize_value_for_csv,
|
||||
|
Loading…
Reference in New Issue
Block a user