mirror of
https://github.com/plausible/analytics.git
synced 2024-12-29 04:22:34 +03:00
2cc80ebd7a
* Extend the Tokens context module * Extract GA Import to separate component * Extract Search Console settings to separate component * Remove Search Console from the router * Stop counting imported pageviews in general settings * Remove search console controller action * Add settings_integrations controller action * Fix remaining redirects * Add Integrations route * Replace SC sidebar item with Integrations * Update site controller tests * Implement Plugins API Tokens LV * Apply universal heroicon to docs info links * Add flash on token creation * Update CHANGELOG * Redirect to integrations upon forgetting GA import * Update moduledocs * Remove unnecessary wildcards * WIP: attempt at fixing broken oauth flow * Fix post-import redirect * Fixup missing attribute * Format * Seed random google auth * Use example.com for seeded e-mails * Tweak Google integrations layout * Remove dangling IO.inspect * Bugfix: copy to clipboard breaking LV form bindings * Update lib/plausible/plugins/api/tokens.ex Co-authored-by: Adrian Gruntkowski <adrian.gruntkowski@gmail.com> * Update lib/plausible_web/controllers/site_controller.ex Co-authored-by: Adrian Gruntkowski <adrian.gruntkowski@gmail.com> * Update lib/plausible_web/live/plugins/api/settings.ex Co-authored-by: Adrian Gruntkowski <adrian.gruntkowski@gmail.com> * Update test/plausible/plugins/api/tokens_test.exs Co-authored-by: Adrian Gruntkowski <adrian.gruntkowski@gmail.com> --------- Co-authored-by: Adrian Gruntkowski <adrian.gruntkowski@gmail.com>
284 lines
7.8 KiB
Elixir
284 lines
7.8 KiB
Elixir
defmodule PlausibleWeb.Live.Components.Form do
|
|
@moduledoc """
|
|
Generic components stolen from mix phx.new templates
|
|
"""
|
|
|
|
use Phoenix.Component
|
|
|
|
@doc """
|
|
Renders an input with label and error messages.
|
|
|
|
A `Phoenix.HTML.FormField` may be passed as argument,
|
|
which is used to retrieve the input name, id, and values.
|
|
Otherwise all attributes may be passed explicitly.
|
|
|
|
## Examples
|
|
|
|
<.input field={@form[:email]} type="email" />
|
|
<.input name="my-input" errors={["oh no!"]} />
|
|
"""
|
|
attr(:id, :any, default: nil)
|
|
attr(:name, :any)
|
|
attr(:label, :string, default: nil)
|
|
attr(:value, :any)
|
|
|
|
attr(:type, :string,
|
|
default: "text",
|
|
values: ~w(checkbox color date datetime-local email file hidden month number password
|
|
range radio search select tel text textarea time url week)
|
|
)
|
|
|
|
attr(:field, Phoenix.HTML.FormField,
|
|
doc: "a form field struct retrieved from the form, for example: @form[:email]"
|
|
)
|
|
|
|
attr(:errors, :list, default: [])
|
|
attr(:checked, :boolean, doc: "the checked flag for checkbox inputs")
|
|
attr(:prompt, :string, default: nil, doc: "the prompt for select inputs")
|
|
attr(:options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2")
|
|
attr(:multiple, :boolean, default: false, doc: "the multiple flag for select inputs")
|
|
|
|
attr(:rest, :global,
|
|
include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
|
|
multiple pattern placeholder readonly required rows size step)
|
|
)
|
|
|
|
slot(:inner_block)
|
|
|
|
def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
|
|
assigns
|
|
|> assign(field: nil, id: assigns.id || field.id)
|
|
|> assign(:errors, Enum.map(field.errors, &translate_error(&1)))
|
|
|> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
|
|
|> assign_new(:value, fn -> field.value end)
|
|
|> input()
|
|
end
|
|
|
|
# All other inputs text, datetime-local, url, password, etc. are handled here...
|
|
def input(assigns) do
|
|
~H"""
|
|
<div phx-feedback-for={@name}>
|
|
<.label :if={@label != nil and @label != ""} for={@id}>
|
|
<%= @label %>
|
|
</.label>
|
|
<input
|
|
type={@type}
|
|
name={@name}
|
|
id={@id}
|
|
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
|
|
{@rest}
|
|
/>
|
|
<%= render_slot(@inner_block) %>
|
|
<.error :for={msg <- @errors}>
|
|
<%= msg %>
|
|
</.error>
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
attr(:rest, :global)
|
|
attr(:id, :string, required: true)
|
|
attr(:class, :string, default: "")
|
|
attr(:name, :string, required: true)
|
|
attr(:label, :string, required: true)
|
|
attr(:value, :string, default: "")
|
|
|
|
def input_with_clipboard(assigns) do
|
|
~H"""
|
|
<div class="my-4">
|
|
<div class="relative mt-1">
|
|
<.input
|
|
id={@id}
|
|
name={@name}
|
|
label={@label}
|
|
value={@value}
|
|
type="text"
|
|
readonly="readonly"
|
|
class={[@class, "pr-20"]}
|
|
{@rest}
|
|
/>
|
|
<a
|
|
onclick={"var input = document.getElementById('#{@id}'); input.focus(); input.select(); document.execCommand('copy'); event.stopPropagation();"}
|
|
href="javascript:void(0)"
|
|
class="absolute flex items-center text-xs font-medium text-indigo-600 no-underline hover:underline"
|
|
style="top: 42px; right: 12px;"
|
|
>
|
|
<Heroicons.document_duplicate class="pr-1 text-indigo-600 dark:text-indigo-500 w-5 h-5" />COPY
|
|
</a>
|
|
</div>
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
attr(:id, :any, default: nil)
|
|
attr(:label, :string, default: nil)
|
|
|
|
attr(:field, Phoenix.HTML.FormField,
|
|
doc: "a form field struct retrieved from the form, for example: @form[:password]",
|
|
required: true
|
|
)
|
|
|
|
attr(:strength, :any)
|
|
|
|
attr(:rest, :global,
|
|
include: ~w(autocomplete disabled form maxlength minlength readonly required size)
|
|
)
|
|
|
|
def password_input_with_strength(%{field: field} = assigns) do
|
|
{too_weak?, errors} =
|
|
case pop_strength_errors(field.errors) do
|
|
{strength_errors, other_errors} when strength_errors != [] ->
|
|
{true, other_errors}
|
|
|
|
{[], other_errors} ->
|
|
{false, other_errors}
|
|
end
|
|
|
|
strength =
|
|
if too_weak? and assigns.strength.score >= 3 do
|
|
%{assigns.strength | score: 2}
|
|
else
|
|
assigns.strength
|
|
end
|
|
|
|
assigns =
|
|
assigns
|
|
|> assign(:too_weak?, too_weak?)
|
|
|> assign(:field, %{field | errors: errors})
|
|
|> assign(:strength, strength)
|
|
|
|
~H"""
|
|
<.input field={@field} type="password" autocomplete="new-password" label={@label} id={@id} {@rest}>
|
|
<.strength_meter :if={@too_weak? or @strength.score > 0} {@strength} />
|
|
</.input>
|
|
"""
|
|
end
|
|
|
|
attr(:minimum, :integer, required: true)
|
|
|
|
attr(:class, :any)
|
|
attr(:ok_class, :any)
|
|
attr(:error_class, :any)
|
|
|
|
attr(:field, Phoenix.HTML.FormField,
|
|
doc: "a form field struct retrieved from the form, for example: @form[:password]",
|
|
required: true
|
|
)
|
|
|
|
def password_length_hint(%{field: field} = assigns) do
|
|
{strength_errors, _} = pop_strength_errors(field.errors)
|
|
|
|
ok_class = assigns[:ok_class] || "text-gray-500"
|
|
error_class = assigns[:error_class] || "text-red-500"
|
|
class = assigns[:class] || ["text-xs", "mt-1"]
|
|
|
|
color =
|
|
if :length in strength_errors do
|
|
error_class
|
|
else
|
|
ok_class
|
|
end
|
|
|
|
final_class = [color | class]
|
|
|
|
assigns = assign(assigns, :class, final_class)
|
|
|
|
~H"""
|
|
<p class={@class}>Min <%= @minimum %> characters</p>
|
|
"""
|
|
end
|
|
|
|
defp pop_strength_errors(errors) do
|
|
Enum.reduce(errors, {[], []}, fn {_, meta} = error, {detected, other_errors} ->
|
|
cond do
|
|
meta[:validation] == :required ->
|
|
{[:required | detected], other_errors}
|
|
|
|
meta[:validation] == :length and meta[:kind] == :min ->
|
|
{[:length | detected], other_errors}
|
|
|
|
meta[:validation] == :strength ->
|
|
{[:strength | detected], other_errors}
|
|
|
|
true ->
|
|
{detected, [error | other_errors]}
|
|
end
|
|
end)
|
|
end
|
|
|
|
attr(:score, :integer, default: 0)
|
|
attr(:warning, :string, default: "")
|
|
attr(:suggestions, :list, default: [])
|
|
|
|
def strength_meter(assigns) do
|
|
color =
|
|
cond do
|
|
assigns.score <= 1 -> ["bg-red-500", "dark:bg-red-500"]
|
|
assigns.score == 2 -> ["bg-red-300", "dark:bg-red-300"]
|
|
assigns.score == 3 -> ["bg-indigo-300", "dark:bg-indigo-300"]
|
|
assigns.score >= 4 -> ["bg-indigo-600", "dark:bg-indigo-500"]
|
|
end
|
|
|
|
feedback =
|
|
cond do
|
|
assigns.warning != "" -> assigns.warning <> "."
|
|
assigns.suggestions != [] -> List.first(assigns.suggestions)
|
|
true -> nil
|
|
end
|
|
|
|
assigns =
|
|
assigns
|
|
|> assign(:color, color)
|
|
|> assign(:feedback, feedback)
|
|
|
|
~H"""
|
|
<div class="w-full bg-gray-200 rounded-full h-1.5 mb-2 mt-2 dark:bg-gray-700 mt-1">
|
|
<div
|
|
class={["h-1.5", "rounded-full"] ++ @color}
|
|
style={["width: " <> to_string(@score * 25) <> "%"]}
|
|
>
|
|
</div>
|
|
</div>
|
|
<p :if={@score <= 2} class="text-sm text-red-500 phx-no-feedback:hidden">
|
|
Password is too weak
|
|
</p>
|
|
<p :if={@feedback} class="text-xs text-gray-500">
|
|
<%= @feedback %>
|
|
</p>
|
|
"""
|
|
end
|
|
|
|
@doc """
|
|
Renders a label.
|
|
"""
|
|
attr(:for, :string, default: nil)
|
|
slot(:inner_block, required: true)
|
|
|
|
def label(assigns) do
|
|
~H"""
|
|
<label for={@for} class="block font-medium dark:text-gray-100">
|
|
<%= render_slot(@inner_block) %>
|
|
</label>
|
|
"""
|
|
end
|
|
|
|
@doc """
|
|
Generates a generic error message.
|
|
"""
|
|
slot(:inner_block, required: true)
|
|
|
|
def error(assigns) do
|
|
~H"""
|
|
<p class="flex gap-3 text-sm leading-6 text-red-500 phx-no-feedback:hidden">
|
|
<%= render_slot(@inner_block) %>
|
|
</p>
|
|
"""
|
|
end
|
|
|
|
def translate_error({msg, opts}) do
|
|
Enum.reduce(opts, msg, fn {key, value}, acc ->
|
|
String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end)
|
|
end)
|
|
end
|
|
end
|