neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
Issues: only the author or a triager closes and reopens from the web
9af3f43 · Gabriel Morell · 2026-10-18 18:08
Message
{commit_body(@commit)}
Files changed
modified
lib/git_gud_web/live/issue_live/show.ex
+22
−5
@@ -51,6 +51,16 @@ defmodule GitGudWeb.IssueLive.Show do
| 51 | 51 | |
| 52 | 52 | defp can_edit?(_assigns), do: false |
| 53 | 53 | |
| 54 | + # Closing and reopening: the author, or anyone with triage access on | |
| 55 | + # the repository (which an admin has) — the rule the mobile API's | |
| 56 | + # `set_state` applies too. | |
| 57 | + defp can_change_state?(%{current_scope: %{user: %{id: uid} = user}} = assigns), | |
| 58 | + do: | |
| 59 | + assigns.issue.author_id == uid or | |
| 60 | + GitGud.Organizations.can?(user, assigns.repo, :triage) == true | |
| 61 | + | |
| 62 | + defp can_change_state?(_assigns), do: false | |
| 63 | + | |
| 54 | 64 | # Same rule as the title: the comment's author, or a repo admin. A |
| 55 | 65 | # deleted comment is nobody's to edit. |
| 56 | 66 | defp can_edit_comment?(%{current_scope: %{user: %{id: uid}}} = assigns, comment) do |
@@ -67,13 +77,18 @@ defmodule GitGudWeb.IssueLive.Show do
| 67 | 77 | @impl true |
| 68 | 78 | def handle_event("toggle_state", _params, socket) do |
| 69 | 79 | issue = socket.assigns.issue |
| 70 | − new_state = if issue.state == "open", do: "closed", else: "open" | |
| 71 | − {:ok, _} = Issues.set_state(issue, new_state) | |
| 72 | 80 | |
| 73 | − kind = if new_state == "closed", do: "closed", else: "reopened" | |
| 74 | − :ok = Events.record(issue, kind, viewer(socket)) | |
| 81 | + if can_change_state?(socket.assigns) do | |
| 82 | + new_state = if issue.state == "open", do: "closed", else: "open" | |
| 83 | + {:ok, _} = Issues.set_state(issue, new_state) | |
| 84 | + | |
| 85 | + kind = if new_state == "closed", do: "closed", else: "reopened" | |
| 86 | + :ok = Events.record(issue, kind, viewer(socket)) | |
| 75 | 87 | |
| 76 | − {:noreply, reload(socket)} | |
| 88 | + {:noreply, reload(socket)} | |
| 89 | + else | |
| 90 | + {:noreply, socket} | |
| 91 | + end | |
| 77 | 92 | end |
| 78 | 93 | |
| 79 | 94 | def handle_event("edit_title", _params, socket) do |
@@ -577,6 +592,8 @@ defmodule GitGudWeb.IssueLive.Show do
| 577 | 592 | >{@comment_body}</textarea> |
| 578 | 593 | <div class="flex justify-end gap-2"> |
| 579 | 594 | <button |
| 595 | + :if={can_change_state?(assigns)} | |
| 596 | + id="issue-toggle-state" | |
| 580 | 597 | type="button" |
| 581 | 598 | phx-click="toggle_state" |
| 582 | 599 | class="btn btn-sm btn-ghost" |
added
test/git_gud_web/live/issue_live/state_toggle_test.exs
+72
−0
@@ -0,0 +1,72 @@
| 1 | +defmodule GitGudWeb.IssueLive.StateToggleTest do | |
| 2 | + @moduledoc """ | |
| 3 | + Closing and reopening an issue from its page is for its author or | |
| 4 | + someone with triage access — the mobile API's rule. Anyone else, signed | |
| 5 | + in or not, sees no button, and pushing the event by hand changes | |
| 6 | + nothing. | |
| 7 | + """ | |
| 8 | + | |
| 9 | + use GitGudWeb.ConnCase, async: false | |
| 10 | + | |
| 11 | + import Phoenix.LiveViewTest | |
| 12 | + import GitGud.AccountsFixtures | |
| 13 | + import GitGud.ForgeFixtures | |
| 14 | + | |
| 15 | + alias GitGud.Issues | |
| 16 | + alias GitGud.Repo | |
| 17 | + alias GitGud.Repositories | |
| 18 | + | |
| 19 | + defp handle(repo), | |
| 20 | + do: Repositories.Storage.repo_handle(Repo.preload(repo, [:owner, :organization])) | |
| 21 | + | |
| 22 | + defp issue_path(repo, issue), do: ~p"/r/#{handle(repo)}/#{repo.name}/issues/#{issue.number}" | |
| 23 | + defp state(repo, issue), do: Issues.get_issue!(repo, issue.number).state | |
| 24 | + | |
| 25 | + setup do | |
| 26 | + {owner, repo} = repository_fixture(%{visibility: "public"}) | |
| 27 | + author = user_fixture() | |
| 28 | + issue = issue_fixture(repo, author) | |
| 29 | + %{owner: owner, repo: repo, author: author, issue: issue} | |
| 30 | + end | |
| 31 | + | |
| 32 | + test "the author closes and reopens their issue", %{conn: conn} = ctx do | |
| 33 | + {:ok, lv, _} = live(log_in_user(conn, ctx.author), issue_path(ctx.repo, ctx.issue)) | |
| 34 | + | |
| 35 | + lv |> element("#issue-toggle-state") |> render_click() | |
| 36 | + assert state(ctx.repo, ctx.issue) == "closed" | |
| 37 | + | |
| 38 | + lv |> element("#issue-toggle-state") |> render_click() | |
| 39 | + assert state(ctx.repo, ctx.issue) == "open" | |
| 40 | + end | |
| 41 | + | |
| 42 | + test "a repository admin closes someone else's issue", %{conn: conn} = ctx do | |
| 43 | + {:ok, lv, _} = live(log_in_user(conn, ctx.owner), issue_path(ctx.repo, ctx.issue)) | |
| 44 | + | |
| 45 | + lv |> element("#issue-toggle-state") |> render_click() | |
| 46 | + assert state(ctx.repo, ctx.issue) == "closed" | |
| 47 | + end | |
| 48 | + | |
| 49 | + test "another signed-in user gets no button, and the event does nothing", %{conn: conn} = ctx do | |
| 50 | + {:ok, lv, _} = live(log_in_user(conn, user_fixture()), issue_path(ctx.repo, ctx.issue)) | |
| 51 | + | |
| 52 | + refute has_element?(lv, "#issue-toggle-state") | |
| 53 | + _ = render_hook(lv, "toggle_state", %{}) | |
| 54 | + assert state(ctx.repo, ctx.issue) == "open" | |
| 55 | + end | |
| 56 | + | |
| 57 | + test "a reopen by hand is refused too", %{conn: conn} = ctx do | |
| 58 | + {:ok, _} = Issues.set_state(ctx.issue, "closed") | |
| 59 | + {:ok, lv, _} = live(log_in_user(conn, user_fixture()), issue_path(ctx.repo, ctx.issue)) | |
| 60 | + | |
| 61 | + _ = render_hook(lv, "toggle_state", %{}) | |
| 62 | + assert state(ctx.repo, ctx.issue) == "closed" | |
| 63 | + end | |
| 64 | + | |
| 65 | + test "a visitor who isn't signed in can't close it", %{conn: conn} = ctx do | |
| 66 | + {:ok, lv, _} = live(conn, issue_path(ctx.repo, ctx.issue)) | |
| 67 | + | |
| 68 | + refute has_element?(lv, "#issue-toggle-state") | |
| 69 | + _ = render_hook(lv, "toggle_state", %{}) | |
| 70 | + assert state(ctx.repo, ctx.issue) == "open" | |
| 71 | + end | |
| 72 | +end |
Parents: 8975744