Skip to content

Commit 13a83e5

Browse files
hannahblairgradio-pr-botabidlabs
authored
Allow reordering files in gr.File (#10210)
* allow reordering files in gr.File * add changeset * fix test * Update gradio/components/file.py Co-authored-by: Abubakar Abid <[email protected]> --------- Co-authored-by: gradio-pr-bot <[email protected]> Co-authored-by: Abubakar Abid <[email protected]>
1 parent d334769 commit 13a83e5

File tree

8 files changed

+137
-9
lines changed

8 files changed

+137
-9
lines changed

.changeset/real-ads-like.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@gradio/file": minor
3+
"gradio": minor
4+
---
5+
6+
feat:Allow reordering files in gr.File

gradio/components/file.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ def __init__(
6161
elem_classes: list[str] | str | None = None,
6262
render: bool = True,
6363
key: int | str | None = None,
64+
allow_reordering: bool = False,
6465
):
6566
"""
6667
Parameters:
@@ -82,6 +83,7 @@ def __init__(
8283
elem_classes: An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.
8384
render: If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.
8485
key: if assigned, will be used to assume identity across a re-render. Components that have the same key across a re-render will have their value preserved.
86+
allow_reordering: if True, will allow users to reorder uploaded files by dragging and dropping.
8587
"""
8688
file_count_valid_types = ["single", "multiple", "directory"]
8789
self.file_count = file_count
@@ -129,6 +131,7 @@ def __init__(
129131
)
130132
self.type = type
131133
self.height = height
134+
self.allow_reordering = allow_reordering
132135

133136
def _process_single_file(self, f: FileData) -> NamedString | bytes:
134137
file_name = f.path

gradio/templates.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,7 @@ def __init__(
534534
elem_classes: list[str] | str | None = None,
535535
render: bool = True,
536536
key: int | str | None = None,
537+
allow_reordering: bool = False,
537538
):
538539
super().__init__(
539540
value,
@@ -554,6 +555,7 @@ def __init__(
554555
elem_classes=elem_classes,
555556
render=render,
556557
key=key,
558+
allow_reordering=allow_reordering,
557559
)
558560

559561

js/file/File.stories.svelte

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,23 @@
3434
}}
3535
/>
3636
<Story
37-
name="Multiple files, with height set to 150px"
37+
name="Multiple files, with height set to 150px and reordering enabled"
3838
args={{
39-
value: Array(10).fill({
40-
path: "cheetah.jpg",
41-
orig_name: "cheetah.jpg",
42-
url: "https://gradio-builds.s3.amazonaws.com/demo-files/ghepardo-primo-piano.jpg",
43-
size: 10000
44-
}),
45-
height: 150
39+
value: [
40+
{
41+
path: "cheetah.jpg",
42+
orig_name: "cheetah.jpgz",
43+
url: "https://raw.githubusercontent.com/gradio-app/gradio/main/test/test_files/bus.png",
44+
size: 10000
45+
},
46+
{
47+
path: "cheetah.jpgs",
48+
orig_name: "cheetah.jpg",
49+
url: "https://gradio-builds.s3.amazonaws.com/demo-files/ghepardo-primo-piano.jpg",
50+
size: 10000
51+
}
52+
],
53+
height: 150,
54+
allow_reordering: true
4655
}}
4756
/>

