7.7 KiB · text 5af55d9
defmodule GitGudWeb.IssueLive.New do
@moduledoc """
New-issue flow with three improvements over a bare form:
* **Write / Preview tabs** — render the markdown live so the
reporter sees what they're about to file.
* **Label picker** — multi-select against the repo's pickable
labels (own + inherited org labels minus opt-outs).
* **Autofill from query string** — `?title=...&body=...&labels=bug,security`
lets external buttons pre-seed the form.
"""
use GitGudWeb, :live_view
alias GitGud.Issues
alias GitGud.Issues.Issue
alias GitGud.Labels
alias GitGud.Repositories
alias GitGud.Repositories.Storage
@impl true
def mount(%{"owner" => owner, "name" => name} = params, _session, socket) do
repo =
Repositories.get_repository_by_path!(owner, name)
|> GitGud.Repo.preload([:owner, :organization])
labels = Labels.list_labels(repo)
initial = autofill_from_params(params, labels)
{:ok,
socket
|> assign(:repo, repo)
|> assign(:handle, Storage.repo_handle(repo))
|> GitGudWeb.RepoLive.Header.assign_chrome(repo)
|> assign(:labels, labels)
|> assign(:selected_label_ids, initial.label_ids)
|> assign(:mode, :write)
|> assign(:preview_html, "")
|> assign(:body, initial.body)
|> assign_form(Issue.create_changeset(%Issue{}, initial.attrs))
|> assign(:page_title, "New issue")}
end
defp autofill_from_params(params, labels) do
title = params["title"] || ""
body = params["body"] || ""
label_ids =
case params["labels"] do
nil ->
[]
csv ->
wanted = csv |> String.split(",", trim: true) |> Enum.map(&String.trim/1)
labels
|> Enum.filter(&(&1.name in wanted))
|> Enum.map(& &1.id)
end
%{attrs: %{"title" => title, "body" => body}, label_ids: label_ids, body: body}
end
defp assign_form(socket, cs), do: assign(socket, :form, to_form(cs))
@impl true
def handle_event("validate", %{"issue" => attrs} = params, socket) do
cs = %Issue{} |> Issue.create_changeset(attrs) |> Map.put(:action, :validate)
body = attrs["body"] || ""
{:noreply,
socket
|> assign(:body, body)
|> assign_form(cs)
|> update_selection(params)}
end
def handle_event("set_mode", %{"mode" => mode}, socket) when mode in ~w(write preview) do
socket =
if mode == "preview" do
preview = GitGud.Markdown.to_html(socket.assigns.body, repo: socket.assigns.repo)
assign(socket, :preview_html, preview)
else
socket
end
|> assign(:mode, String.to_atom(mode))
{:noreply, socket}
end
def handle_event("toggle_label", %{"id" => id}, socket) do
id = String.to_integer(id)
current = socket.assigns.selected_label_ids
next =
if id in current,
do: Enum.reject(current, &(&1 == id)),
else: [id | current]
{:noreply, assign(socket, :selected_label_ids, next)}
end
def handle_event("save", %{"issue" => attrs}, socket) do
user = socket.assigns.current_scope.user
case Issues.create_issue(socket.assigns.repo, user, attrs) do
{:ok, issue} ->
Enum.each(socket.assigns.selected_label_ids, fn lid ->
_ = Issues.attach_label(issue, Labels.get_label!(lid))
end)
{:noreply,
socket
|> put_flash(:info, "Issue ##{issue.number} created.")
|> push_navigate(
to: ~p"/r/#{socket.assigns.handle}/#{socket.assigns.repo.name}/issues/#{issue.number}"
)}
{:error, cs} ->
{:noreply, assign_form(socket, cs)}
end
end
# Keep the label selection sticky across `validate` events — the
# hidden inputs round-trip as `label_ids[]`.
defp update_selection(socket, %{"label_ids" => ids}) when is_list(ids) do
parsed = ids |> Enum.flat_map(&parse_int/1)
assign(socket, :selected_label_ids, parsed)
end
defp update_selection(socket, _), do: socket
defp parse_int(s) when is_binary(s) do
case Integer.parse(s) do
{n, _} -> [n]
_ -> []
end
end
defp parse_int(_), do: []
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope}>
<div class="space-y-4 max-w-3xl">
<GitGudWeb.RepoLive.Header.header
repo={@repo}
handle={@handle}
current_scope={@current_scope}
has_packages?={@has_packages?}
can_admin?={@can_admin?}
latest_run={@latest_run}
/>
<header>
<p class="text-xs opacity-60">
<.link
navigate={~p"/r/#{@handle}/#{@repo.name}/issues"}
class="link link-hover"
>
Back to issues
</.link>
</p>
<h1 class="text-xl font-semibold">New issue</h1>
</header>
<.form
for={@form}
id="new-issue-form"
phx-change="validate"
phx-submit="save"
class="space-y-4"
>
<.input field={@form[:title]} label="Title" required autofocus />
<div>
<div role="tablist" class="tabs tabs-bordered mb-2">
<button
type="button"
role="tab"
phx-click="set_mode"
phx-value-mode="write"
class={["tab", @mode == :write && "tab-active"]}
>
Write
</button>
<button
type="button"
role="tab"
phx-click="set_mode"
phx-value-mode="preview"
class={["tab", @mode == :preview && "tab-active"]}
>
Preview
</button>
</div>
<%= case @mode do %>
<% :write -> %>
<.input
field={@form[:body]}
label="Description"
type="textarea"
rows="12"
phx-debounce="200"
/>
<p class="text-xs opacity-60 mt-1">
Markdown supported. <code>#42</code>
links to issue or PR <code>#42</code>;
<code>owner/repo#42</code>
links cross-repo.
</p>
<% :preview -> %>
<article class="prose max-w-none border border-base-300 rounded p-4 min-h-32">
<%= if @body == "" do %>
<p class="opacity-50 not-prose text-sm">Nothing to preview yet.</p>
<% else %>
{Phoenix.HTML.raw(@preview_html)}
<% end %>
</article>
<% end %>
</div>
<section :if={@labels != []}>
<p class="text-sm font-semibold mb-1">Labels</p>
<div class="flex flex-wrap gap-2">
<button
:for={label <- @labels}
type="button"
phx-click="toggle_label"
phx-value-id={label.id}
class={[
"badge cursor-pointer",
label.id not in @selected_label_ids && "opacity-40"
]}
style={"background-color: " <> label.color <> "; color: white"}
title={label.description || label.name}
>
{label.name}
<span :if={Map.get(label, :inherited?, false)} class="ml-1 opacity-70">(org)</span>
</button>
</div>
<input
:for={id <- @selected_label_ids}
type="hidden"
name="label_ids[]"
value={id}
/>
</section>
<button type="submit" class="btn btn-primary">Submit</button>
</.form>
</div>
</Layouts.app>
"""
end
end