6.3 KiB · text 5af55d9
defmodule GitGudWeb.OrgLive.Labels do
@moduledoc """
Org-level labels — auto-inherited by every repo the org owns,
unless that repo opts out. Same shape as the repo labels page but
scoped one level up.
"""
use GitGudWeb, :live_view
alias GitGud.Labels
alias GitGud.Labels.Label
alias GitGud.Organizations
@palette ~w(
#ef4444 #f97316 #f59e0b #eab308 #84cc16 #22c55e #14b8a6 #06b6d4
#3b82f6 #6366f1 #8b5cf6 #a855f7 #ec4899 #f43f5e #64748b #475569
)
@impl true
def mount(%{"handle" => handle}, _session, socket) do
org = Organizations.get_organization_by_handle!(handle)
user = socket.assigns.current_scope && socket.assigns.current_scope.user
cond do
is_nil(user) ->
{:ok,
socket
|> put_flash(:error, "Sign in to manage org labels.")
|> redirect(to: ~p"/users/log-in")}
not Organizations.org_admin?(org, user) ->
{:ok,
socket
|> put_flash(:error, "Org admin only.")
|> redirect(to: ~p"/orgs/#{handle}")}
true ->
{:ok,
socket
|> assign(:org, org)
|> assign(:palette, @palette)
|> assign_labels()
|> assign_form(Label.changeset(%Label{}, default_attrs()))
|> assign(:page_title, "Labels — #{org.handle}")}
end
end
defp default_attrs, do: %{"color" => Enum.random(@palette), "name" => "", "description" => ""}
defp assign_labels(socket) do
assign(socket, :labels, Labels.list_organization_labels(socket.assigns.org))
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
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_organization_label(socket.assigns.org, 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)
{:ok, _} = Labels.delete_label(label)
{:noreply,
socket
|> assign_labels()
|> put_flash(:info, "Label deleted.")}
end
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope}>
<div class="space-y-6">
<.org_settings_header org={@org} current={:labels} />
<p class="text-xs opacity-70">
Labels created here are inherited by every repository the org owns.
Individual repos can opt out from their own labels page.
</p>
<.form
for={@form}
id="org-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>
</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 class="space-y-2">
<li :if={@labels == []} class="opacity-60 text-sm">No org 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>
<button
type="button"
class="btn btn-xs btn-ghost"
phx-click="delete"
phx-value-id={label.id}
data-confirm="Delete this org label? Repos that haven't opted out will lose it."
>
<.icon name="hero-trash" class="size-4" />
</button>
</li>
</ul>
</div>
</Layouts.app>
"""
end
end