js/file/Index.svelte

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
export let file_count: "single" | "multiple" | "directory";
4747
export let file_types: string[] = ["file"];
4848
export let input_ready: boolean;
49+
export let allow_reordering = false;
4950
let uploading = false;
5051
$: input_ready = !uploading;
5152
@@ -103,6 +104,7 @@
103104
selectable={_selectable}
104105
{root}
105106
{height}
107+
{allow_reordering}
106108
bind:uploading
107109
max_file_size={gradio.max_file_size}
108110
on:change={({ detail }) => {

js/file/shared/FilePreview.svelte

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,64 @@
1515
export let selectable = false;
1616
export let height: number | undefined = undefined;
1717
export let i18n: I18nFormatter;
18+
export let allow_reordering = false;
19+
20+
let dragging_index: number | null = null;
21+
let drop_target_index: number | null = null;
22+
23+
function handle_drag_start(event: DragEvent, index: number): void {
24+
dragging_index = index;
25+
if (event.dataTransfer) {
26+
event.dataTransfer.effectAllowed = "move";
27+
event.dataTransfer.setData("text/plain", index.toString());
28+
}
29+
}
30+
31+
function handle_drag_over(event: DragEvent, index: number): void {
32+
event.preventDefault();
33+
if (index === normalized_files.length - 1) {
34+
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
35+
const midY = rect.top + rect.height / 2;
36+
drop_target_index =
37+
event.clientY > midY ? normalized_files.length : index;
38+
} else {
39+
drop_target_index = index;
40+
}
41+
if (event.dataTransfer) {
42+
event.dataTransfer.dropEffect = "move";
43+
}
44+
}
45+
46+
function handle_drag_end(event: DragEvent): void {
47+
if (
48+
!event.dataTransfer?.dropEffect ||
49+
event.dataTransfer.dropEffect === "none"
50+
) {
51+
dragging_index = null;
52+
drop_target_index = null;
53+
}
54+
}
55+
56+
function handle_drop(event: DragEvent, index: number): void {
57+
event.preventDefault();
58+
if (dragging_index === null || dragging_index === index) return;
59+
60+
const files = Array.isArray(value) ? [...value] : [value];
61+
const [removed] = files.splice(dragging_index, 1);
62+
files.splice(
63+
drop_target_index === normalized_files.length
64+
? normalized_files.length
65+
: index,
66+
0,
67+
removed
68+
);
69+
70+
const new_value = Array.isArray(value) ? files : files[0];
71+
dispatch("change", new_value);
72+
73+
dragging_index = null;
74+
drop_target_index = null;
75+
}
1876
1977
function split_filename(filename: string): [string, string] {
2078
const last_dot = filename.lastIndexOf(".");
@@ -70,15 +128,34 @@
70128
>
71129
<table class="file-preview">
72130
<tbody>
73-
{#each normalized_files as file, i (file)}
131+
{#each normalized_files as file, i (file.url)}
74132
<tr
75133
class="file"
76134
class:selectable
135+
class:dragging={dragging_index === i}
136+
class:drop-target={drop_target_index === i ||
137+
(i === normalized_files.length - 1 &&
138+
drop_target_index === normalized_files.length)}
139+
data-drop-target={drop_target_index === normalized_files.length &&
140+
i === normalized_files.length - 1
141+
? "after"
142+
: drop_target_index === i + 1
143+
? "after"
144+
: "before"}
145+
draggable={allow_reordering && normalized_files.length > 1}
77146
on:click={(event) => {
78147
handle_row_click(event, i);
79148
}}
149+
on:dragstart={(event) => handle_drag_start(event, i)}
150+
on:dragenter|preventDefault
151+
on:dragover={(event) => handle_drag_over(event, i)}
152+
on:drop={(event) => handle_drop(event, i)}
153+
on:dragend={handle_drag_end}
80154
>
81155
<td class="filename" aria-label={file.orig_name}>
156+
{#if allow_reordering && normalized_files.length > 1}
157+
<span class="drag-handle">⋮⋮</span>
158+
{/if}
82159
<span class="stem">{file.filename_stem}</span>
83160
<span class="ext">{file.filename_ext}</span>
84161
</td>
@@ -204,4 +281,30 @@
204281
tbody > tr:nth-child(odd) {
205282
background: var(--table-odd-background-fill);
206283
}
284+
285+
.drag-handle {
286+
cursor: grab;
287+
color: var(--body-text-color-subdued);
288+
padding-right: var(--size-2);
289+
user-select: none;
290+
}
291+
292+
.dragging {
293+
opacity: 0.5;
294+
cursor: grabbing;
295+
}
296+
297+
.drop-target {
298+
border-top: 2px solid var(--color-accent);
299+
}
300+
301+
tr:last-child.drop-target[data-drop-target="before"] {
302+
border-top: 2px solid var(--color-accent);
303+
border-bottom: none;
304+
}
305+
306+
tr:last-child.drop-target[data-drop-target="after"] {
307+
border-top: none;
308+
border-bottom: 2px solid var(--color-accent);
309+
}
207310
</style>

js/file/shared/FileUpload.svelte

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
export let upload: Client["upload"];
2323
export let stream_handler: Client["stream"];
2424
export let uploading = false;
25+
export let allow_reordering = false;
2526
2627
async function handle_upload({
2728
detail
@@ -97,6 +98,7 @@
9798
{height}
9899
on:change
99100
on:delete
101+
{allow_reordering}
100102
/>
101103
{:else}
102104
<Upload

test/components/test_file.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def test_component_functions(self):
4545
"key": None,
4646
"height": None,
4747
"type": "filepath",
48+
"allow_reordering": False,
4849
}
4950
assert file_input.preprocess(None) is None
5051
assert file_input.preprocess(x_file) is not None

0 commit comments

Comments
 (0)