diff --git a/assets/js/session/index.js b/assets/js/session/index.js index 3af405a10..986949141 100644 --- a/assets/js/session/index.js +++ b/assets/js/session/index.js @@ -281,11 +281,10 @@ function handleDocumentKeyDown(hook, event) { saveNotebook(hook); } else if (keyBuffer.tryMatch(["d", "d"])) { deleteFocusedCell(hook); - } else if ( - hook.state.focusedCellType === "elixir" && - (keyBuffer.tryMatch(["e", "e"]) || (cmd && key === "Enter")) - ) { - queueFocusedCellEvaluation(hook); + } else if (keyBuffer.tryMatch(["e", "e"]) || (cmd && key === "Enter")) { + if (hook.state.focusedCellType === "elixir") { + queueFocusedCellEvaluation(hook); + } } else if (keyBuffer.tryMatch(["e", "a"])) { queueAllCellsEvaluation(hook); } else if (keyBuffer.tryMatch(["e", "s"])) { diff --git a/assets/js/user_form/index.js b/assets/js/user_form/index.js index e8766e7e6..4046280a4 100644 --- a/assets/js/user_form/index.js +++ b/assets/js/user_form/index.js @@ -10,8 +10,8 @@ import { storeUserData } from "../lib/user"; const UserForm = { mounted() { this.el.addEventListener("submit", (event) => { - const name = this.el.data_name.value; - const hex_color = this.el.data_hex_color.value; + const name = this.el.user_form_name.value; + const hex_color = this.el.user_form_hex_color.value; storeUserData({ name, hex_color }); }); }, diff --git a/assets/package-lock.json b/assets/package-lock.json index db0da7218..e4755f629 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -46,14 +46,14 @@ } }, "../deps/phoenix": { - "version": "1.5.9", + "version": "1.5.0", "license": "MIT" }, "../deps/phoenix_html": { - "version": "2.14.3" + "version": "2.14.2" }, "../deps/phoenix_live_view": { - "version": "0.15.7", + "version": "0.15.5", "license": "MIT" }, "node_modules/@babel/code-frame": { diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js index bbac961ca..fda66463f 100644 --- a/assets/tailwind.config.js +++ b/assets/tailwind.config.js @@ -3,6 +3,7 @@ module.exports = { purge: [ '../lib/**/*.ex', '../lib/**/*.leex', + '../lib/**/*.heex', '../lib/**/*.eex', './js/**/*.js' ], diff --git a/lib/livebook_web/helpers.ex b/lib/livebook_web/helpers.ex index a2f448303..b1dc85313 100644 --- a/lib/livebook_web/helpers.ex +++ b/lib/livebook_web/helpers.ex @@ -1,6 +1,6 @@ defmodule LivebookWeb.Helpers do - import Phoenix.LiveView.Helpers - import Phoenix.HTML.Tag + use Phoenix.Component + alias LivebookWeb.Router.Helpers, as: Routes @doc """ @@ -14,7 +14,7 @@ defmodule LivebookWeb.Helpers do modal_class = Keyword.get(opts, :modal_class) modal_opts = [ - id: :modal, + id: "modal", return_to: path, modal_class: modal_class, component: component, @@ -41,15 +41,6 @@ defmodule LivebookWeb.Helpers do defp mac?(user_agent), do: String.match?(user_agent, ~r/Mac OS X/) defp windows?(user_agent), do: String.match?(user_agent, ~r/Windows/) - @doc """ - Returns [Remix](https://remixicon.com) icon tag. - """ - def remix_icon(name, attrs \\ []) do - icon_class = "ri-#{name}" - attrs = Keyword.update(attrs, :class, icon_class, fn class -> "#{icon_class} #{class}" end) - content_tag(:i, "", attrs) - end - defdelegate ansi_string_to_html(string, opts \\ []), to: LivebookWeb.ANSI @doc """ @@ -83,7 +74,7 @@ defmodule LivebookWeb.Helpers do Returns path to specific process dialog within LiveDashboard. """ def live_dashboard_process_path(socket, pid) do - pid_str = Phoenix.LiveDashboard.Helpers.encode_pid(pid) + pid_str = Phoenix.LiveDashboard.PageBuilder.encode_pid(pid) Routes.live_dashboard_path(socket, :page, node(), "processes", info: pid_str) end @@ -116,15 +107,40 @@ defmodule LivebookWeb.Helpers do end @doc """ - Renders a list of select input options with the given one selected. - """ - def render_select(name, options, selected) do - assigns = %{name: name, options: options, selected: selected} + Renders [Remix](https://remixicon.com) icon. - ~L""" - + <%= for {value, label} <- @options do %> + <% end %> @@ -134,23 +150,47 @@ defmodule LivebookWeb.Helpers do @doc """ Renders a checkbox input styled as a switch. - """ - def render_switch(name, checked, label, opts \\ []) do - assigns = %{ - name: name, - checked: checked, - label: label, - disabled: Keyword.get(opts, :disabled, false) - } - ~L""" + ## Examples + + <.switch_checkbox + name="likes_cats" + label="I very much like cats" + checked={@likes_cats} /> + """ + def switch_checkbox(assigns) do + assigns = assign_new(assigns, :disabled, fn -> false end) + + ~H"""
<%= @label %> -
""" end + + @doc """ + Renders a choice button that is either active or not. + + ## Examples + + <.choice_button active={@tab == "my_tab"} phx-click="set_my_tab"> + My tab + + """ + def choice_button(assigns) do + assigns = + assigns + |> assign_new(:class, fn -> "" end) + |> assign(:attrs, assigns_to_attributes(assigns, [:active, :class])) + + ~H""" + + """ + end end diff --git a/lib/livebook_web/live/notebook_card_component.ex b/lib/livebook_web/live/explore_helpers.ex similarity index 69% rename from lib/livebook_web/live/notebook_card_component.ex rename to lib/livebook_web/live/explore_helpers.ex index a7d5c1320..d47c464d8 100644 --- a/lib/livebook_web/live/notebook_card_component.ex +++ b/lib/livebook_web/live/explore_helpers.ex @@ -1,13 +1,17 @@ -defmodule LivebookWeb.NotebookCardComponent do - use LivebookWeb, :live_component +defmodule LivebookWeb.ExploreHelpers do + use Phoenix.Component - @impl true - def render(assigns) do - ~L""" + alias LivebookWeb.Router.Helpers, as: Routes + + @doc """ + Renders an explore notebook card. + """ + def notebook_card(assigns) do + ~H"""
<%= live_redirect to: Routes.explore_path(@socket, :notebook, @notebook_info.slug), class: "flex items-center justify-center p-6 border-2 border-gray-100 rounded-t-2xl h-[150px]" do %> - + <% end %>
<%= live_redirect @notebook_info.title, diff --git a/lib/livebook_web/live/explore_live.ex b/lib/livebook_web/live/explore_live.ex index 0fed3c116..ea05c9a83 100644 --- a/lib/livebook_web/live/explore_live.ex +++ b/lib/livebook_web/live/explore_live.ex @@ -4,6 +4,7 @@ defmodule LivebookWeb.ExploreLive do import LivebookWeb.UserHelpers import LivebookWeb.SessionHelpers + alias LivebookWeb.{SidebarHelpers, ExploreHelpers} alias Livebook.Notebook.Explore @impl true @@ -26,22 +27,20 @@ defmodule LivebookWeb.ExploreLive do @impl true def render(assigns) do - ~L""" + ~H"""
- <%= live_component LivebookWeb.SidebarComponent, - id: :sidebar, - items: [ - %{type: :logo}, - %{type: :break}, - %{type: :user, current_user: @current_user, path: Routes.explore_path(@socket, :user)} - ] %> + + + + +
<%= live_patch to: Routes.home_path(@socket, :page), class: "hidden md:block absolute top-[50%] left-[-12px] transform -translate-y-1/2 -translate-x-full" do %> - <%= remix_icon("arrow-left-line", class: "text-2xl align-middle") %> + <.remix_icon icon="arrow-left-line" class="text-2xl align-middle" /> <% end %>

Explore @@ -61,19 +60,20 @@ defmodule LivebookWeb.ExploreLive do <%= @lead_notebook_info.description %>

- <%= live_patch "Let's go", to: Routes.explore_path(@socket, :notebook, @lead_notebook_info.slug), + <%= live_patch "Let's go", + to: Routes.explore_path(@socket, :notebook, @lead_notebook_info.slug), class: "button button-blue" %>

- <%= for {info, idx} <- Enum.with_index(@notebook_infos) do %> - <%= live_component LivebookWeb.NotebookCardComponent, - id: "notebook-card-#{idx}", - notebook_info: info %> + <%# Note: it's fine to use stateless components in this comprehension, + because @notebook_infos never change %> + <%= for info <- @notebook_infos do %> + <% end %>
@@ -82,7 +82,7 @@ defmodule LivebookWeb.ExploreLive do <%= if @live_action == :user do %> <%= live_modal LivebookWeb.UserComponent, - id: :user_modal, + id: "user", modal_class: "w-full max-w-sm", user: @current_user, return_to: Routes.explore_path(@socket, :page) %> diff --git a/lib/livebook_web/live/home_live.ex b/lib/livebook_web/live/home_live.ex index 926540d00..c679acf26 100644 --- a/lib/livebook_web/live/home_live.ex +++ b/lib/livebook_web/live/home_live.ex @@ -4,6 +4,7 @@ defmodule LivebookWeb.HomeLive do import LivebookWeb.UserHelpers import LivebookWeb.SessionHelpers + alias LivebookWeb.{SidebarHelpers, ExploreHelpers} alias Livebook.{SessionSupervisor, Session, LiveMarkdown, Notebook} @impl true @@ -28,64 +29,61 @@ defmodule LivebookWeb.HomeLive do @impl true def render(assigns) do - ~L""" + ~H"""
- <%= live_component LivebookWeb.SidebarComponent, - id: :sidebar, - items: [ - %{type: :break}, - %{type: :user, current_user: @current_user, path: Routes.home_path(@socket, :user)} - ] %> + + + +
-
+
Livebook
- <%= live_patch to: Routes.home_path(@socket, :import, "url"), - class: "button button-outlined-gray whitespace-nowrap" do %> - Import - <% end %> -
+
<%= live_component LivebookWeb.PathSelectComponent, - id: "path_select", - path: @path, - extnames: [LiveMarkdown.extension()], - running_paths: paths(@session_summaries), - phx_target: nil, - phx_submit: nil do %> + id: "path_select", + path: @path, + extnames: [LiveMarkdown.extension()], + running_paths: paths(@session_summaries), + phx_target: nil, + phx_submit: nil do %>
- <%= content_tag :button, - class: "button button-outlined-gray whitespace-nowrap", - phx_click: "fork", - disabled: not path_forkable?(@path) do %> - <%= remix_icon("git-branch-line", class: "align-middle mr-1") %> + <%= if path_running?(@path, @session_summaries) do %> - <%= live_redirect "Join session", to: Routes.session_path(@socket, :page, session_id_by_path(@path, @session_summaries)), - class: "button button-blue" %> + <%= live_redirect "Join session", + to: Routes.session_path(@socket, :page, session_id_by_path(@path, @session_summaries)), + class: "button button-blue" %> <% else %> - <%= tag :span, if(File.regular?(@path) and not file_writable?(@path), - do: [class: "tooltip top", aria_label: "This file is write-protected, please fork instead"], - else: [] - ) %> - <%= content_tag :button, "Open", - class: "button button-blue", - phx_click: "open", - disabled: not path_openable?(@path, @session_summaries) %> + + <% end %>
<% end %>
+

@@ -94,37 +92,23 @@ defmodule LivebookWeb.HomeLive do <%= live_redirect to: Routes.explore_path(@socket, :page), class: "flex items-center text-blue-600" do %> See all - <%= remix_icon("arrow-right-line", class: "align-middle ml-1") %> + <.remix_icon icon="arrow-right-line" class="align-middle ml-1" /> <% end %>

- <%= for {info, idx} <- Enum.with_index(@notebook_infos) do %> - <%= live_component LivebookWeb.NotebookCardComponent, - id: "notebook-card-#{idx}", - notebook_info: info %> + <%# Note: it's fine to use stateless components in this comprehension, + because @notebook_infos never change %> + <%= for info <- @notebook_infos do %> + <% end %>
+

Running sessions

- <%= if @session_summaries == [] do %> -
-
- <%= remix_icon("windy-line", class: "text-gray-400 text-xl") %> -
-
- You do not have any running sessions. -
- Please create a new one by clicking “New notebook” -
-
- <% else %> - <%= live_component LivebookWeb.HomeLive.SessionsComponent, - id: "sessions_list", - session_summaries: @session_summaries %> - <% end %> + <.sessions_list session_summaries={@session_summaries} socket={@socket} />
@@ -132,7 +116,7 @@ defmodule LivebookWeb.HomeLive do <%= if @live_action == :user do %> <%= live_modal LivebookWeb.UserComponent, - id: :user_modal, + id: "user", modal_class: "w-full max-w-sm", user: @current_user, return_to: Routes.home_path(@socket, :page) %> @@ -140,7 +124,7 @@ defmodule LivebookWeb.HomeLive do <%= if @live_action == :close_session do %> <%= live_modal LivebookWeb.HomeLive.CloseSessionComponent, - id: :close_session_modal, + id: "close-session", modal_class: "w-full max-w-xl", return_to: Routes.home_path(@socket, :page), session_summary: @session_summary %> @@ -148,7 +132,7 @@ defmodule LivebookWeb.HomeLive do <%= if @live_action == :import do %> <%= live_modal LivebookWeb.HomeLive.ImportComponent, - id: :import_modal, + id: "import", modal_class: "w-full max-w-xl", return_to: Routes.home_path(@socket, :page), tab: @tab %> @@ -156,6 +140,73 @@ defmodule LivebookWeb.HomeLive do """ end + defp open_button_tooltip_attrs(path) do + if File.regular?(path) and not file_writable?(path) do + [class: "tooltip top", aria_label: "This file is write-protected, please fork instead"] + else + [] + end + end + + defp sessions_list(%{session_summaries: []} = assigns) do + ~H""" +
+
+ <.remix_icon icon="windy-line" class="text-gray-400 text-xl" /> +
+
+ You do not have any running sessions. +
+ Please create a new one by clicking “New notebook” +
+
+ """ + end + + defp sessions_list(assigns) do + ~H""" +
+ <%= for summary <- @session_summaries do %> +
+
+ <%= live_redirect summary.notebook_name, + to: Routes.session_path(@socket, :page, summary.session_id), + class: "font-semibold text-gray-800 hover:text-gray-900" %> +
+ <%= summary.path || "No file" %> +
+
+
+ + +
+
+ <% end %> +
+ """ + end + @impl true def handle_params(%{"session_id" => session_id}, _url, socket) do session_summary = Enum.find(socket.assigns.session_summaries, &(&1.session_id == session_id)) diff --git a/lib/livebook_web/live/home_live/close_session_component.ex b/lib/livebook_web/live/home_live/close_session_component.ex index 018b5bcf8..360bfaf0a 100644 --- a/lib/livebook_web/live/home_live/close_session_component.ex +++ b/lib/livebook_web/live/home_live/close_session_component.ex @@ -5,7 +5,7 @@ defmodule LivebookWeb.HomeLive.CloseSessionComponent do @impl true def render(assigns) do - ~L""" + ~H"""

Close session @@ -16,8 +16,8 @@ defmodule LivebookWeb.HomeLive.CloseSessionComponent do This won't delete any persisted files.

- <%= live_patch "Cancel", to: @return_to, class: "button button-outlined-gray" %> diff --git a/lib/livebook_web/live/home_live/import_component.ex b/lib/livebook_web/live/home_live/import_component.ex index a55bd679f..1f41e4783 100644 --- a/lib/livebook_web/live/home_live/import_component.ex +++ b/lib/livebook_web/live/home_live/import_component.ex @@ -3,22 +3,22 @@ defmodule LivebookWeb.HomeLive.ImportComponent do @impl true def render(assigns) do - ~L""" + ~H"""

Import notebook

<%= live_patch to: Routes.home_path(@socket, :import, "url"), - class: "tab #{if(@tab == "url", do: "active")}" do %> - <%= remix_icon("download-cloud-2-line", class: "align-middle") %> + class: "tab #{if(@tab == "url", do: "active")}" do %> + <.remix_icon icon="download-cloud-2-line" class="align-middle" /> From URL <% end %> <%= live_patch to: Routes.home_path(@socket, :import, "content"), - class: "tab #{if(@tab == "content", do: "active")}" do %> - <%= remix_icon("clipboard-line", class: "align-middle") %> + class: "tab #{if(@tab == "content", do: "active")}" do %> + <.remix_icon icon="clipboard-line" class="align-middle" /> From clipboard @@ -27,17 +27,12 @@ defmodule LivebookWeb.HomeLive.ImportComponent do
- <%= case @tab do %> - <% "url" -> %> - <%= live_component LivebookWeb.HomeLive.ImportUrlComponent, - id: "import_url" %> - - <% "content" -> %> - <%= live_component LivebookWeb.HomeLive.ImportContentComponent, - id: "import_content" %> - <% end %> + <%= live_component component_for_tab(@tab), id: "import-#{@tab}" %>
""" end + + defp component_for_tab("url"), do: LivebookWeb.HomeLive.ImportUrlComponent + defp component_for_tab("content"), do: LivebookWeb.HomeLive.ImportContentComponent end diff --git a/lib/livebook_web/live/home_live/import_content_component.ex b/lib/livebook_web/live/home_live/import_content_component.ex index 0578c00ff..a5b8da396 100644 --- a/lib/livebook_web/live/home_live/import_content_component.ex +++ b/lib/livebook_web/live/home_live/import_content_component.ex @@ -8,26 +8,26 @@ defmodule LivebookWeb.HomeLive.ImportContentComponent do @impl true def render(assigns) do - ~L""" + ~H"""

Import notebook by directly pasting the live markdown content.

- <%= f = form_for :data, "#", - phx_submit: "import", - phx_change: "validate", - phx_target: @myself, - autocomplete: "off" %> + <.form let={f} for={:data} + url="#" + phx-submit="import" + phx-change="validate" + phx-target={@myself} + autocomplete="off"> <%= textarea f, :content, value: @content, class: "input resize-none", placeholder: "Notebook content", autofocus: true, spellcheck: "false", rows: 5 %> - - <%= submit "Import", - class: "mt-5 button button-blue", - disabled: @content == "" %> - + +
""" end diff --git a/lib/livebook_web/live/home_live/import_url_component.ex b/lib/livebook_web/live/home_live/import_url_component.ex index 7ab3c6757..ad83ff20f 100644 --- a/lib/livebook_web/live/home_live/import_url_component.ex +++ b/lib/livebook_web/live/home_live/import_url_component.ex @@ -10,7 +10,7 @@ defmodule LivebookWeb.HomeLive.ImportUrlComponent do @impl true def render(assigns) do - ~L""" + ~H"""
<%= if @error_message do %>
@@ -20,19 +20,22 @@ defmodule LivebookWeb.HomeLive.ImportUrlComponent do

Paste the URL to a .livemd file, to a GitHub file, or to a Gist to import it.

- <%= f = form_for :data, "#", - phx_submit: "import", - phx_change: "validate", - phx_target: @myself, - autocomplete: "off" %> + <.form let={f} for={:data} + url="#" + phx-submit="import" + phx-change="validate" + phx-target={@myself} + autocomplete="off"> <%= text_input f, :url, value: @url, class: "input", placeholder: "Notebook URL", autofocus: true, spellcheck: "false" %> - <%= submit "Import", - class: "mt-5 button button-blue", - disabled: not Utils.valid_url?(@url) %> - + +
""" end diff --git a/lib/livebook_web/live/home_live/sessions_component.ex b/lib/livebook_web/live/home_live/sessions_component.ex deleted file mode 100644 index 0b6ff005e..000000000 --- a/lib/livebook_web/live/home_live/sessions_component.ex +++ /dev/null @@ -1,47 +0,0 @@ -defmodule LivebookWeb.HomeLive.SessionsComponent do - use LivebookWeb, :live_component - - @impl true - def render(assigns) do - ~L""" -
- <%= for summary <- @session_summaries do %> -
-
- <%= live_redirect summary.notebook_name, to: Routes.session_path(@socket, :page, summary.session_id), - class: "font-semibold text-gray-800 hover:text-gray-900" %> -
- <%= summary.path || "No file" %> -
-
-
- - -
-
- <% end %> -
- """ - end -end diff --git a/lib/livebook_web/live/modal_component.ex b/lib/livebook_web/live/modal_component.ex index ca1a2089c..25578ba59 100644 --- a/lib/livebook_web/live/modal_component.ex +++ b/lib/livebook_web/live/modal_component.ex @@ -3,9 +3,8 @@ defmodule LivebookWeb.ModalComponent do @impl true def render(assigns) do - ~L""" -
+ ~H""" +
@@ -15,17 +14,17 @@ defmodule LivebookWeb.ModalComponent do phx-capture-click="close" phx-window-keydown="close" phx-key="escape" - phx-target="#<%= @id %>" + phx-target={@myself} phx-page-loading>
-