Skip to content

Commit

Permalink
Pluto ux process file drop (#707)
Browse files Browse the repository at this point in the history
* Add Support for file drag & drop

* Interoperability with Drop Ruler

* Save file and get path in the front-end

* Working POC

* Add 'raw' before path string literals

* Move code templates to Julia

* Change Image sample to use Images

* Use TableIOInterface for Files -> Table conversions

* Move drop handler out of Cell.js

* Update the code for images & text to use let block

* Make file transfer !!!25%!!! FASTER dropping base64 usage

* Use correct is_extension_supported

* Handle non-empty cells

* Rename hook, var for debounce ms

* Prevent error message while saving

* Make JavaScripty names more Plutonic

* update the handling of Juia files

* Move assets folder along with notebook

* Don't run file event when moving cells

* Save files with the same name with incremental numbers

* make warning message less aggressive

* 🦆 Fix expression equality again

* Revert "🦆 Fix expression equality again"

This reverts commit 81d81be.

* Fix generated path code by lungben

* Add statistics

* Benjamin's compat suggestion

* Use the latest dralbase

Also, we now support file-drops to cells with code (adds cell after it)

* fix 1 bug, handle drops on body

* fix typo

* Fix frontend tests thanks)

* don't change whitespace!

Co-authored-by: Fons van der Plas <[email protected]>
  • Loading branch information
Παναγιώτης Γεωργακόπουλος and fonsp authored Dec 30, 2020
1 parent dff84f0 commit 03a28fd
Show file tree
Hide file tree
Showing 11 changed files with 266 additions and 11 deletions.
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ MsgPack = "99f44e22-a591-53d1-9472-aa23ef4bd671"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
Sockets = "6462fe0b-24de-5631-8697-dd941f90decc"
TableIOInterface = "d1efa939-5518-4425-949f-ab857e148477"
Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"

Expand All @@ -26,6 +27,7 @@ HTTP = "0.8.18,0.9"
MsgPack = "1.1"
Tables = "1"
julia = "^1.0.4"
TableIOInterface = "0.1"

[extras]
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
Expand Down
1 change: 1 addition & 0 deletions frontend/common/Feedback.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const create_counter_statistics = () => {
numEvals: 0, // integer
numRuns: 0, // integer
numBondSets: 0, // integer
numFileDrops: 0, // integer
}
}

Expand Down
11 changes: 10 additions & 1 deletion frontend/components/Cell.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { html, useState, useEffect, useLayoutEffect, useRef, useContext } from "../imports/Preact.js"
import { html, useState, useEffect, useMemo, useRef, useContext } from "../imports/Preact.js"

import { CellOutput } from "./CellOutput.js"
import { CellInput } from "./CellInput.js"
import { RunArea, useMillisSinceTruthy } from "./RunArea.js"
import { cl } from "../common/ClassTable.js"
import { useDropHandler } from "./useDropHandler.js"
import { PlutoContext } from "../common/PlutoContext.js"

