+ <.file_drop_input
+ upload={@uploads.notebook}
+ label="Notebook"
+ on_clear={JS.push("clear_file", target: @myself)}
+ />
<%= if @error do %>
@@ -59,7 +32,7 @@ defmodule LivebookWeb.OpenLive.UploadComponent do
Import
@@ -68,12 +41,6 @@ defmodule LivebookWeb.OpenLive.UploadComponent do
"""
end
- @impl true
- def handle_event("clear-file", _params, socket) do
- {socket, _entries} = Phoenix.LiveView.Upload.maybe_cancel_uploads(socket)
- {:noreply, assign(socket, error: false)}
- end
-
@impl true
def handle_event("validate", _params, socket) do
has_error? = Enum.any?(socket.assigns.uploads.notebook.entries, &(not &1.valid?))
@@ -81,7 +48,11 @@ defmodule LivebookWeb.OpenLive.UploadComponent do
{:noreply, assign(socket, error: has_error?)}
end
- @impl true
+ def handle_event("clear_file", _params, socket) do
+ {socket, _entries} = Phoenix.LiveView.Upload.maybe_cancel_uploads(socket)
+ {:noreply, assign(socket, error: false)}
+ end
+
def handle_event("save", _params, socket) do
consume_uploaded_entries(socket, :notebook, fn %{path: path}, _entry ->
content = File.read!(path)
diff --git a/lib/livebook_web/live/session_live.ex b/lib/livebook_web/live/session_live.ex
index be22842b5..b38ab013c 100644
--- a/lib/livebook_web/live/session_live.ex
+++ b/lib/livebook_web/live/session_live.ex
@@ -860,6 +860,13 @@ defmodule LivebookWeb.SessionLive do
<.remix_icon icon="download-cloud-2-line" class="align-middle" />
From URL
+ <.link
+ patch={~p"/sessions/#{@session.id}/add-file/upload"}
+ class={["tab", @tab == "upload" && "active"]}
+ >
+ <.remix_icon icon="file-upload-line" class="align-middle" />
+
From upload
+
<.link
patch={~p"/sessions/#{@session.id}/add-file/unlisted"}
class={["tab", @tab == "unlisted" && "active"]}
@@ -881,6 +888,12 @@ defmodule LivebookWeb.SessionLive do
id="add-file-entry-from-url"
session={@session}
/>
+ <.live_component
+ :if={@tab == "upload"}
+ module={LivebookWeb.SessionLive.AddFileEntryUploadComponent}
+ id="add-file-entry-from-upload"
+ session={@session}
+ />
<.live_component
:if={@tab == "unlisted"}
module={LivebookWeb.SessionLive.AddFileEntryUnlistedComponent}
diff --git a/lib/livebook_web/live/session_live/add_file_entry_upload_component.ex b/lib/livebook_web/live/session_live/add_file_entry_upload_component.ex
new file mode 100644
index 000000000..94cba847b
--- /dev/null
+++ b/lib/livebook_web/live/session_live/add_file_entry_upload_component.ex
@@ -0,0 +1,119 @@
+defmodule LivebookWeb.SessionLive.AddFileEntryUploadComponent do
+ use LivebookWeb, :live_component
+
+ import Ecto.Changeset
+
+ alias Livebook.FileSystem
+
+ @impl true
+ def mount(socket) do
+ {:ok,
+ socket
+ |> assign(changeset: changeset(), error_message: nil)
+ |> allow_upload(:file, accept: :any, max_entries: 1, max_file_size: 100_000_000_000)}
+ end
+
+ defp changeset(attrs \\ %{}) do
+ data = %{name: nil}
+ types = %{name: :string}
+
+ cast({data, types}, attrs, [:name])
+ |> validate_required([:name])
+ |> Livebook.Notebook.validate_file_entry_name(:name)
+ end
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+
+ <%= @error_message %>
+
+ <.form
+ :let={f}
+ for={@changeset}
+ as={:data}
+ id="add-file-entry-form"
+ phx-change="validate"
+ phx-submit="add"
+ phx-target={@myself}
+ >
+
+ <.file_drop_input
+ upload={@uploads.file}
+ label="File"
+ on_clear={JS.push("clear_file", target: @myself)}
+ />
+ <.text_field field={f[:name]} label="Name" autocomplete="off" phx-debounce="blur" />
+
+
+
+ Add
+
+ <.link patch={~p"/sessions/#{@session.id}"} class="button-base button-outlined-gray">
+ Cancel
+
+
+
+
+ """
+ end
+
+ @impl true
+ def handle_event("validate", %{"data" => data} = params, socket) do
+ upload_entries = socket.assigns.uploads.file.entries
+
+ data =
+ case {params["_target"], data["name"], upload_entries} do
+ {["file"], "", [entry]} ->
+ %{data | "name" => entry.client_name}
+
+ _ ->
+ data
+ end
+
+ changeset = data |> changeset() |> Map.replace!(:action, :validate)
+
+ {:noreply, assign(socket, changeset: changeset)}
+ end
+
+ def handle_event("clear_file", _params, socket) do
+ {socket, _entries} = Phoenix.LiveView.Upload.maybe_cancel_uploads(socket)
+ {:noreply, assign(socket, error_message: nil)}
+ end
+
+ def handle_event("add", %{"data" => data}, socket) do
+ data
+ |> changeset()
+ |> apply_action(:insert)
+ |> case do
+ {:ok, data} ->
+ %{files_dir: files_dir} = socket.assigns.session
+
+ [upload_result] =
+ consume_uploaded_entries(socket, :file, fn %{path: path}, _entry ->
+ upload_file = FileSystem.File.local(path)
+ destination_file = FileSystem.File.resolve(files_dir, data.name)
+ result = FileSystem.File.copy(upload_file, destination_file)
+ {:ok, result}
+ end)
+
+ case upload_result do
+ :ok ->
+ file_entry = %{name: data.name, type: :attachment}
+ Livebook.Session.add_file_entries(socket.assigns.session.pid, [file_entry])
+ {:noreply, push_patch(socket, to: ~p"/sessions/#{socket.assigns.session.id}")}
+
+ {:error, message} ->
+ {:noreply, assign(socket, error_message: message)}
+ end
+
+ {:error, changeset} ->
+ {:noreply, assign(socket, changeset: changeset)}
+ end
+ end
+end
diff --git a/lib/livebook_web/live/session_live/insert_image_component.ex b/lib/livebook_web/live/session_live/insert_image_component.ex
index 5a72f3774..eaa82caf8 100644
--- a/lib/livebook_web/live/session_live/insert_image_component.ex
+++ b/lib/livebook_web/live/session_live/insert_image_component.ex
@@ -39,18 +39,8 @@ defmodule LivebookWeb.SessionLive.InsertImageComponent do
<%= @error_message %>
-
-
- <%= entry.client_name %>
- <%= entry.progress %>%
-
-
+
+ <.live_img_preview entry={entry} class="max-h-80 m-auto" />
<.form
:let={f}
@@ -60,18 +50,13 @@ defmodule LivebookWeb.SessionLive.InsertImageComponent do
phx-submit="save"
phx-target={@myself}
>
-
-
-
- <.live_file_input upload={@uploads.image} class="hidden" />
-
- <.remix_icon icon="folder-upload-line" />
-
-
-
-
- <.text_field field={f[:name]} placeholder="Name" autocomplete="off" phx-debounce="blur" />
-
+
+ <.file_drop_input
+ upload={@uploads.image}
+ label="File"
+ on_clear={JS.push("clear_file", target: @myself)}
+ />
+ <.text_field field={f[:name]} label="Name" autocomplete="off" phx-debounce="blur" />
<.link patch={@return_to} class="button-base button-outlined-gray">
@@ -80,10 +65,7 @@ defmodule LivebookWeb.SessionLive.InsertImageComponent do
Upload
@@ -111,6 +93,11 @@ defmodule LivebookWeb.SessionLive.InsertImageComponent do
{:noreply, assign(socket, changeset: changeset)}
end
+ def handle_event("clear_file", _params, socket) do
+ {socket, _entries} = Phoenix.LiveView.Upload.maybe_cancel_uploads(socket)
+ {:noreply, assign(socket, error_message: nil)}
+ end
+
def handle_event("save", %{"data" => data}, socket) do
data
|> changeset()
diff --git a/test/livebook_web/live/open_live_test.exs b/test/livebook_web/live/open_live_test.exs
index 6c58fb7bc..3d0480ee2 100644
--- a/test/livebook_web/live/open_live_test.exs
+++ b/test/livebook_web/live/open_live_test.exs
@@ -195,6 +195,35 @@ defmodule LivebookWeb.OpenLiveTest do
close_session_by_path(path)
end
+
+ test "allows importing notebook from upload", %{conn: conn} do
+ {:ok, view, _} = live(conn, ~p"/open/upload")
+
+ view
+ |> file_input("#upload-file-form", :notebook, [
+ %{
+ last_modified: 1_594_171_879_000,
+ name: "notebook.livemd",
+ content: """
+ # My notebook
+ """,
+ size: 14,
+ type: "text/plain"
+ }
+ ])
+ |> render_upload("notebook.livemd")
+
+ view
+ |> element("#upload-file-form")
+ |> render_submit()
+
+ {path, _flash} = assert_redirect(view, 5000)
+
+ {:ok, view, _} = live(conn, path)
+ assert render(view) =~ "My notebook"
+
+ close_session_by_path(path)
+ end
end
describe "public import endpoint" do
diff --git a/test/livebook_web/live/session_live_test.exs b/test/livebook_web/live/session_live_test.exs
index e598c1769..ee6c635ae 100644
--- a/test/livebook_web/live/session_live_test.exs
+++ b/test/livebook_web/live/session_live_test.exs
@@ -1667,6 +1667,42 @@ defmodule LivebookWeb.SessionLiveTest do
Session.get_data(session.pid)
end
+ test "adding :attachment file entry from upload", %{conn: conn, session: session} do
+ Session.subscribe(session.id)
+
+ {:ok, view, _} = live(conn, ~p"/sessions/#{session.id}/add-file/upload")
+
+ view
+ |> file_input(~s{#add-file-entry-form}, :file, [
+ %{
+ last_modified: 1_594_171_879_000,
+ name: "image.jpg",
+ content: "content",
+ size: 7,
+ type: "text/plain"
+ }
+ ])
+ |> render_upload("image.jpg")
+
+ # Validations
+ assert view
+ |> element(~s{#add-file-entry-form})
+ |> render_change(%{"data" => %{"name" => "na me"}}) =~
+ "should contain only alphanumeric characters, dash, underscore and dot"
+
+ view
+ |> element(~s{#add-file-entry-form})
+ |> render_submit(%{"data" => %{"name" => "image.jpg"}})
+
+ assert_receive {:operation, {:add_file_entries, _client_id, [%{name: "image.jpg"}]}}
+
+ assert %{notebook: %{file_entries: [%{type: :attachment, name: "image.jpg"}]}} =
+ Session.get_data(session.pid)
+
+ assert FileSystem.File.resolve(session.files_dir, "image.jpg") |> FileSystem.File.read() ==
+ {:ok, "content"}
+ end
+
test "adding :attachment file entry from unlisted files", %{conn: conn, session: session} do
for name <- ["file1.txt", "file2.txt", "file3.txt"] do
file = FileSystem.File.resolve(session.files_dir, name)