neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
New repository from an org preselects it; org packages tabs as paths
59086ac · Gabriel Morell · 2026-09-29 18:48
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