/**
Expand Down Expand Up @@ -36,6 +37,7 @@ export const Cell = ({

// cm_forced_focus is null, except when a line needs to be highlighted because it is part of a stack trace
const [cm_forced_focus, set_cm_forced_focus] = useState(null)
const { saving_file, drag_active, handler } = useDropHandler()
const localTimeRunning = 10e5 * useMillisSinceTruthy(running)
useEffect(() => {
const focusListener = (e) => {
Expand Down Expand Up @@ -65,13 +67,19 @@ export const Cell = ({

return html`
<pluto-cell
onDragOver=${handler}
onDrop=${handler}
onDragEnter=${handler}
onDragLeave=${handler}
class=${cl({
queued: queued,
running: running,
errored: errored,
selected: selected,
code_differs: class_code_differs,
code_folded: class_code_folded,
drop_target: drag_active,
saving_file: saving_file,
})}
id=${cell_id}
>
Expand Down Expand Up @@ -110,6 +118,7 @@ export const Cell = ({
focus_after_creation=${focus_after_creation}
cm_forced_focus=${cm_forced_focus}
set_cm_forced_focus=${set_cm_forced_focus}
on_drag_drop_events=${handler}
on_submit=${() => {
pluto_actions.change_remote_cell(cell_id)
}}
Expand Down
20 changes: 20 additions & 0 deletions frontend/components/CellInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const CellInput = ({
on_change,
on_update_doc_query,
on_focus_neighbor,
on_drag_drop_events,
cell_id,
notebook_id,
}) => {
Expand Down Expand Up @@ -354,6 +355,25 @@ export const CellInput = ({
}
return true
}

cm.on("dragover", (cm_, e) => {
on_drag_drop_events(e)
return true
})
cm.on("drop", (cm_, e) => {
on_drag_drop_events(e)
e.preventDefault()
return true
})
cm.on("dragenter", (cm_, e) => {
on_drag_drop_events(e)
return true
})
cm.on("dragleave", (cm_, e) => {
on_drag_drop_events(e)
return true
})

cm.on("cursorActivity", () => {
setTimeout(() => {
if (!cm.hasFocus()) return
Expand Down
7 changes: 7 additions & 0 deletions frontend/components/DropRuler.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,20 @@ export class DropRuler extends Component {
}
})
document.addEventListener("dragenter", (e) => {
if (e.dataTransfer.types[0] !== "text/pluto-cell") return
if (!this.state.drag_target) this.precompute_cell_edges()
this.lastenter = e.target
this.setState({ drag_target: true })
})
document.addEventListener("dragleave", (e) => {
if (e.dataTransfer.types[0] !== "text/pluto-cell") return
if (e.target === this.lastenter) {
this.setState({ drag_target: false })
}
})
document.addEventListener("dragover", (e) => {
// Called continuously during drag
if (e.dataTransfer.types[0] !== "text/pluto-cell") return
this.mouse_position = e

this.setState({
Expand All @@ -78,6 +81,10 @@ export class DropRuler extends Component {
})
document.addEventListener("drop", (e) => {
// Guaranteed to fire before the 'dragend' event
// Ignore files
if (e.dataTransfer.types[0] !== "text/pluto-cell") {
return
}
this.setState({
drag_target: false,
})
Expand Down
51 changes: 41 additions & 10 deletions frontend/components/Editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { slice_utf8, length_utf8 } from "../common/UnicodeTools.js"
import { has_ctrl_or_cmd_pressed, ctrl_or_cmd_name, is_mac_keyboard, in_textarea_or_input } from "../common/KeyboardShortcuts.js"
import { handle_log } from "../common/Logging.js"
import { PlutoContext, PlutoBondsContext } from "../common/PlutoContext.js"
import { useDropHandler } from "./useDropHandler.js"

const default_path = "..."
const DEBUG_DIFFING = false
Expand Down Expand Up @@ -60,6 +61,23 @@ function deserialize_cells(serialized_cells) {
return segments.map((s) => s.trim()).filter((s) => s !== "")
}

const Main = ({ children }) => {
const { handler } = useDropHandler()
useEffect(() => {
document.body.addEventListener("drop", handler)
document.body.addEventListener("dragover", handler)
document.body.addEventListener("dragenter", handler)
document.body.addEventListener("dragleave", handler)
return () => {
document.body.removeEventListener("drop", handler)
document.body.removeEventListener("dragover", handler)
document.body.removeEventListener("dragenter", handler)
document.body.removeEventListener("dragleave", handler)
}
})
return html`<main>${children}</main>`
}

/**
* @typedef CellInputData
* @type {{
Expand Down Expand Up @@ -141,14 +159,15 @@ export class Editor extends Component {
send: (...args) => this.client.send(...args),
update_notebook: (...args) => this.update_notebook(...args),
set_doc_query: (query) => this.setState({ desired_doc_query: query }),
set_local_cell: (cell_id, new_val) => {
this.setState(
set_local_cell: (cell_id, new_val, callback) => {
return this.setState(
immer((state) => {
state.cell_inputs_local[cell_id] = {
code: new_val,
}
state.selected_cells = []
})
}),
callback
)
},
focus_on_neighbor: (cell_id, delta, line = delta === -1 ? Infinity : -1, ch) => {
Expand Down Expand Up @@ -286,24 +305,24 @@ export class Editor extends Component {
notebook.cell_order = [...before, ...cell_ids, ...after]
})
},
add_remote_cell_at: async (index) => {
add_remote_cell_at: async (index, code = "") => {
let id = uuidv4()
this.setState({ last_created_cell: id })
await update_notebook((notebook) => {
notebook.cell_inputs[id] = {
cell_id: id,
code: "",
code,
code_folded: false,
}
notebook.cell_order = [...notebook.cell_order.slice(0, index), id, ...notebook.cell_order.slice(index, Infinity)]
})
await this.client.send("run_multiple_cells", { cells: [id] }, { notebook_id: this.state.notebook.notebook_id })
return id
},
add_remote_cell: async (cell_id, before_or_after) => {
add_remote_cell: async (cell_id, before_or_after, code) => {
const index = this.state.notebook.cell_order.indexOf(cell_id)
const delta = before_or_after == "before" ? 0 : 1

await this.actions.add_remote_cell_at(index + delta)
return await this.actions.add_remote_cell_at(index + delta, code)
},
confirm_delete_multiple: async (verb, cell_ids) => {
if (cell_ids.length <= 1 || confirm(`${verb} ${cell_ids.length} cells?`)) {
Expand Down Expand Up @@ -397,6 +416,18 @@ export class Editor extends Component {
false
)
},
write_file: (cell_id, { file, name, type }) => {
this.counter_statistics.numFileDrops++
return this.client.send(
"write_file",
{ file, name, type, path: this.state.notebook.path },
{
notebook_id: this.state.notebook.notebook_id,
cell_id: cell_id,
},
true
)
},
}

// these are update message that are _not_ a response to a `send(*, *, {create_promise: true})`
Expand Down Expand Up @@ -777,7 +808,7 @@ export class Editor extends Component {
</button>
</nav>
</header>
<main>
<${Main}>
<preamble>
<button
onClick=${() => {
Expand Down Expand Up @@ -819,7 +850,7 @@ export class Editor extends Component {
}
}}
/>
</main>
</${Main}>
<${LiveDocs}
desired_doc_query=${this.state.desired_doc_query}
on_update_doc_query=${this.actions.set_doc_query}
Expand Down
1 change: 1 addition & 0 deletions frontend/components/Notebook.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { PlutoContext } from "../common/PlutoContext.js"
import { html, useContext, useEffect, useMemo, useState } from "../imports/Preact.js"

import { Cell } from "./Cell.js"
import { useDropHandler } from "./useDropHandler.js"

let CellMemo = ({
cell_input,
Expand Down
90 changes: 90 additions & 0 deletions frontend/components/useDropHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { PlutoContext } from "../common/PlutoContext.js"
import { useState, useMemo, useContext } from "../imports/Preact.js"

const MAGIC_TIMEOUT = 500
const DEBOUNCE_MAGIC_MS = 250

const prepareFile = (file) =>
new Promise((resolve, reject) => {
const { name, type } = file
const fr = new FileReader()
fr.onerror = () => reject("Failed to read file!")
fr.onloadstart = () => {}
fr.onprogress = ({ loaded, total }) => {}
fr.onload = () => {}
fr.onloadend = ({ target: { result } }) => resolve({ file: result, name, type })
fr.readAsArrayBuffer(file)
})

export const useDropHandler = () => {
let pluto_actions = useContext(PlutoContext)
const [saving_file, set_saving_file] = useState(false)
const [drag_active, set_drag_active_fast] = useState(false)
const set_drag_active = useMemo(() => _.debounce(set_drag_active_fast, DEBOUNCE_MAGIC_MS), [set_drag_active_fast])

const handler = useMemo(() => {
const uploadAndCreateCodeTemplate = async (file, drop_cell_id) => {
if (!(file instanceof File)) return " # File can't be read"
set_saving_file(true)
const {
message: { success, code },
} = await prepareFile(file).then(
(preparedObj) => {
return pluto_actions.write_file(drop_cell_id, preparedObj)
},
() => alert("Pluto can't save this file 😥")
)
set_saving_file(false)
set_drag_active_fast(false)
if (!success) {
alert("Pluto can't save this file 😥")
return "# File save failed"
}
if (code) return code
alert("Pluto doesn't know what to do with this file 😥. Feel that's wrong? Open an issue!")
return ""
}
return (ev) => {
ev.stopPropagation()
// dataTransfer is in Protected Mode here. see type, let Pluto DropRuler handle it.
if (ev.dataTransfer.types[0] === "text/pluto-cell") return
switch (ev.type) {
case "cmdrop":
case "drop":
ev.preventDefault() // don't file open
const cell_element = ev.path.find((el) => el.tagName === "PLUTO-CELL")
const drop_cell_id = cell_element?.id || document.querySelector("pluto-cell:last-child")?.id
const drop_cell_value = cell_element?.querySelector(".CodeMirror")?.CodeMirror?.getValue()
const is_empty = drop_cell_value?.length === 0 && !cell_element?.classList?.contains("code_folded")
set_drag_active(false)
if (!ev.dataTransfer.files.length) {
return
}
uploadAndCreateCodeTemplate(ev.dataTransfer.files[0], drop_cell_id).then((code) => {
if (code) {
if (!is_empty) {
pluto_actions.add_remote_cell(drop_cell_id, "after", code)
} else {
pluto_actions.set_local_cell(drop_cell_id, code, () => pluto_actions.set_and_run_multiple([drop_cell_id]))
}
}
})
break
case "dragover":
ev.preventDefault()
ev.dataTransfer.dropEffect = "copy"
set_drag_active(true)
setTimeout(() => set_drag_active(false), MAGIC_TIMEOUT)
break
case "dragenter":
set_drag_active_fast(true)
break
case "dragleave":
set_drag_active(false)
break
default:
}
}
}, [set_drag_active, set_drag_active_fast, set_saving_file, pluto_actions])
return { saving_file, drag_active, handler }
}
31 changes: 31 additions & 0 deletions frontend/editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -990,6 +990,37 @@ pluto-cell.running.errored > pluto-trafficlight::after {
background-size: 4px var(--patternHeight); /* 16 * sqrt(2) */
}

pluto-cell.drop_target {
position: relative;
}

pluto-cell.drop_target pluto-input::after {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
color: black;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
background-color: rgba(255, 255, 255, 0.75);
content: "+ Drop file here";
}

pluto-cell.drop_target.saving_file pluto-input::after {
content: "The 💾 File you dropped is being saved... ";
color: white;
background-color: rgba(3, 92, 151, 0.75);
}

pluto-cell.drop_target.drop_invalid pluto-input::after {
background-color: #ece5e5bf;
color: rgba(0, 0, 0, 0.85);
content: "Try dropping your file in an empty cell!";
}

/* Define --patternHeight for this keyframes animation to work! */
@keyframes scrollbackground {
0% {
Expand Down
Loading

0 comments on commit 03a28fd

Please sign in to comment.