defmodule GitGudWeb.LabelLive.Index do
use GitGudWeb, :live_view
alias GitGud.Labels
alias GitGud.Labels.Label
alias GitGud.Repositories
alias GitGud.Repositories.Storage
@palette ~w(
#ef4444 #f97316 #f59e0b #eab308 #84cc16 #22c55e #14b8a6 #06b6d4
#3b82f6 #6366f1 #8b5cf6 #a855f7 #ec4899 #f43f5e #64748b #475569
)
@impl true
def mount(%{"owner" => owner, "name" => name}, _session, socket) do
repo =
Repositories.get_repository_by_path!(owner, name)
|> GitGud.Repo.preload([:owner, :organization])
{:ok,
socket
|> assign(:repo, repo)
|> assign(:handle, Storage.repo_handle(repo))
|> GitGudWeb.RepoLive.Header.assign_chrome(repo)
|> assign(:palette, @palette)
|> assign_labels()
|> assign_form(Label.changeset(%Label{}, default_attrs()))
|> assign(:page_title, "Labels — #{owner}/#{name}")}
end
defp default_attrs, do: %{"color" => Enum.random(@palette), "name" => "", "description" => ""}
defp assign_labels(socket) do
socket
|> assign(:labels, Labels.list_labels(socket.assigns.repo))
|> assign(:hidden_labels, Labels.list_opted_out_labels(socket.assigns.repo))
end
defp assign_form(socket, cs), do: assign(socket, :form, to_form(cs))
@impl true
def handle_event("validate", %{"label" => attrs}, socket) do
cs = %Label{} |> Label.changeset(attrs) |> Map.put(:action, :validate)
{:noreply, assign_form(socket, cs)}
end
def handle_event("pick_color", %{"color" => color}, socket) do
# Merge into the in-flight form params so the form reflects the
# swatch choice immediately + survives a subsequent validate.
current =
socket.assigns.form.source
|> Ecto.Changeset.apply_changes()
|> Map.from_struct()
|> Map.take([:name, :description])
|> Map.new(fn {k, v} -> {Atom.to_string(k), v || ""} end)
cs =
%Label{}
|> Label.changeset(Map.put(current, "color", color))
|> Map.put(:action, :validate)
{:noreply, assign_form(socket, cs)}
end
def handle_event("save", %{"label" => attrs}, socket) do
case Labels.create_label(socket.assigns.repo, attrs) do
{:ok, _label} ->
{:noreply,
socket
|> assign_labels()
|> assign_form(Label.changeset(%Label{}, default_attrs()))
|> put_flash(:info, "Label added.")}
{:error, cs} ->
{:noreply, assign_form(socket, cs)}
end
end
def handle_event("delete", %{"id" => id}, socket) do
label = Labels.get_label!(id)
# Inherited org labels can't be deleted from the repo view — use
# opt-out instead. Belt-and-braces gate against a forged event.
cond do
Label.org_label?(label) ->
{:noreply,
put_flash(socket, :error, "Org labels can only be edited at the org level. Opt out to hide.")}
true ->
{:ok, _} = Labels.delete_label(label)
{:noreply,
socket
|> assign_labels()
|> put_flash(:info, "Label deleted.")}
end
end
def handle_event("opt_out", %{"id" => id}, socket) do
label = Labels.get_label!(id)
_ = Labels.opt_out(socket.assigns.repo, label)
{:noreply,
socket
|> assign_labels()
|> put_flash(:info, "Hidden from this repo.")}
end
def handle_event("opt_in", %{"id" => id}, socket) do
label = Labels.get_label!(id)
{:ok, _} = Labels.opt_in(socket.assigns.repo, label)
{:noreply,
socket
|> assign_labels()
|> put_flash(:info, "Re-enabled for this repo.")}
end
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope}>
<div class="space-y-6">
<GitGudWeb.RepoLive.Header.header
repo={@repo}
handle={@handle}
current_scope={@current_scope}
has_packages?={@has_packages?}
can_admin?={@can_admin?}
latest_run={@latest_run}
/>
<h1 class="text-xl font-semibold">Labels</h1>
<.form
for={@form}
id="label-form"
phx-change="validate"
phx-submit="save"
class="space-y-3 border border-base-300 rounded p-4"
>
<div class="flex items-end gap-3 flex-wrap">
<.input field={@form[:name]} label="Name" required />
<.input field={@form[:description]} label="Description" />
<div class="form-control">
<label class="label py-0 text-xs opacity-70">Color</label>
<div class="flex items-center gap-2">
<input
type="color"
name={@form[:color].name}
value={@form[:color].value || "#3b82f6"}
class="w-10 h-10 rounded cursor-pointer border-0 bg-transparent p-0"
/>
<input
type="text"
name={@form[:color].name}
value={@form[:color].value}
class="input input-sm input-bordered w-24 font-mono"
placeholder="#3b82f6"
pattern="^#[0-9a-fA-F]{6}$"
/>
<span
class="badge"
style={"background-color: " <> (@form[:color].value || "#3b82f6") <> "; color: white"}
>
{if @form[:name].value && @form[:name].value != "",
do: @form[:name].value,
else: "preview"}
</span>
</div>
<p :for={msg <- Enum.map(@form[:color].errors, &translate_error/1)} class="text-error text-xs mt-1">
{msg}
</p>
</div>
<button type="submit" class="btn btn-primary">Add</button>
</div>
<div>
<p class="text-xs opacity-70 mb-1">Pick from the palette:</p>
<div class="flex flex-wrap gap-1">
<button
:for={swatch <- @palette}
type="button"
phx-click="pick_color"
phx-value-color={swatch}
style={"background-color: " <> swatch}
class="w-6 h-6 rounded border border-base-300 hover:scale-110 transition-transform"
title={swatch}
/>
</div>
</div>
</.form>
<ul id="labels" class="space-y-2">
<li :if={@labels == []} class="opacity-60 text-sm">No labels yet.</li>
<li
:for={label <- @labels}
class="flex items-center gap-2 border border-base-200 rounded px-2 py-1"
>
<span
class="badge"
style={"background-color: " <> label.color <> "; color: white"}
>
{label.name}
</span>
<span class="text-sm opacity-70 flex-1">{label.description}</span>
<span
:if={Map.get(label, :inherited?, false)}
class="badge badge-xs badge-info"
title="Inherited from this repo's organization"
>
inherited
</span>
<%= cond do %>
<% Map.get(label, :inherited?, false) -> %>
<button
type="button"
class="btn btn-xs"
phx-click="opt_out"
phx-value-id={label.id}
data-confirm="Hide this org label from this repo? You can re-enable it later."
>
Hide
</button>
<% true -> %>
<button
type="button"
class="btn btn-xs btn-ghost"
phx-click="delete"
phx-value-id={label.id}
data-confirm="Delete this label?"
>
<.icon name="hero-trash" class="size-4" />
</button>
<% end %>
</li>
</ul>
<details :if={@hidden_labels != []} class="border border-base-300 rounded">
<summary class="cursor-pointer px-3 py-2 text-sm flex items-center gap-2 hover:bg-base-200">
<.icon name="hero-eye-slash" class="size-4 opacity-60" />
Hidden org labels
<span class="badge badge-xs opacity-70">{length(@hidden_labels)}</span>
</summary>
<ul class="divide-y divide-base-300">
<li
:for={label <- @hidden_labels}
class="flex items-center gap-2 px-3 py-2"
>
<span
class="badge opacity-60"
style={"background-color: " <> label.color <> "; color: white"}
>
{label.name}
</span>
<span class="text-sm opacity-70 flex-1">{label.description}</span>
<span class="badge badge-xs badge-ghost" title="From this repo's organization">
org
</span>
<button
type="button"
class="btn btn-xs"
phx-click="opt_in"
phx-value-id={label.id}
>
Unhide
</button>
</li>
</ul>
</details>
</div>
</Layouts.app>
"""
end
end
9.1 KiB · text
5af55d9