mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-10-25 04:46:04 +08:00
Unify several forms in the application (#1392)
* Define .input_wrapper and .hex_color_input components * Define Tailwind variants from Phoenix v1.6 to help styling * Make sure single-action modals redirect on save * Add pill to session secrets in the "Secrets" session sidebar * Fix alignments of "Users" in the session sidebar
This commit is contained in:
parent
357ba7320a
commit
be862173a3
16 changed files with 183 additions and 210 deletions
|
|
@ -83,7 +83,7 @@
|
|||
/* Form fields */
|
||||
|
||||
.input {
|
||||
@apply w-full px-3 py-2 bg-gray-50 text-sm border border-gray-200 rounded-lg placeholder-gray-400 text-gray-600;
|
||||
@apply w-full px-3 py-2 bg-gray-50 text-sm border border-gray-200 rounded-lg placeholder-gray-400 text-gray-600 phx-form-error:border-red-300;
|
||||
}
|
||||
|
||||
.input[type="color"] {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
const plugin = require("tailwindcss/plugin")
|
||||
|
||||
module.exports = {
|
||||
content: [
|
||||
"../lib/**/*.ex",
|
||||
|
|
@ -101,5 +103,15 @@ module.exports = {
|
|||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
plugins: [
|
||||
plugin(({ addVariant }) => {
|
||||
addVariant("phx-loading", [".phx-loading&", ".phx-loading &"]);
|
||||
addVariant("phx-connected", [".phx-connected&", ".phx-connected &"]);
|
||||
addVariant("phx-error", [".phx-error&", ".phx-error &"]);
|
||||
addVariant("phx-form-error", [":not(.phx-no-feedback).show-errors &"]);
|
||||
addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"]);
|
||||
addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"]);
|
||||
addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"]);
|
||||
})
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ defmodule Livebook.Hubs.Fly do
|
|||
end
|
||||
end
|
||||
|
||||
def changeset(fly, attrs \\ %{}) do
|
||||
defp changeset(fly, attrs) do
|
||||
fly
|
||||
|> cast(attrs, @fields)
|
||||
|> validate_required(@fields)
|
||||
|
|
|
|||
|
|
@ -62,12 +62,12 @@ defmodule LivebookWeb do
|
|||
|
||||
# Import basic rendering functionality (render, render_layout, etc)
|
||||
import Phoenix.View
|
||||
import LivebookWeb.ErrorHelpers
|
||||
alias Phoenix.LiveView.JS
|
||||
alias LivebookWeb.Router.Helpers, as: Routes
|
||||
|
||||
# Custom helpers
|
||||
import LivebookWeb.Helpers
|
||||
import LivebookWeb.FormHelpers
|
||||
import LivebookWeb.LiveHelpers
|
||||
end
|
||||
end
|
||||
|
|
|
|||
68
lib/livebook_web/live/form_helpers.ex
Normal file
68
lib/livebook_web/live/form_helpers.ex
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
defmodule LivebookWeb.FormHelpers do
|
||||
@moduledoc """
|
||||
Conveniences for translating and building error messages.
|
||||
"""
|
||||
|
||||
use Phoenix.Component
|
||||
|
||||
import Phoenix.HTML.Form
|
||||
import LivebookWeb.LiveHelpers
|
||||
|
||||
@doc """
|
||||
A wrapper for inputs with conveniences.
|
||||
"""
|
||||
def input_wrapper(assigns) do
|
||||
assigns = assign_new(assigns, :class, fn -> [] end)
|
||||
|
||||
~H"""
|
||||
<div
|
||||
phx-feedback-for={input_name(@form, @field)}
|
||||
class={[@class, if(@form.errors[@field], do: "show-errors", else: "")]}
|
||||
>
|
||||
<%= render_slot(@inner_block) %>
|
||||
<%= for error <- Keyword.get_values(@form.errors, @field) do %>
|
||||
<span class="hidden text-red-600 text-sm phx-form-error:block">
|
||||
<%= translate_error(error) %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Hex color input.
|
||||
"""
|
||||
def hex_color_input(assigns) do
|
||||
~H"""
|
||||
<div class="flex space-x-4 items-center">
|
||||
<div
|
||||
class="border-[3px] rounded-lg p-1 flex justify-center items-center"
|
||||
style={"border-color: #{input_value(@form, @field)}"}
|
||||
>
|
||||
<div class="rounded h-5 w-5" style={"background-color: #{input_value(@form, @field)}"}></div>
|
||||
</div>
|
||||
<div class="relative grow">
|
||||
<%= text_input(@form, @field,
|
||||
class: "input",
|
||||
spellcheck: "false",
|
||||
maxlength: 7
|
||||
) %>
|
||||
<button class="icon-button absolute right-2 top-1" type="button" phx-click={@randomize}>
|
||||
<.remix_icon icon="refresh-line" class="text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Translates an error message.
|
||||
"""
|
||||
def translate_error({msg, opts}) do
|
||||
# Because the error messages we show in our forms and APIs
|
||||
# are defined inside Ecto, we need to translate them dynamically.
|
||||
Enum.reduce(opts, msg, fn {key, value}, acc ->
|
||||
String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
defmodule LivebookWeb.Hub.Edit.FlyComponent do
|
||||
use LivebookWeb, :live_component
|
||||
|
||||
import Ecto.Changeset, only: [get_field: 2]
|
||||
|
||||
alias Livebook.EctoTypes.HexColor
|
||||
alias Livebook.Hubs.{Fly, FlyClient}
|
||||
|
||||
|
|
@ -63,44 +61,19 @@ defmodule LivebookWeb.Hub.Edit.FlyComponent do
|
|||
phx-debounce="blur"
|
||||
>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div class="flex flex-col space-y-1">
|
||||
<h3 class="text-gray-800 font-semibold">
|
||||
Name
|
||||
</h3>
|
||||
<.input_wrapper form={f} field={:hub_name} class="flex flex-col space-y-1">
|
||||
<div class="input-label">Name</div>
|
||||
<%= text_input(f, :hub_name, class: "input") %>
|
||||
<%= error_tag(f, :hub_name) %>
|
||||
</div>
|
||||
</.input_wrapper>
|
||||
|
||||
<div class="flex flex-col space-y-1">
|
||||
<h3 class="text-gray-800 font-semibold">
|
||||
Color
|
||||
</h3>
|
||||
|
||||
<div class="flex space-x-4 items-center">
|
||||
<div
|
||||
class="border-[3px] rounded-lg p-1 flex justify-center items-center"
|
||||
style={"border-color: #{hub_color(@changeset)}"}
|
||||
>
|
||||
<div class="rounded h-5 w-5" style={"background-color: #{hub_color(@changeset)}"} />
|
||||
</div>
|
||||
<div class="relative grow">
|
||||
<%= text_input(f, :hub_color,
|
||||
class: "input",
|
||||
spellcheck: "false",
|
||||
maxlength: 7
|
||||
) %>
|
||||
<button
|
||||
class="icon-button absolute right-2 top-1"
|
||||
type="button"
|
||||
phx-click="randomize_color"
|
||||
phx-target={@myself}
|
||||
>
|
||||
<.remix_icon icon="refresh-line" class="text-xl" />
|
||||
</button>
|
||||
<%= error_tag(f, :hub_color) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<.input_wrapper form={f} field={:hub_color} class="flex flex-col space-y-1">
|
||||
<div class="input-label">Color</div>
|
||||
<.hex_color_input
|
||||
form={f}
|
||||
field={:hub_color}
|
||||
randomize={JS.push("randomize_color", target: @myself)}
|
||||
/>
|
||||
</.input_wrapper>
|
||||
</div>
|
||||
|
||||
<%= submit("Update Hub",
|
||||
|
|
@ -239,9 +212,7 @@ defmodule LivebookWeb.Hub.Edit.FlyComponent do
|
|||
<%= text_input(f, :key,
|
||||
value: @data["key"],
|
||||
class: "input",
|
||||
placeholder: "environment variable key",
|
||||
autofocus: true,
|
||||
aria_labelledby: "env-var-key",
|
||||
spellcheck: "false"
|
||||
) %>
|
||||
</div>
|
||||
|
|
@ -250,8 +221,6 @@ defmodule LivebookWeb.Hub.Edit.FlyComponent do
|
|||
<%= text_input(f, :value,
|
||||
value: @data["value"],
|
||||
class: "input",
|
||||
placeholder: "environment variable value",
|
||||
aria_labelledby: "env-var-value",
|
||||
spellcheck: "false"
|
||||
) %>
|
||||
</div>
|
||||
|
|
@ -349,6 +318,4 @@ defmodule LivebookWeb.Hub.Edit.FlyComponent do
|
|||
valid? = String.match?(attrs["key"], ~r/^\w+$/) and attrs["value"] not in ["", nil]
|
||||
{:noreply, assign(socket, valid_env_var?: valid?, env_var_data: attrs)}
|
||||
end
|
||||
|
||||
defp hub_color(changeset), do: get_field(changeset, :hub_color)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ defmodule LivebookWeb.Hub.New.FlyComponent do
|
|||
socket
|
||||
|> assign(assigns)
|
||||
|> assign(
|
||||
base: %Fly{},
|
||||
changeset: Fly.change_hub(%Fly{}),
|
||||
selected_app: nil,
|
||||
select_options: [],
|
||||
|
|
@ -33,71 +34,40 @@ defmodule LivebookWeb.Hub.New.FlyComponent do
|
|||
phx-target={@myself}
|
||||
phx-debounce="blur"
|
||||
>
|
||||
<div class="flex flex-col space-y-1">
|
||||
<h3 class="text-gray-800 font-semibold">
|
||||
Access Token
|
||||
</h3>
|
||||
<.input_wrapper form={f} field={:access_token} class="flex flex-col space-y-1">
|
||||
<div class="input-label">Access Token</div>
|
||||
<%= password_input(f, :access_token,
|
||||
phx_change: "fetch_data",
|
||||
phx_debounce: "blur",
|
||||
phx_target: @myself,
|
||||
value: access_token(@changeset),
|
||||
class: "input w-full",
|
||||
class: "input w-full phx-form-error:border-red-300",
|
||||
autofocus: true,
|
||||
spellcheck: "false",
|
||||
autocomplete: "off"
|
||||
) %>
|
||||
<%= error_tag(f, :access_token) %>
|
||||
</div>
|
||||
</.input_wrapper>
|
||||
|
||||
<%= if length(@apps) > 0 do %>
|
||||
<div class="flex flex-col space-y-1">
|
||||
<h3 class="text-gray-800 font-semibold">
|
||||
Application
|
||||
</h3>
|
||||
<.input_wrapper form={f} field={:application_id} class="flex flex-col space-y-1">
|
||||
<div class="input-label">Application</div>
|
||||
<%= select(f, :application_id, @select_options, class: "input") %>
|
||||
<%= error_tag(f, :application_id) %>
|
||||
</div>
|
||||
</.input_wrapper>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div class="flex flex-col space-y-1">
|
||||
<h3 class="text-gray-800 font-semibold">
|
||||
Name
|
||||
</h3>
|
||||
<.input_wrapper form={f} field={:hub_name} class="flex flex-col space-y-1">
|
||||
<div class="input-label">Name</div>
|
||||
<%= text_input(f, :hub_name, class: "input") %>
|
||||
<%= error_tag(f, :hub_name) %>
|
||||
</div>
|
||||
</.input_wrapper>
|
||||
|
||||
<div class="flex flex-col space-y-1">
|
||||
<h3 class="text-gray-800 font-semibold">
|
||||
Color
|
||||
</h3>
|
||||
|
||||
<div class="flex space-x-4 items-center">
|
||||
<div
|
||||
class="border-[3px] rounded-lg p-1 flex justify-center items-center"
|
||||
style={"border-color: #{hub_color(@changeset)}"}
|
||||
>
|
||||
<div class="rounded h-5 w-5" style={"background-color: #{hub_color(@changeset)}"} />
|
||||
</div>
|
||||
<div class="relative grow">
|
||||
<%= text_input(f, :hub_color,
|
||||
class: "input",
|
||||
spellcheck: "false",
|
||||
maxlength: 7
|
||||
) %>
|
||||
<button
|
||||
class="icon-button absolute right-2 top-1"
|
||||
type="button"
|
||||
phx-click="randomize_color"
|
||||
phx-target={@myself}
|
||||
>
|
||||
<.remix_icon icon="refresh-line" class="text-xl" />
|
||||
</button>
|
||||
<%= error_tag(f, :hub_color) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<.input_wrapper form={f} field={:hub_color} class="flex flex-col space-y-1">
|
||||
<div class="input-label">Color</div>
|
||||
<.hex_color_input
|
||||
form={f}
|
||||
field={:hub_color}
|
||||
randomize={JS.push("randomize_color", target: @myself)}
|
||||
/>
|
||||
</.input_wrapper>
|
||||
</div>
|
||||
|
||||
<%= submit("Add Hub",
|
||||
|
|
@ -116,9 +86,11 @@ defmodule LivebookWeb.Hub.New.FlyComponent do
|
|||
case FlyClient.fetch_apps(token) do
|
||||
{:ok, apps} ->
|
||||
opts = select_options(apps)
|
||||
changeset = Fly.change_hub(%Fly{}, %{access_token: token, hub_color: HexColor.random()})
|
||||
base = %Fly{access_token: token, hub_color: HexColor.random()}
|
||||
changeset = Fly.change_hub(base)
|
||||
|
||||
{:noreply, assign(socket, changeset: changeset, select_options: opts, apps: apps)}
|
||||
{:noreply,
|
||||
assign(socket, changeset: changeset, base: base, select_options: opts, apps: apps)}
|
||||
|
||||
{:error, _} ->
|
||||
changeset =
|
||||
|
|
@ -126,7 +98,8 @@ defmodule LivebookWeb.Hub.New.FlyComponent do
|
|||
|> Fly.change_hub(%{access_token: token})
|
||||
|> add_error(:access_token, "is invalid")
|
||||
|
||||
{:noreply, assign(socket, changeset: changeset, select_options: [], apps: [])}
|
||||
{:noreply,
|
||||
assign(socket, changeset: changeset, base: %Fly{}, select_options: [], apps: [])}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -157,15 +130,7 @@ defmodule LivebookWeb.Hub.New.FlyComponent do
|
|||
application_id = params["application_id"]
|
||||
selected_app = Enum.find(socket.assigns.apps, &(&1.application_id == application_id))
|
||||
opts = select_options(socket.assigns.apps, application_id)
|
||||
|
||||
changeset =
|
||||
if selected_app do
|
||||
Fly.change_hub(selected_app, params)
|
||||
else
|
||||
socket.assigns.changeset
|
||||
|> Fly.changeset(params)
|
||||
|> Map.replace!(:action, :validate)
|
||||
end
|
||||
changeset = Fly.change_hub(selected_app || socket.assigns.base, params)
|
||||
|
||||
{:noreply,
|
||||
assign(socket, changeset: changeset, selected_app: selected_app, select_options: opts)}
|
||||
|
|
@ -186,6 +151,5 @@ defmodule LivebookWeb.Hub.New.FlyComponent do
|
|||
[disabled_option] ++ options
|
||||
end
|
||||
|
||||
defp hub_color(changeset), do: get_field(changeset, :hub_color)
|
||||
defp access_token(changeset), do: get_field(changeset, :access_token)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ defmodule LivebookWeb.Hub.NewLive do
|
|||
<%= render_slot(@logo) %>
|
||||
</div>
|
||||
<div class="card-item-body px-6 py-4 rounded-b-2xl grow">
|
||||
<p class="text-gray-800 font-semibold cursor-pointer mt-2 text-sm text-gray-600">
|
||||
<p class="text-gray-800 font-semibold cursor-pointer">
|
||||
<%= @title %>
|
||||
</p>
|
||||
|
||||
|
|
|
|||
|
|
@ -244,10 +244,10 @@ defmodule LivebookWeb.Output do
|
|||
~H"""
|
||||
<div class="-m-4 space-x-4 py-4">
|
||||
<div
|
||||
class="flex items-center justify-between font-editor border-b px-4 pb-4 mb-4"
|
||||
class="flex items-center justify-between border-b px-4 pb-4 mb-4"
|
||||
style="color: var(--ansi-color-red);"
|
||||
>
|
||||
<div class="flex space-x-2">
|
||||
<div class="flex space-x-2 font-editor">
|
||||
<.remix_icon icon="close-circle-line" />
|
||||
<span>Missing secret <%= inspect(@secret_label) %></span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -489,7 +489,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
defp clients_list(assigns) do
|
||||
~H"""
|
||||
<div class="flex flex-col grow">
|
||||
<div class="flex items-center justify-between space-x-4">
|
||||
<div class="flex items-center justify-between space-x-4 -mt-1">
|
||||
<h3 class="uppercase text-sm font-semibold text-gray-500">
|
||||
Users
|
||||
</h3>
|
||||
|
|
@ -498,7 +498,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
<span><%= length(@data_view.clients) %> connected</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col mt-4 space-y-4">
|
||||
<div class="flex flex-col mt-5 space-y-4">
|
||||
<%= for {client_id, user} <- @data_view.clients do %>
|
||||
<div
|
||||
class="flex items-center justify-between space-x-2"
|
||||
|
|
@ -512,7 +512,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
data-el-client-link
|
||||
>
|
||||
<.user_avatar user={user} class="shrink-0 h-7 w-7" text_class="text-xs" />
|
||||
<span><%= user.name || "Anonymous" %></span>
|
||||
<span class="text-left"><%= user.name || "Anonymous" %></span>
|
||||
</button>
|
||||
<%= if client_id != @client_id do %>
|
||||
<span
|
||||
|
|
@ -551,10 +551,13 @@ defmodule LivebookWeb.SessionLive do
|
|||
</h3>
|
||||
<div class="flex flex-col mt-4 space-y-4">
|
||||
<%= for secret <- @data_view.secrets do %>
|
||||
<div class="flex items-center text-gray-500">
|
||||
<span class="flex items-center space-x-1">
|
||||
<div class="flex justify-between items-center text-gray-500">
|
||||
<span class="break-all">
|
||||
<%= secret.label %>
|
||||
</span>
|
||||
<span class="rounded-full bg-gray-200 px-2 text-xs text-gray-600">
|
||||
Session
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
@ -749,7 +752,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
|
||||
def handle_params(params, _url, socket)
|
||||
when socket.assigns.live_action == :secrets do
|
||||
{:noreply, assign(socket, prefill_secret_label: Map.get(params, "secret_label", ""))}
|
||||
{:noreply, assign(socket, prefill_secret_label: params["secret_label"])}
|
||||
end
|
||||
|
||||
def handle_params(_params, _url, socket) do
|
||||
|
|
|
|||
|
|
@ -38,11 +38,11 @@ defmodule LivebookWeb.SessionLive.CodeCellSettingsComponent do
|
|||
checked={@reevaluate_automatically}
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-8 flex justify-end space-x-2">
|
||||
<%= live_patch("Cancel", to: @return_to, class: "button-base button-outlined-gray") %>
|
||||
<div class="mt-8 flex justify-begin space-x-2">
|
||||
<button class="button-base button-blue" type="submit">
|
||||
Save
|
||||
</button>
|
||||
<%= live_patch("Cancel", to: @return_to, class: "button-base button-outlined-gray") %>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -223,17 +223,7 @@ defmodule LivebookWeb.SessionLive.PersistenceLive do
|
|||
Session.save_sync(assigns.session.pid)
|
||||
end
|
||||
|
||||
running_files =
|
||||
[new_attrs.file | assigns.running_files]
|
||||
|> List.delete(attrs.file)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
||||
{:noreply,
|
||||
assign(socket,
|
||||
running_files: running_files,
|
||||
attrs: assigns.new_attrs,
|
||||
saved_file: new_attrs.file
|
||||
)}
|
||||
{:noreply, push_patch(socket, to: Routes.session_path(socket, :page, assigns.session.id))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
|
|||
if socket.assigns[:data] do
|
||||
socket
|
||||
else
|
||||
assign(socket, data: %{"label" => assigns.prefill_secret_label, "value" => ""})
|
||||
assign(socket, data: %{"label" => assigns.prefill_secret_label || "", "value" => ""})
|
||||
end
|
||||
|
||||
{:ok, socket}
|
||||
|
|
@ -32,33 +32,31 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
|
|||
phx-change="validate"
|
||||
autocomplete="off"
|
||||
phx-target={@myself}
|
||||
errors={data_errors(@data)}
|
||||
>
|
||||
<div class="flex flex-col space-y-4">
|
||||
<div>
|
||||
<.input_wrapper form={f} field={:label}>
|
||||
<div class="input-label">
|
||||
Label <span class="text-xs text-gray-500">(alphanumeric and underscore)</span>
|
||||
</div>
|
||||
<%= text_input(f, :label,
|
||||
value: @data["label"],
|
||||
class: "input",
|
||||
placeholder: "secret label",
|
||||
autofocus: true,
|
||||
aria_labelledby: "secret-label",
|
||||
autofocus: !@prefill_secret_label,
|
||||
spellcheck: "false"
|
||||
) %>
|
||||
</div>
|
||||
<div>
|
||||
</.input_wrapper>
|
||||
<.input_wrapper form={f} field={:value}>
|
||||
<div class="input-label">Value</div>
|
||||
<%= text_input(f, :value,
|
||||
value: @data["value"],
|
||||
class: "input",
|
||||
placeholder: "secret value",
|
||||
aria_labelledby: "secret-value",
|
||||
autofocus: !!@prefill_secret_label,
|
||||
spellcheck: "false"
|
||||
) %>
|
||||
</div>
|
||||
</.input_wrapper>
|
||||
<div class="flex space-x-2">
|
||||
<button class="button-base button-blue" type="submit" disabled={not data_valid?(@data)}>
|
||||
<button class="button-base button-blue" type="submit" disabled={f.errors != []}>
|
||||
Save
|
||||
</button>
|
||||
<%= live_patch("Cancel", to: @return_to, class: "button-base button-outlined-gray") %>
|
||||
|
|
@ -71,16 +69,37 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
|
|||
|
||||
@impl true
|
||||
def handle_event("save", %{"data" => data}, socket) do
|
||||
secret = %{label: String.upcase(data["label"]), value: data["value"]}
|
||||
Livebook.Session.put_secret(socket.assigns.session.pid, secret)
|
||||
{:noreply, assign(socket, data: %{"label" => "", "value" => ""})}
|
||||
if data_errors(data) == [] do
|
||||
secret = %{label: String.upcase(data["label"]), value: data["value"]}
|
||||
Livebook.Session.put_secret(socket.assigns.session.pid, secret)
|
||||
{:noreply, push_patch(socket, to: socket.assigns.return_to)}
|
||||
else
|
||||
{:noreply, assign(socket, data: data)}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("validate", %{"data" => data}, socket) do
|
||||
{:noreply, assign(socket, data: data)}
|
||||
end
|
||||
|
||||
defp data_valid?(data) do
|
||||
String.match?(data["label"], ~r/^\w+$/) and data["value"] != ""
|
||||
defp data_errors(data) do
|
||||
Enum.flat_map(data, fn {key, value} ->
|
||||
if error = data_error(key, value) do
|
||||
[{String.to_existing_atom(key), {error, []}}]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp data_error("label", value) do
|
||||
cond do
|
||||
String.match?(value, ~r/^\w+$/) -> nil
|
||||
value == "" -> "can't be blank"
|
||||
true -> "is invalid"
|
||||
end
|
||||
end
|
||||
|
||||
defp data_error("value", ""), do: "can't be blank"
|
||||
defp data_error(_key, _value), do: nil
|
||||
end
|
||||
|
|
|
|||
|
|
@ -35,39 +35,18 @@ defmodule LivebookWeb.UserComponent do
|
|||
phx-hook="UserForm"
|
||||
>
|
||||
<div class="flex flex-col space-y-5">
|
||||
<div>
|
||||
<.input_wrapper form={f} field={:name}>
|
||||
<div class="input-label">Display name</div>
|
||||
<%= text_input(f, :name, class: "input", spellcheck: "false") %>
|
||||
<%= error_tag(f, :name) %>
|
||||
</div>
|
||||
<div>
|
||||
</.input_wrapper>
|
||||
<.input_wrapper form={f} field={:hex_color}>
|
||||
<div class="input-label">Cursor color</div>
|
||||
<div class="flex space-x-4 items-center">
|
||||
<div
|
||||
class="border-[3px] rounded-lg p-1 flex justify-center items-center"
|
||||
style={"border-color: #{hex_color(@changeset)}"}
|
||||
>
|
||||
<div class="rounded h-5 w-5" style={"background-color: #{hex_color(@changeset)}"}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative grow">
|
||||
<%= text_input(f, :hex_color,
|
||||
class: "input",
|
||||
spellcheck: "false",
|
||||
maxlength: 7
|
||||
) %>
|
||||
<button
|
||||
class="icon-button absolute right-2 top-1"
|
||||
type="button"
|
||||
phx-click="randomize_color"
|
||||
phx-target={@myself}
|
||||
>
|
||||
<.remix_icon icon="refresh-line" class="text-xl" />
|
||||
</button>
|
||||
<%= error_tag(f, :hex_color) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<.hex_color_input
|
||||
form={f}
|
||||
field={:hex_color}
|
||||
randomize={JS.push("randomize_color", target: @myself)}
|
||||
/>
|
||||
</.input_wrapper>
|
||||
<button
|
||||
class="button-base button-blue flex space-x-1 justify-center items-center"
|
||||
type="submit"
|
||||
|
|
@ -112,6 +91,4 @@ defmodule LivebookWeb.UserComponent do
|
|||
{:noreply, assign(socket, changeset: changeset, valid?: changeset.valid?)}
|
||||
end
|
||||
end
|
||||
|
||||
defp hex_color(changeset), do: Ecto.Changeset.get_field(changeset, :hex_color)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
defmodule LivebookWeb.ErrorHelpers do
|
||||
@moduledoc """
|
||||
Conveniences for translating and building error messages.
|
||||
"""
|
||||
use Phoenix.HTML
|
||||
|
||||
@doc """
|
||||
Generates tag for inlined form input errors.
|
||||
"""
|
||||
def error_tag(form, field) do
|
||||
for error <- Keyword.get_values(form.errors, field) do
|
||||
content_tag(:span, translate_error(error),
|
||||
class: "invalid-feedback text-red-600",
|
||||
phx_feedback_for: input_name(form, field)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Translates an error message.
|
||||
"""
|
||||
def translate_error({msg, opts}) do
|
||||
# Because the error messages we show in our forms and APIs
|
||||
# are defined inside Ecto, we need to translate them dynamically.
|
||||
Enum.reduce(opts, msg, fn {key, value}, acc ->
|
||||
String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
@ -441,7 +441,6 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
{:ok, view, _} = live(conn, "/sessions/#{session.id}/settings/file")
|
||||
|
||||
assert view = find_live_child(view, "persistence")
|
||||
|
||||
path = Path.join(tmp_dir, "notebook.livemd")
|
||||
|
||||
view
|
||||
|
|
@ -460,6 +459,9 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
|> element(~s{button}, "Save now")
|
||||
|> render_click()
|
||||
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}/settings/file")
|
||||
assert view = find_live_child(view, "persistence")
|
||||
|
||||
view
|
||||
|> element("button", "Change file")
|
||||
|> render_click()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue