New repository from an org preselects it; org packages tabs as paths

59086ac · Gabriel Morell · 2026-09-29 18:48

18 files +82 -17
Message
{commit_body(@commit)}

Files changed

modified lib/git_gud_web/live/org_live/dashboard.ex
+1 −1
@@ -150,7 +150,7 @@ defmodule GitGudWeb.OrgLive.Dashboard do
150 150 <.link
151 151 :if={@is_admin?}
152 152 id="org-new-repo"
153 − navigate={~p"/repositories/new"}
153 + navigate={~p"/repositories/new/#{@org.handle}"}
154 154 class="btn btn-sm btn-primary"
155 155 >
156 156 <.icon name="hero-plus-micro" class="size-4" /> New repository
modified lib/git_gud_web/live/org_live/packages.ex
+10 −6
@@ -16,7 +16,8 @@ defmodule GitGudWeb.OrgLive.Packages do
16 16 cards for what's published, with icons and descriptions from
17 17 `GitGud.Packages.About`; **Registries** — the namespace-wide URL per
18 18 format and what each offers; **Storage** — what it all costs. The tab
19 − and the catalog's format filter live in the URL, so a link to "our
19 + and the catalog's format filter are path segments
20 + (`/packages/storage`, `/packages/catalog/fdroid`), so a link to "our
20 21 Android apps" is a link.
21 22
22 23 Read-only on purpose. Retention is set per package (on the
@@ -125,9 +126,12 @@ defmodule GitGudWeb.OrgLive.Packages do
125 126 |> assign(:cards, cards)
126 127 end
127 128
128 − defp tab_path(org, tab, format \\ nil),
129 − do:
130 − ~p"/orgs/#{org.handle}/packages?#{Enum.reject([tab: tab, format: format], &is_nil(elem(&1, 1)))}"
129 + # Deep links: `/packages/registries`, `/packages/catalog/fdroid`. The
130 + # catalog is the bare path, so it has no segment of its own until a
131 + # format narrows it.
132 + defp tab_path(org, "catalog", nil), do: ~p"/orgs/#{org.handle}/packages"
133 + defp tab_path(org, "catalog", format), do: ~p"/orgs/#{org.handle}/packages/catalog/#{format}"
134 + defp tab_path(org, tab, _format), do: ~p"/orgs/#{org.handle}/packages/#{tab}"
131 135
132 136 defp visible_cards(cards, nil), do: cards
133 137 defp visible_cards(cards, format), do: Enum.filter(cards, &(&1.package.format == format))
@@ -240,7 +244,7 @@ defmodule GitGudWeb.OrgLive.Packages do
240 244 <.link
241 245 :for={{id, label} <- @tabs}
242 246 id={"org-packages-tab-#{id}"}
243 − patch={tab_path(@org, id)}
247 + patch={tab_path(@org, id, nil)}
244 248 role="tab"
245 249 aria-selected={to_string(@tab == id)}
246 250 class={[
@@ -262,7 +266,7 @@ defmodule GitGudWeb.OrgLive.Packages do
262 266 <section :if={@tab == "catalog"} id="org-packages-catalog" class="space-y-4">
263 267 <div :if={map_size(@catalog) > 1} class="flex flex-wrap gap-2" id="catalog-format-filter">
264 268 <.link
265 − patch={tab_path(@org, "catalog")}
269 + patch={tab_path(@org, "catalog", nil)}
266 270 class={[
267 271 "px-2.5 py-1 rounded-full text-xs border transition-colors duration-150",
268 272 if(is_nil(@format_filter),
modified lib/git_gud_web/live/org_live/show.ex
+1 −1
@@ -185,7 +185,7 @@ defmodule GitGudWeb.OrgLive.Show do
185 185 </div>
186 186 <.link
187 187 :if={@is_admin?}
188 − navigate={~p"/repositories/new"}
188 + navigate={~p"/repositories/new/#{@org.handle}"}
189 189 class="btn btn-sm btn-primary"
190 190 >
191 191 New repo
modified lib/git_gud_web/live/repo_live/new.ex
+12 −2
@@ -7,7 +7,7 @@ defmodule GitGudWeb.RepoLive.New do
7 7 alias GitGud.Repositories.Storage
8 8
9 9 @impl true
10 − def mount(_params, _session, socket) do
10 + def mount(params, _session, socket) do
11 11 user = socket.assigns.current_scope.user
12 12 orgs = Organizations.list_organizations_for(user) |> admin_only(user)
13 13
@@ -16,10 +16,20 @@ defmodule GitGudWeb.RepoLive.New do
16 16 |> assign(:page_title, "New repository")
17 17 |> assign(:user, user)
18 18 |> assign(:orgs, orgs)
19 − |> assign(:namespace_choice, "user")
19 + |> assign(:namespace_choice, initial_namespace(params["org"], orgs))
20 20 |> assign_form(Repository.create_changeset(%Repository{}, %{}))}
21 21 end
22 22
23 + # `/repositories/new/<org>` — "New repository" from an org's page starts with
24 + # that org as the owner. Only an org this user may create in; any
25 + # other handle falls back to the user rather than offering an option
26 + # the select doesn't have.
27 + defp initial_namespace(handle, orgs) when is_binary(handle) do
28 + if Enum.any?(orgs, &(&1.handle == handle)), do: "org:" <> handle, else: "user"
29 + end
30 +
31 + defp initial_namespace(_handle, _orgs), do: "user"
32 +
23 33 defp admin_only(orgs, user),
24 34 do: Enum.filter(orgs, &Organizations.org_admin?(&1, user))
25 35
modified lib/git_gud_web/router.ex
+5 −0
@@ -192,6 +192,9 @@ defmodule GitGudWeb.Router do
192 192 {GitGudWeb.RepoAuth, :require_read}
193 193 ] do
194 194 live "/repositories/new", RepoLive.New, :new
195 + # The same form with that org preselected as owner — what an org's
196 + # "New repository" button links to.
197 + live "/repositories/new/:org", RepoLive.New, :new
195 198 live "/repositories/import", ImportLive.New, :new
196 199 live "/imports", ImportLive.Index, :index
197 200 live "/imports/:id", ImportLive.Show, :show
@@ -219,6 +222,8 @@ defmodule GitGudWeb.Router do
219 222 live "/orgs/:handle/settings/runners", OrgLive.SettingsRunners, :index
220 223 live "/orgs/:handle/settings/webhooks", WebhookLive.Org, :index
221 224 live "/orgs/:handle/packages", OrgLive.Packages, :index
225 + live "/orgs/:handle/packages/:tab", OrgLive.Packages, :index
226 + live "/orgs/:handle/packages/catalog/:format", OrgLive.Packages, :index
222 227 live "/orgs/:handle/settings/labels", OrgLive.Labels, :index
223 228 live "/orgs/:handle/settings/moderation", OrgLive.Moderation, :index
224 229 live "/users/ci", UserLive.Ci, :index
modified test/git_gud_web/live/org_packages_catalog_test.exs
+5 −5
@@ -111,7 +111,7 @@ defmodule GitGudWeb.OrgPackagesCatalogTest do
111 111 org: org
112 112 } do
113 113 conn = log_in_user(conn, owner)
114 − {:ok, _view, html} = live(conn, ~p"/orgs/#{org.handle}/packages?tab=registries")
114 + {:ok, _view, html} = live(conn, ~p"/orgs/#{org.handle}/packages/registries")
115 115
116 116 assert html =~ "What consumers see"
117 117 assert html =~ "/pkg/deb/#{org.handle}/_all"
@@ -123,7 +123,7 @@ defmodule GitGudWeb.OrgPackagesCatalogTest do
123 123 stranger = user_fixture()
124 124
125 125 {:ok, _view, html} =
126 − live(log_in_user(conn, stranger), ~p"/orgs/#{org.handle}/packages?tab=registries")
126 + live(log_in_user(conn, stranger), ~p"/orgs/#{org.handle}/packages/registries")
127 127
128 128 assert html =~ "public-tool"
129 129 refute html =~ "private-tool"
@@ -131,7 +131,7 @@ defmodule GitGudWeb.OrgPackagesCatalogTest do
131 131
132 132 test "says whose view it is", %{conn: conn, owner: owner, org: org} do
133 133 {:ok, _view, html} =
134 − live(log_in_user(conn, owner), ~p"/orgs/#{org.handle}/packages?tab=registries")
134 + live(log_in_user(conn, owner), ~p"/orgs/#{org.handle}/packages/registries")
135 135
136 136 # Without this the listing reads as "what exists", and someone
137 137 # sharing a screenshot promises access they can't grant.
@@ -147,7 +147,7 @@ defmodule GitGudWeb.OrgPackagesCatalogTest do
147 147 org: org
148 148 } do
149 149 {:ok, _view, html} =
150 − live(log_in_user(conn, owner), ~p"/orgs/#{org.handle}/packages?tab=registries")
150 + live(log_in_user(conn, owner), ~p"/orgs/#{org.handle}/packages/registries")
151 151
152 152 # Two deb packages, one of them from a private repository. An
153 153 # owner seeing both with nothing distinguishing them is how "it
@@ -174,7 +174,7 @@ defmodule GitGudWeb.OrgPackagesCatalogTest do
174 174 })
175 175
176 176 {:ok, _view, html} =
177 − live(log_in_user(conn, owner), ~p"/orgs/#{org.handle}/packages?tab=registries")
177 + live(log_in_user(conn, owner), ~p"/orgs/#{org.handle}/packages/registries")
178 178
179 179 # F-Droid rewrites an address that doesn't end in /repo, so
180 180 # printing the bare one here would hand out a URL that 404s.
modified test/git_gud_web/live/package_about_ui_test.exs
+1 −1
@@ -133,7 +133,7 @@ defmodule GitGudWeb.PackageAboutUiTest do
133 133
134 134 test "filters by format through the URL", ctx do
135 135 conn = log_in_user(ctx.conn, ctx.owner)
136 − {:ok, lv, _html} = live(conn, ~p"/orgs/#{ctx.org.handle}/packages?format=deb")
136 + {:ok, lv, _html} = live(conn, ~p"/orgs/#{ctx.org.handle}/packages/catalog/deb")
137 137
138 138 assert has_element?(lv, "#package-card-#{ctx.server.id}")
139 139 refute has_element?(lv, "#package-card-#{ctx.app.id}")
modified test/git_gud_web/live/package_retention_ui_test.exs
+1 −1
@@ -146,7 +146,7 @@ defmodule GitGudWeb.PackageRetentionUiTest do
146 146 end
147 147
148 148 test "totals an organization's registries", %{conn: conn, org: org} do
149 − {:ok, lv, _html} = live(conn, ~p"/orgs/#{org.handle}/packages?tab=storage")
149 + {:ok, lv, _html} = live(conn, ~p"/orgs/#{org.handle}/packages/storage")
150 150
151 151 html = render_async(lv)
152 152
added test/git_gud_web/live/repo_live/new_owner_default_test.exs
+46 −0
@@ -0,0 +1,46 @@
1 +defmodule GitGudWeb.RepoLive.NewOwnerDefaultTest do
2 + @moduledoc """
3 + "New repository" from an org's page starts with that org as owner.
4 + """
5 + use GitGudWeb.ConnCase, async: false
6 +
7 + import Phoenix.LiveViewTest
8 + import GitGud.AccountsFixtures
9 +
10 + alias GitGud.Organizations
11 +
12 + setup :register_and_log_in_user
13 +
14 + setup %{user: user} do
15 + handle = "acme#{System.unique_integer([:positive])}"
16 + {:ok, org} = Organizations.create_organization(user, %{"handle" => handle, "name" => "Acme"})
17 + %{org: org}
18 + end
19 +
20 + test "the org page links with the org, and the form selects it", %{conn: conn, org: org} do
21 + {:ok, view, _html} = live(conn, ~p"/orgs/#{org.handle}")
22 + assert has_element?(view, ~s{a[href="/repositories/new/#{org.handle}"]})
23 +
24 + {:ok, view, _html} = live(conn, ~p"/repositories/new/#{org.handle}")
25 +
26 + assert has_element?(
27 + view,
28 + ~s{select[name="namespace"] option[value="org:#{org.handle}"][selected]}
29 + )
30 + end
31 +
32 + test "an org the user can't create in falls back to the user", %{conn: conn} do
33 + other = user_fixture()
34 + handle = "other#{System.unique_integer([:positive])}"
35 + {:ok, _} = Organizations.create_organization(other, %{"handle" => handle, "name" => "Other"})
36 +
37 + {:ok, view, _html} = live(conn, ~p"/repositories/new/#{handle}")
38 + assert has_element?(view, ~s{select[name="namespace"] option[value="user"][selected]})
39 + refute has_element?(view, ~s{option[value="org:#{handle}"]})
40 + end
41 +
42 + test "with no org, the user is the owner", %{conn: conn} do
43 + {:ok, view, _html} = live(conn, ~p"/repositories/new")
44 + assert has_element?(view, ~s{select[name="namespace"] option[value="user"][selected]})
45 + end
46 +end

Parents: 4255c3f