From b2adcdf9691b33499d1afe66d9a1ff4a7c25069a Mon Sep 17 00:00:00 2001 From: logikonline Date: Sat, 7 Feb 2026 15:02:16 -0500 Subject: [PATCH] feat(packages): add bulk visibility management for packages Add ability to bulk set packages as private or public in both admin and repository package views. Includes new bulk action buttons, visibility grouping in repository view, and corresponding backend handlers for processing visibility changes. Admin can manage all packages while repository owners can manage their own packages. --- options/locale/locale_en-US.json | 15 ++ routers/web/admin/packages.go | 35 +++++ routers/web/repo/packages.go | 46 +++++- routers/web/web.go | 2 + templates/admin/packages/list.tmpl | 21 ++- templates/package/shared/list.tmpl | 219 +++++++++++++++++++++++++---- 6 files changed, 307 insertions(+), 31 deletions(-) diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 328f56407b..968444cd0e 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -3322,6 +3322,13 @@ "admin.packages.bulk.global.partial": "Enabled global access for %d package(s), %d failed (may already exist as global)", "admin.packages.bulk.automatch.success": "Auto-matched %d package(s) to repositories", "admin.packages.bulk.automatch.none": "No matching repositories found for selected packages", + "admin.packages.bulk.make_private": "Make Private", + "admin.packages.bulk.make_public": "Make Public", + "admin.packages.bulk.private.enabled": "Made %d package(s) private", + "admin.packages.bulk.private.disabled": "Made %d package(s) public", + "admin.packages.visibility": "Visibility", + "admin.packages.visibility.private": "Private", + "admin.packages.visibility.public": "Public", "admin.packages.automatch.button": "Find matching repository", "admin.packages.automatch.match": "Match", "admin.packages.automatch.success": "Package linked to matching repository", @@ -3726,6 +3733,14 @@ "packages.no_metadata": "No metadata.", "packages.empty.documentation": "For more information on the package registry, see the documentation.", "packages.empty.repo": "Did you upload a package, but it's not shown here? Go to package settings and link it to this repo.", + "packages.visibility.public": "Public Packages", + "packages.visibility.private": "Private Packages", + "packages.bulk.actions": "Bulk Actions", + "packages.bulk.make_private": "Make Private", + "packages.bulk.make_public": "Make Public", + "packages.bulk.selected": "Selected:", + "packages.bulk.select_all": "Select all", + "packages.bulk.no_selection": "Please select at least one package.", "packages.registry.documentation": "For more information on the %s registry, see the documentation.", "packages.filter.type": "Type", "packages.filter.type.all": "All", diff --git a/routers/web/admin/packages.go b/routers/web/admin/packages.go index 240119f58d..0d1011d16a 100644 --- a/routers/web/admin/packages.go +++ b/routers/web/admin/packages.go @@ -177,6 +177,41 @@ func BulkAutoMatch(ctx *context.Context) { ctx.JSONRedirect(setting.AppSubURL + "/-/admin/packages") } +// BulkSetPrivate sets/unsets private flag on multiple packages +func BulkSetPrivate(ctx *context.Context) { + packageIDs := ctx.FormStrings("ids[]") + isPrivate := ctx.FormBool("is_private") + + ids := make([]int64, 0, len(packageIDs)) + for _, idStr := range packageIDs { + var id int64 + if _, err := fmt.Sscanf(idStr, "%d", &id); err == nil && id > 0 { + ids = append(ids, id) + } + } + + if len(ids) == 0 { + ctx.Flash.Error(ctx.Tr("admin.packages.bulk.no_selection")) + ctx.JSONRedirect(setting.AppSubURL + "/-/admin/packages") + return + } + + succeeded := 0 + for _, id := range ids { + if err := packages_model.SetPackageIsPrivate(ctx, id, isPrivate); err == nil { + succeeded++ + } + } + + if isPrivate { + ctx.Flash.Success(ctx.Tr("admin.packages.bulk.private.enabled", succeeded)) + } else { + ctx.Flash.Success(ctx.Tr("admin.packages.bulk.private.disabled", succeeded)) + } + + ctx.JSONRedirect(setting.AppSubURL + "/-/admin/packages") +} + // SingleAutoMatch automatically matches a single package to a repository func SingleAutoMatch(ctx *context.Context) { packageID := ctx.FormInt64("id") diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go index 6d44b9fde1..29e05aadcc 100644 --- a/routers/web/repo/packages.go +++ b/routers/web/repo/packages.go @@ -5,6 +5,7 @@ package repo import ( "net/http" + "strconv" "code.gitcaddy.com/server/v3/models/db" "code.gitcaddy.com/server/v3/models/packages" @@ -19,7 +20,7 @@ const ( tplPackagesList templates.TplName = "repo/packages" ) -// Packages displays a list of all packages in the repository +// Packages displays a list of all packages in the repository, grouped by visibility func Packages(ctx *context.Context) { page := max(ctx.FormInt("page"), 1) query := ctx.FormTrim("q") @@ -47,6 +48,16 @@ func Packages(ctx *context.Context) { return } + // Group packages by visibility + var publicPackages, privatePackages []*packages.PackageDescriptor + for _, pd := range pds { + if pd.Package.IsPrivate { + privatePackages = append(privatePackages, pd) + } else { + publicPackages = append(publicPackages, pd) + } + } + hasPackages, err := packages.HasRepositoryPackages(ctx, ctx.Repo.Repository.ID) if err != nil { ctx.ServerError("HasRepositoryPackages", err) @@ -61,6 +72,8 @@ func Packages(ctx *context.Context) { ctx.Data["HasPackages"] = hasPackages ctx.Data["CanWritePackages"] = ctx.Repo.CanWrite(unit.TypePackages) || ctx.IsUserSiteAdmin() ctx.Data["PackageDescriptors"] = pds + ctx.Data["PublicPackages"] = publicPackages + ctx.Data["PrivatePackages"] = privatePackages ctx.Data["Total"] = total ctx.Data["RepositoryAccessMap"] = map[int64]bool{ctx.Repo.Repository.ID: true} // There is only the current repository @@ -70,3 +83,34 @@ func Packages(ctx *context.Context) { ctx.HTML(http.StatusOK, tplPackagesList) } + +// BulkSetPackageVisibility changes visibility for multiple packages +func BulkSetPackageVisibility(ctx *context.Context) { + if !ctx.Repo.CanWrite(unit.TypePackages) && !ctx.IsUserSiteAdmin() { + ctx.JSONError(ctx.Tr("packages.settings.visibility.no_permission")) + return + } + + isPrivate := ctx.FormString("is_private") == "true" + ids := ctx.Req.Form["ids[]"] + + for _, idStr := range ids { + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + continue + } + + // Verify the package belongs to this repository + pkg, err := packages.GetPackageByID(ctx, id) + if err != nil || pkg.RepoID != ctx.Repo.Repository.ID { + continue + } + + if err := packages.SetPackageIsPrivate(ctx, id, isPrivate); err != nil { + ctx.ServerError("SetPackageIsPrivate", err) + return + } + } + + ctx.JSONOK() +} diff --git a/routers/web/web.go b/routers/web/web.go index 73d948e104..e86945abe1 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -851,6 +851,7 @@ func registerWebRoutes(m *web.Router) { m.Post("/delete", admin.DeletePackageVersion) m.Post("/cleanup", admin.CleanupExpiredData) m.Post("/bulk-global", admin.BulkSetGlobal) + m.Post("/bulk-private", admin.BulkSetPrivate) m.Post("/bulk-automatch", admin.BulkAutoMatch) m.Post("/automatch", admin.SingleAutoMatch) }, packagesEnabled) @@ -1638,6 +1639,7 @@ func registerWebRoutes(m *web.Router) { m.Group("/{username}/{reponame}", func() { if setting.Packages.Enabled { m.Get("/packages", repo.Packages) + m.Post("/packages/bulk-visibility", reqSignIn, repo.BulkSetPackageVisibility) } }, optSignIn, context.RepoAssignment) diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl index e3bbf77836..c5ae5f208a 100644 --- a/templates/admin/packages/list.tmpl +++ b/templates/admin/packages/list.tmpl @@ -34,6 +34,9 @@
{{svg "octicon-globe" 14}} {{ctx.Locale.Tr "admin.packages.bulk.enable_global"}}
{{svg "octicon-lock" 14}} {{ctx.Locale.Tr "admin.packages.bulk.disable_global"}}
+
{{svg "octicon-lock" 14}} {{ctx.Locale.Tr "admin.packages.bulk.make_private"}}
+
{{svg "octicon-eye" 14}} {{ctx.Locale.Tr "admin.packages.bulk.make_public"}}
+
{{svg "octicon-link" 14}} {{ctx.Locale.Tr "admin.packages.bulk.automatch"}}
@@ -58,6 +61,7 @@ {{ctx.Locale.Tr "admin.packages.creator"}} {{ctx.Locale.Tr "admin.packages.repository"}} + {{ctx.Locale.Tr "admin.packages.visibility"}} {{ctx.Locale.Tr "admin.packages.global"}} {{ctx.Locale.Tr "admin.packages.size"}} @@ -91,6 +95,13 @@ {{end}} + + {{if .Package.IsPrivate}} + {{svg "octicon-lock" 12}} {{ctx.Locale.Tr "admin.packages.visibility.private"}} + {{else}} + {{svg "octicon-eye" 12}} {{ctx.Locale.Tr "admin.packages.visibility.public"}} + {{end}} + {{if .Package.IsGlobal}} {{svg "octicon-globe" 12}} {{ctx.Locale.Tr "admin.packages.global.yes"}} @@ -108,7 +119,7 @@ {{else}} - {{ctx.Locale.Tr "no_results_found"}} + {{ctx.Locale.Tr "no_results_found"}} {{end}} @@ -177,6 +188,14 @@ document.addEventListener('DOMContentLoaded', function() { url = '{{AppSubUrl}}/-/admin/packages/bulk-global'; formData.append('is_global', 'false'); break; + case 'make-private': + url = '{{AppSubUrl}}/-/admin/packages/bulk-private'; + formData.append('is_private', 'true'); + break; + case 'make-public': + url = '{{AppSubUrl}}/-/admin/packages/bulk-private'; + formData.append('is_private', 'false'); + break; case 'automatch': url = '{{AppSubUrl}}/-/admin/packages/bulk-automatch'; break; diff --git a/templates/package/shared/list.tmpl b/templates/package/shared/list.tmpl index 910fa79418..39ef828aa0 100644 --- a/templates/package/shared/list.tmpl +++ b/templates/package/shared/list.tmpl @@ -14,37 +14,125 @@ {{end}} -
- {{range .PackageDescriptors}} -
-
-
-
- {{.Package.Name}} - {{svg .Package.Type.SVGName 16}} {{.Package.Type.Name}} - {{if .Package.IsPrivate}} - {{ctx.Locale.Tr "repo.visibility.private"}} - {{end}} -
-
- {{$timeStr := DateUtils.TimeSince .Version.CreatedUnix}} - {{$hasRepositoryAccess := false}} - {{if .Repository}} - {{$hasRepositoryAccess = index $.RepositoryAccessMap .Repository.ID}} - {{end}} - {{if $hasRepositoryAccess}} - {{ctx.Locale.Tr "packages.published_by_in" $timeStr .Creator.HomeLink .Creator.GetDisplayName .Repository.Link .Repository.FullName}} - {{else}} - {{ctx.Locale.Tr "packages.published_by" $timeStr .Creator.HomeLink .Creator.GetDisplayName}} - {{end}} -
-
-
- {{DateUtils.TimeSince .Version.CreatedUnix}} -
+ +{{if and .CanWritePackages (or .PublicPackages .PrivatePackages)}} +
+ - {{else}} + +
+{{end}} + +
+ {{/* Public Packages Section */}} + {{if .PublicPackages}} +
+

+ {{svg "octicon-globe" 16}} + {{ctx.Locale.Tr "packages.visibility.public"}} + {{len .PublicPackages}} + {{if .CanWritePackages}} +
+ +
+ {{end}} +

+
+ {{range .PublicPackages}} +
+
+ {{if $.CanWritePackages}} +
+ +
+ {{end}} +
+
+ {{.Package.Name}} + {{svg .Package.Type.SVGName 16}} {{.Package.Type.Name}} +
+
+ {{$timeStr := DateUtils.TimeSince .Version.CreatedUnix}} + {{$hasRepositoryAccess := false}} + {{if .Repository}} + {{$hasRepositoryAccess = index $.RepositoryAccessMap .Repository.ID}} + {{end}} + {{if $hasRepositoryAccess}} + {{ctx.Locale.Tr "packages.published_by_in" $timeStr .Creator.HomeLink .Creator.GetDisplayName .Repository.Link .Repository.FullName}} + {{else}} + {{ctx.Locale.Tr "packages.published_by" $timeStr .Creator.HomeLink .Creator.GetDisplayName}} + {{end}} +
+
+
+ {{DateUtils.TimeSince .Version.CreatedUnix}} +
+
+
+ {{end}} +
+
+ {{end}} + + {{/* Private Packages Section */}} + {{if .PrivatePackages}} +
+

+ {{svg "octicon-lock" 16}} + {{ctx.Locale.Tr "packages.visibility.private"}} + {{len .PrivatePackages}} + {{if .CanWritePackages}} +
+ +
+ {{end}} +

+
+ {{range .PrivatePackages}} +
+
+ {{if $.CanWritePackages}} +
+ +
+ {{end}} +
+
+ {{.Package.Name}} + {{svg .Package.Type.SVGName 16}} {{.Package.Type.Name}} + {{ctx.Locale.Tr "repo.visibility.private"}} +
+
+ {{$timeStr := DateUtils.TimeSince .Version.CreatedUnix}} + {{$hasRepositoryAccess := false}} + {{if .Repository}} + {{$hasRepositoryAccess = index $.RepositoryAccessMap .Repository.ID}} + {{end}} + {{if $hasRepositoryAccess}} + {{ctx.Locale.Tr "packages.published_by_in" $timeStr .Creator.HomeLink .Creator.GetDisplayName .Repository.Link .Repository.FullName}} + {{else}} + {{ctx.Locale.Tr "packages.published_by" $timeStr .Creator.HomeLink .Creator.GetDisplayName}} + {{end}} +
+
+
+ {{DateUtils.TimeSince .Version.CreatedUnix}} +
+
+
+ {{end}} +
+
+ {{end}} + + {{/* Empty state - no packages at all */}} + {{if and (not .PublicPackages) (not .PrivatePackages)}} {{if not .HasPackages}}
{{svg "octicon-package" 48}} @@ -61,3 +149,76 @@ {{end}} {{template "base/paginate" .}}
+ +{{if .CanWritePackages}} + +{{end}}