diff --git a/.changeset/full-flies-join.md b/.changeset/full-flies-join.md new file mode 100644 index 0000000000000..78163117b8a5d --- /dev/null +++ b/.changeset/full-flies-join.md @@ -0,0 +1,8 @@ +--- +"@gradio/file": minor +"@gradio/tootils": minor +"@gradio/upload": minor +"gradio": minor +--- + +feat:add delete event to `File` component diff --git a/demo/file_component_events/run.ipynb b/demo/file_component_events/run.ipynb index 90a46080e1753..6aa1a62cebbc3 100644 --- a/demo/file_component_events/run.ipynb +++ b/demo/file_component_events/run.ipynb @@ -1 +1 @@ -{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: file_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": ["import gradio as gr\n", "\n", "with gr.Blocks() as demo:\n", " \n", " with gr.Row():\n", " with gr.Column():\n", " file_component = gr.File(label=\"Upload Single File\", file_count=\"single\")\n", " with gr.Column():\n", " output_file_1 = gr.File(label=\"Upload Single File Output\", file_count=\"single\")\n", " num_load_btn_1 = gr.Number(label=\"# Load Upload Single File\", value=0)\n", " file_component.upload(lambda s,n: (s, n + 1), [file_component, num_load_btn_1], [output_file_1, num_load_btn_1])\n", " with gr.Row():\n", " with gr.Column():\n", " file_component_multiple = gr.File(label=\"Upload Multiple Files\", file_count=\"multiple\")\n", " with gr.Column():\n", " output_file_2 = gr.File(label=\"Upload Multiple Files Output\", file_count=\"multiple\")\n", " num_load_btn_2 = gr.Number(label=\"# Load Upload Multiple Files\", value=0)\n", " file_component_multiple.upload(lambda s,n: (s, n + 1), [file_component_multiple, num_load_btn_2], [output_file_2, num_load_btn_2])\n", " with gr.Row():\n", " with gr.Column():\n", " file_component_specific = gr.File(label=\"Upload Multiple Files Image/Video\", file_count=\"multiple\", file_types=[\"image\", \"video\"])\n", " with gr.Column():\n", " output_file_3 = gr.File(label=\"Upload Multiple Files Output Image/Video\", file_count=\"multiple\")\n", " num_load_btn_3 = gr.Number(label=\"# Load Upload Multiple Files Image/Video\", value=0)\n", " file_component_specific.upload(lambda s,n: (s, n + 1), [file_component_specific, num_load_btn_3], [output_file_3, num_load_btn_3])\n", " with gr.Row():\n", " with gr.Column():\n", " file_component_pdf = gr.File(label=\"Upload PDF File\", file_types=[\"pdf\"])\n", " with gr.Column():\n", " output_file_4 = gr.File(label=\"Upload PDF File Output\")\n", " num_load_btn_4 = gr.Number(label=\"# Load Upload PDF File\", value=0)\n", " file_component_pdf.upload(lambda s,n: (s, n + 1), [file_component_pdf, num_load_btn_4], [output_file_4, num_load_btn_4])\n", " with gr.Row():\n", " with gr.Column():\n", " file_component_invalid = gr.File(label=\"Upload File with Invalid file_types\", file_types=[\"invalid file_type\"])\n", " with gr.Column():\n", " output_file_5 = gr.File(label=\"Upload File with Invalid file_types Output\")\n", " num_load_btn_5 = gr.Number(label=\"# Load Upload File with Invalid file_types\", value=0)\n", " file_component_invalid.upload(lambda s,n: (s, n + 1), [file_component_invalid, num_load_btn_5], [output_file_5, num_load_btn_5])\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_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": ["import gradio as gr\n", "\n", "\n", "def delete_file(n: int, file: gr.DeletedFileData):\n", " return [file.file.path, n + 1]\n", "\n", "\n", "with gr.Blocks() as demo:\n", "\n", " with gr.Row():\n", " with gr.Column():\n", " file_component = gr.File(label=\"Upload Single File\", file_count=\"single\")\n", " with gr.Column():\n", " output_file_1 = gr.File(\n", " label=\"Upload Single File Output\", file_count=\"single\"\n", " )\n", " num_load_btn_1 = gr.Number(label=\"# Load Upload Single File\", value=0)\n", " file_component.upload(\n", " lambda s, n: (s, n + 1),\n", " [file_component, num_load_btn_1],\n", " [output_file_1, num_load_btn_1],\n", " )\n", " with gr.Row():\n", " with gr.Column():\n", " file_component_multiple = gr.File(\n", " label=\"Upload Multiple Files\", file_count=\"multiple\"\n", " )\n", " with gr.Column():\n", " output_file_2 = gr.File(\n", " label=\"Upload Multiple Files Output\", file_count=\"multiple\"\n", " )\n", " num_load_btn_2 = gr.Number(label=\"# Load Upload Multiple Files\", value=0)\n", " file_component_multiple.upload(\n", " lambda s, n: (s, n + 1),\n", " [file_component_multiple, num_load_btn_2],\n", " [output_file_2, num_load_btn_2],\n", " )\n", " with gr.Row():\n", " with gr.Column():\n", " file_component_specific = gr.File(\n", " label=\"Upload Multiple Files Image/Video\",\n", " file_count=\"multiple\",\n", " file_types=[\"image\", \"video\"],\n", " )\n", " with gr.Column():\n", " output_file_3 = gr.File(\n", " label=\"Upload Multiple Files Output Image/Video\", file_count=\"multiple\"\n", " )\n", " num_load_btn_3 = gr.Number(\n", " label=\"# Load Upload Multiple Files Image/Video\", value=0\n", " )\n", " file_component_specific.upload(\n", " lambda s, n: (s, n + 1),\n", " [file_component_specific, num_load_btn_3],\n", " [output_file_3, num_load_btn_3],\n", " )\n", " with gr.Row():\n", " with gr.Column():\n", " file_component_pdf = gr.File(label=\"Upload PDF File\", file_types=[\"pdf\"])\n", " with gr.Column():\n", " output_file_4 = gr.File(label=\"Upload PDF File Output\")\n", " num_load_btn_4 = gr.Number(label=\"# Load Upload PDF File\", value=0)\n", " file_component_pdf.upload(\n", " lambda s, n: (s, n + 1),\n", " [file_component_pdf, num_load_btn_4],\n", " [output_file_4, num_load_btn_4],\n", " )\n", " with gr.Row():\n", " with gr.Column():\n", " file_component_invalid = gr.File(\n", " label=\"Upload File with Invalid file_types\",\n", " file_types=[\"invalid file_type\"],\n", " )\n", " with gr.Column():\n", " output_file_5 = gr.File(label=\"Upload File with Invalid file_types Output\")\n", " num_load_btn_5 = gr.Number(\n", " label=\"# Load Upload File with Invalid file_types\", value=0\n", " )\n", " file_component_invalid.upload(\n", " lambda s, n: (s, n + 1),\n", " [file_component_invalid, num_load_btn_5],\n", " [output_file_5, num_load_btn_5],\n", " )\n", " with gr.Row():\n", " with gr.Column():\n", " del_file_input = gr.File(label=\"Delete File\", file_count=\"multiple\")\n", " with gr.Column():\n", " del_file_data = gr.Textbox(label=\"Delete file data\")\n", " num_load_btn_6 = gr.Number(label=\"# Deleted File\", value=0)\n", " del_file_input.delete(\n", " delete_file,\n", " [num_load_btn_6],\n", " [del_file_data, num_load_btn_6],\n", " )\n", " # f = gr.File(label=\"Upload many File\", file_count=\"multiple\")\n", " # # f.delete(delete_file)\n", " # f.delete(delete_file, inputs=None, outputs=None)\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_component_events/run.py b/demo/file_component_events/run.py index dc0096102601b..3965eabe14f32 100644 --- a/demo/file_component_events/run.py +++ b/demo/file_component_events/run.py @@ -1,42 +1,100 @@ import gradio as gr + +def delete_file(n: int, file: gr.DeletedFileData): + return [file.file.path, n + 1] + + with gr.Blocks() as demo: - + with gr.Row(): with gr.Column(): file_component = gr.File(label="Upload Single File", file_count="single") with gr.Column(): - output_file_1 = gr.File(label="Upload Single File Output", file_count="single") + output_file_1 = gr.File( + label="Upload Single File Output", file_count="single" + ) num_load_btn_1 = gr.Number(label="# Load Upload Single File", value=0) - file_component.upload(lambda s,n: (s, n + 1), [file_component, num_load_btn_1], [output_file_1, num_load_btn_1]) + file_component.upload( + lambda s, n: (s, n + 1), + [file_component, num_load_btn_1], + [output_file_1, num_load_btn_1], + ) with gr.Row(): with gr.Column(): - file_component_multiple = gr.File(label="Upload Multiple Files", file_count="multiple") + file_component_multiple = gr.File( + label="Upload Multiple Files", file_count="multiple" + ) with gr.Column(): - output_file_2 = gr.File(label="Upload Multiple Files Output", file_count="multiple") + output_file_2 = gr.File( + label="Upload Multiple Files Output", file_count="multiple" + ) num_load_btn_2 = gr.Number(label="# Load Upload Multiple Files", value=0) - file_component_multiple.upload(lambda s,n: (s, n + 1), [file_component_multiple, num_load_btn_2], [output_file_2, num_load_btn_2]) + file_component_multiple.upload( + lambda s, n: (s, n + 1), + [file_component_multiple, num_load_btn_2], + [output_file_2, num_load_btn_2], + ) with gr.Row(): with gr.Column(): - file_component_specific = gr.File(label="Upload Multiple Files Image/Video", file_count="multiple", file_types=["image", "video"]) + file_component_specific = gr.File( + label="Upload Multiple Files Image/Video", + file_count="multiple", + file_types=["image", "video"], + ) with gr.Column(): - output_file_3 = gr.File(label="Upload Multiple Files Output Image/Video", file_count="multiple") - num_load_btn_3 = gr.Number(label="# Load Upload Multiple Files Image/Video", value=0) - file_component_specific.upload(lambda s,n: (s, n + 1), [file_component_specific, num_load_btn_3], [output_file_3, num_load_btn_3]) + output_file_3 = gr.File( + label="Upload Multiple Files Output Image/Video", file_count="multiple" + ) + num_load_btn_3 = gr.Number( + label="# Load Upload Multiple Files Image/Video", value=0 + ) + file_component_specific.upload( + lambda s, n: (s, n + 1), + [file_component_specific, num_load_btn_3], + [output_file_3, num_load_btn_3], + ) with gr.Row(): with gr.Column(): file_component_pdf = gr.File(label="Upload PDF File", file_types=["pdf"]) with gr.Column(): output_file_4 = gr.File(label="Upload PDF File Output") num_load_btn_4 = gr.Number(label="# Load Upload PDF File", value=0) - file_component_pdf.upload(lambda s,n: (s, n + 1), [file_component_pdf, num_load_btn_4], [output_file_4, num_load_btn_4]) + file_component_pdf.upload( + lambda s, n: (s, n + 1), + [file_component_pdf, num_load_btn_4], + [output_file_4, num_load_btn_4], + ) with gr.Row(): with gr.Column(): - file_component_invalid = gr.File(label="Upload File with Invalid file_types", file_types=["invalid file_type"]) + file_component_invalid = gr.File( + label="Upload File with Invalid file_types", + file_types=["invalid file_type"], + ) with gr.Column(): output_file_5 = gr.File(label="Upload File with Invalid file_types Output") - num_load_btn_5 = gr.Number(label="# Load Upload File with Invalid file_types", value=0) - file_component_invalid.upload(lambda s,n: (s, n + 1), [file_component_invalid, num_load_btn_5], [output_file_5, num_load_btn_5]) + num_load_btn_5 = gr.Number( + label="# Load Upload File with Invalid file_types", value=0 + ) + file_component_invalid.upload( + lambda s, n: (s, n + 1), + [file_component_invalid, num_load_btn_5], + [output_file_5, num_load_btn_5], + ) + with gr.Row(): + with gr.Column(): + del_file_input = gr.File(label="Delete File", file_count="multiple") + with gr.Column(): + del_file_data = gr.Textbox(label="Delete file data") + num_load_btn_6 = gr.Number(label="# Deleted File", value=0) + del_file_input.delete( + delete_file, + [num_load_btn_6], + [del_file_data, num_load_btn_6], + ) + # f = gr.File(label="Upload many File", file_count="multiple") + # # f.delete(delete_file) + # f.delete(delete_file, inputs=None, outputs=None) if __name__ == "__main__": demo.launch() diff --git a/gradio/__init__.py b/gradio/__init__.py index 5143223a89d0f..1352c33112f52 100644 --- a/gradio/__init__.py +++ b/gradio/__init__.py @@ -60,7 +60,14 @@ from gradio.components.audio import WaveformOptions from gradio.components.image_editor import Brush, Eraser from gradio.data_classes import FileData -from gradio.events import EventData, KeyUpData, LikeData, SelectData, on +from gradio.events import ( + DeletedFileData, + EventData, + KeyUpData, + LikeData, + SelectData, + on, +) from gradio.exceptions import Error from gradio.external import load from gradio.flagging import ( diff --git a/gradio/components/file.py b/gradio/components/file.py index 86cb20e0ce531..3d88ff1c6ffd0 100644 --- a/gradio/components/file.py +++ b/gradio/components/file.py @@ -26,7 +26,7 @@ class File(Component): Demo: zip_files, zip_to_json """ - EVENTS = [Events.change, Events.select, Events.clear, Events.upload] + EVENTS = [Events.change, Events.select, Events.clear, Events.upload, Events.delete] def __init__( self, diff --git a/gradio/data_classes.py b/gradio/data_classes.py index 398dd5f2314e6..f43e15073068d 100644 --- a/gradio/data_classes.py +++ b/gradio/data_classes.py @@ -8,7 +8,7 @@ import shutil from abc import ABC, abstractmethod from enum import Enum, auto -from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Union +from typing import TYPE_CHECKING, Any, List, Optional, Tuple, TypedDict, Union from fastapi import Request from gradio_client.utils import traverse @@ -189,6 +189,16 @@ def from_json(cls, x) -> GradioRootModel: GradioDataModel = Union[GradioModel, GradioRootModel] +class FileDataDict(TypedDict): + path: str # server filepath + url: Optional[str] # normalised server url + size: Optional[int] # size in bytes + orig_name: Optional[str] # original filename + mime_type: Optional[str] + is_stream: bool + meta: dict + + class FileData(GradioModel): path: str # server filepath url: Optional[str] = None # normalised server url diff --git a/gradio/events.py b/gradio/events.py index 5ec9e444ea78c..63f8f9341bf5c 100644 --- a/gradio/events.py +++ b/gradio/events.py @@ -20,6 +20,8 @@ from gradio_client.documentation import document from jinja2 import Template +from gradio.data_classes import FileData, FileDataDict + if TYPE_CHECKING: from gradio.blocks import Block, Component @@ -149,6 +151,15 @@ def __init__(self, target: Block | None, data: Any): """ +class DeletedFileData(EventData): + def __init__(self, target: Block | None, data: FileDataDict): + super().__init__(target, data) + self.file: FileData = FileData(**data) + """ + The file that was deleted. + """ + + @dataclasses.dataclass class EventListenerMethod: block: Block | None @@ -585,6 +596,10 @@ class Events: "apply", doc="This listener is triggered when the user applies changes to the {{ component }} through an integrated UI action.", ) + delete = EventListener( + "delete", + doc="This listener is triggered when the user deletes and item from the {{ component }}. Uses event data gradio.DeletedFileData to carry `value` referring to the file that was deleted as an instance of FileData. See EventData documentation on how to use this event data", + ) class LikeData(EventData): diff --git a/js/app/test/file_component_events.spec.ts b/js/app/test/file_component_events.spec.ts index 039049c6fb9a3..0fccf1422d201 100644 --- a/js/app/test/file_component_events.spec.ts +++ b/js/app/test/file_component_events.spec.ts @@ -1,5 +1,13 @@ import { test, expect, drag_and_drop_file } from "@gradio/tootils"; +async function error_modal_showed(page) { + const toast = page.getByTestId("toast-body"); + expect(toast).toContainText("error"); + const close = page.getByTestId("toast-close"); + await close.click(); + await expect(page.getByTestId("toast-body")).toHaveCount(0); +} + test("File component properly dispatches load event for the single file case.", async ({ page }) => { @@ -63,11 +71,33 @@ test("File component properly handles drag and drop of pdf file.", async ({ test("File component properly handles invalid file_types.", async ({ page }) => { - const uploader = await page.locator("input[type=file]").last(); - await uploader.setInputFiles(["./test/files/cheetah1.jpg"]); + const locator = page.locator("input[type=file]").nth(4); + await drag_and_drop_file( + page, + locator, + "./test/files/cheetah1.jpg", + "cheetah1.jpg", + "image/jpeg" + ); - // Check that the pdf file was uploaded - await expect( - page.getByLabel("# Load Upload File with Invalid file_types") - ).toHaveValue("1"); + await error_modal_showed(page); +}); + +test("Delete event is fired correctly", async ({ page }) => { + const locator = page.locator("input[type=file]").nth(5); + await drag_and_drop_file( + page, + locator, + "./test/files/cheetah1.jpg", + "cheetah1.jpg", + "image/jpeg", + 2 + ); + + await page.getByLabel("Remove this file").first().click(); + + await expect(page.getByLabel("# Deleted File")).toHaveValue("1"); + expect( + (await page.getByLabel("Delete file data").inputValue()).length + ).toBeGreaterThan(5); }); diff --git a/js/file/Index.svelte b/js/file/Index.svelte index 805e8454f757c..f8e72a56aa8eb 100644 --- a/js/file/Index.svelte +++ b/js/file/Index.svelte @@ -40,6 +40,7 @@ clear: never; select: SelectData; clear_status: LoadingStatus; + delete: FileData; }>; export let file_count: string; export let file_types: string[] = ["file"]; @@ -110,6 +111,9 @@ loading_status.status = "error"; gradio.dispatch("error", detail); }} + on:delete={({ detail }) => { + gradio.dispatch("delete", detail); + }} i18n={gradio.i18n} > diff --git a/js/file/shared/FilePreview.svelte b/js/file/shared/FilePreview.svelte index 0c63685fae779..fbc9c0bcb41de 100644 --- a/js/file/shared/FilePreview.svelte +++ b/js/file/shared/FilePreview.svelte @@ -8,6 +8,7 @@ const dispatch = createEventDispatcher<{ select: SelectData; change: FileData[] | FileData; + delete: FileData; }>(); export let value: FileData | FileData[]; export let selectable = false; @@ -46,9 +47,10 @@ } function remove_file(index: number): void { - normalized_files.splice(index, 1); + const removed = normalized_files.splice(index, 1); normalized_files = [...normalized_files]; value = normalized_files; + dispatch("delete", removed[0]); dispatch("change", normalized_files); } diff --git a/js/file/shared/FileUpload.svelte b/js/file/shared/FileUpload.svelte index 7e2ab3d8eea2f..a578631e9025a 100644 --- a/js/file/shared/FileUpload.svelte +++ b/js/file/shared/FileUpload.svelte @@ -50,16 +50,19 @@ $: dispatch("drag", dragging); - + {#if value && (Array.isArray(value) ? value.length > 0 : true)} - + {:else} => { const buffer = (await fsPromises.readFile(filePath)).toString("base64"); const dataTransfer = await page.evaluateHandle( - async ({ bufferData, localFileName, localFileType }) => { + async ({ bufferData, localFileName, localFileType, count }) => { const dt = new DataTransfer(); const blobData = await fetch(bufferData).then((res) => res.blob()); - const file = new File([blobData], localFileName, { type: localFileType }); - dt.items.add(file); + const file = new File([blobData], localFileName, { + type: localFileType + }); + + for (let i = 0; i < count; i++) { + dt.items.add(file); + } return dt; }, { bufferData: `data:application/octet-stream;base64,${buffer}`, localFileName: fileName, - localFileType: fileType + localFileType: fileType, + count } ); - await page.dispatchEvent(selector, "drop", { dataTransfer }); + if (typeof selector === "string") { + await page.dispatchEvent(selector, "drop", { dataTransfer }); + } else { + await selector.dispatchEvent("drop", { dataTransfer }); + } }; diff --git a/js/upload/src/Upload.svelte b/js/upload/src/Upload.svelte index 0ce9ce930681d..5870b40f5056b 100644 --- a/js/upload/src/Upload.svelte +++ b/js/upload/src/Upload.svelte @@ -150,6 +150,7 @@ } else { return false; } + return ( acceptArray.includes(uploaded_file_extension) || acceptArray.some((type) => {