VFS explorer - two panes and external file drop¶
Source: examples/panels/tanstack/table/tst_vfsexplorer_extfiledrop.py
Test: tests/panels/tanstack/table/examples/test_tst_vfsexplorer_extfiledrop.py
A virtual filesystem explorer, in two panes. Documents and Staging name the same transfer_group, so a row dragged out of either lands in the other, and both take files dragged in from the desktop - one reading the bytes, one reading the metadata only. This is the tree half of the panel: the shape changes constantly, and Python decides every change.
Features on display¶
Toolbar and context menu sharing the same relabelled
new-folderandnew-fileentries, so both make the same two kinds of node.Multiselect: click,
Ctrlclick,Shiftclick. Only the checkbox cascades, and only underselect_mode: "hierarchy".Drag and drop inside a tree, and across the two panes via
transfer_group. HoldingCtrlorAltat the drop copies rather than moves, across the panes only.Vetoes:
Archive (read only)refuses drops throughmove_callback, and new nodes, renames, deletes, pastes and file drops throughaction_callback.External file drop:
drop_files: "content"on the left reads the bytes,"meta"on the right reads the name, size, MIME type and stamp only.drop_acceptanddrop_max_bytesdecide what is taken, in Python.The bytes never enter the tree. They reach
event_callbackand stop there; the node keeps the name, the size and the MIME type.Clipboard and history:
Ctrl+X/C/V, andCtrl+Zthat steps both panes back together after a transfer.Icons from
icon_for, which maps a file extension onto a bundled glyph.Two live checkboxes toggle
show_checkboxesandmenuon both panes at runtime.
The wiring¶
NEW_FOLDER = {"id": "new-folder", "label": "New folder", "node": {**FOLDER, "children": []}}
NEW_FILE = {
"id": "new-file",
"label": "New file",
"node": {"kind": "file", "icon": "file", "allow_children": False},
}
def allow_move(key: str, anchor_key: str, position: str) -> bool:
"""Veto anything that would land inside the read-only branch."""
return not (position == "child" and anchor_key == LOCKED_KEY)
table = TanstackTable(
source=source,
columns=columns,
options={
"select_mode": "hierarchy",
"enable_dnd": True,
"expand_all": True,
"aria_label": "Documents",
"transfer_group": TRANSFER_GROUP,
"toggle_on_click": True,
"show_checkboxes": False,
"menu": [],
"toolbar": [
"undo", "redo", "|",
NEW_FOLDER, NEW_FILE, "rename", "delete", "|",
"cut", "copy", "paste", "|",
"move-up", "move-down", "outdent", "indent", "|",
"expand-all", "collapse-all", "|",
"select-all", "clear-selection", "search",
],
"search_label": "Search name or kind",
"new_key_prefix": "doc",
"drop_files": "content",
"drop_accept": [".pdf", ".csv", ".md", ".txt", "image/*"],
"drop_max_bytes": 1_000_000,
"drop_node": {"kind": "file"},
},
event_callback=partial(on_event, "explorer"),
move_callback=allow_move,
action_callback=allow_action,
sizing_mode="stretch_both",
)
Key points:
A dict entry in
toolbarrelabels one action and gives it a node template, sonew-foldermints a folder here and could mint anything elsewhere. The label is also the new node’s title.The second table (
staging) shares the group but has no vetoes: it logs throughevent_callbackand takes whatever the explorer will let go of, and the explorer’s own hooks still decide what may leave it.Nodes never travel through the browser. The drag carries the group, the source pane’s id and the keys; the receiving table reads the nodes out of the other one in Python.
on_eventreportsactivate,move,transfer,drop_files,add,rename,delete,cut,copyandpaste, reading from each payload whatever that event carries:keyfor anactivate,appliedandapplied_keyswhere the intent sets them.
How the test exercises it¶
Both panes serve and ask for different halves of one drop (
contentversusmeta), sharing onetransfer_group.Cross-pane drag: dragging
invoice.pdfontoScratchputs it instaging.sourceand takes it out oftable.source, and the arrival is logged by the pane that placed it.File drop: a dropped
plan.mdbecomes a node carryingsize,mimeand thedrop_nodetemplate, with nocontentkey in the tree.Refusals: a file over
drop_max_bytescomes back(size), a.zipon the left comes back(type)while the right pane takes it and reads nothing, and a drop ontoArchive (read only)is refused byaction_callback.One drop, one step: three files dropped together are a single change, so one
undo()takes all three back and leavescan_undofalse.
Run it live¶
This example runs entirely in your browser via Pyodide. The first load downloads packages, so give it a few seconds.
See also¶
TanstackTable - the panel guide
Treegrid - columns and cell editors - columns and cell editors on a tree that never reshapes
Big tree - one node to a million, and what each size costs - what a large tree costs, from one node to a million
Filesystem browser - lazy loading from a real backend - the same panel over a real directory tree