feat(licenses): edit and delete license packages via web UI
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled

Add edit and delete actions for license packages:
- Edit button (pencil icon) opens edit form with all package fields
- Delete button (trash icon) with confirmation dialog
- Edit form includes active/inactive toggle
- Routes: GET/POST /licenses/packages/{id}/edit, POST /licenses/packages/{id}/delete

Ref #239

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan Miller
2026-05-30 23:03:44 -05:00
parent 3a5ca580db
commit 021ddbb17a
5 changed files with 128 additions and 3 deletions
+4
View File
@@ -2646,6 +2646,10 @@
"repo.licenses.key_created": "License Key Created",
"repo.licenses.key_created_copy": "Copy this key now. It will not be shown again.",
"repo.licenses.revoke": "Revoke",
"repo.licenses.edit_package": "Edit License Package",
"repo.licenses.delete_package": "Delete Package",
"repo.licenses.package_updated": "License package updated.",
"repo.licenses.package_deleted": "License package deleted.",
"repo.licenses.key_revoked": "License key revoked.",
"repo.licenses.master_key_created": "Master License Key Created",
"repo.licenses.master_key_created_copy": "This is your organization master key with unlimited access to all update channels. Copy it now — it will not be shown again.",
+57
View File
@@ -179,3 +179,60 @@ func LicensesRevokeKey(ctx *context.Context) {
ctx.Flash.Success(ctx.Tr("repo.licenses.key_revoked"))
ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
}
const tplLicensesEditPackage templates.TplName = "repo/licenses_edit_package"
// LicensesEditPackage shows the edit form for a license package.
func LicensesEditPackage(ctx *context.Context) {
pkgID := ctx.PathParamInt64("id")
pkg, err := licenses.GetLicensePackageByID(ctx, pkgID)
if err != nil {
ctx.ServerError("GetLicensePackageByID", err)
return
}
ctx.Data["Title"] = ctx.Tr("repo.licenses.edit_package")
ctx.Data["PageIsLicenses"] = true
ctx.Data["IsLicensesPage"] = true
ctx.Data["Package"] = pkg
ctx.HTML(http.StatusOK, tplLicensesEditPackage)
}
// LicensesEditPackagePost saves edits to a license package.
func LicensesEditPackagePost(ctx *context.Context) {
pkgID := ctx.PathParamInt64("id")
pkg, err := licenses.GetLicensePackageByID(ctx, pkgID)
if err != nil {
ctx.ServerError("GetLicensePackageByID", err)
return
}
pkg.Name = ctx.FormString("name")
pkg.Description = ctx.FormString("description")
durationDays, _ := strconv.Atoi(ctx.FormString("duration_days"))
pkg.DurationDays = durationDays
maxSites, _ := strconv.Atoi(ctx.FormString("max_sites"))
pkg.MaxSites = maxSites
pkg.AllowedChannels = ctx.FormString("allowed_channels")
pkg.IsActive = ctx.FormString("is_active") == "on"
if err := licenses.UpdateLicensePackage(ctx, pkg); err != nil {
ctx.ServerError("UpdateLicensePackage", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.licenses.package_updated"))
ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
}
// LicensesDeletePackage deletes a license package.
func LicensesDeletePackage(ctx *context.Context) {
pkgID := ctx.PathParamInt64("id")
if err := licenses.DeleteLicensePackage(ctx, pkgID); err != nil {
ctx.ServerError("DeleteLicensePackage", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.licenses.package_deleted"))
ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
}
+3
View File
@@ -1512,6 +1512,9 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Group("/{username}/{reponame}/licenses", func() {
m.Get("", repo.Licenses)
m.Post("/packages", repo.LicensesCreatePackage)
m.Get("/packages/{id}/edit", repo.LicensesEditPackage)
m.Post("/packages/{id}/edit", repo.LicensesEditPackagePost)
m.Post("/packages/{id}/delete", repo.LicensesDeletePackage)
m.Post("/keys/generate", repo.LicensesGenerateKey)
m.Post("/keys/{id}/revoke", repo.LicensesRevokeKey)
}, optSignIn, context.RepoAssignment)
+12 -3
View File
@@ -45,12 +45,21 @@
<td>{{.KeyCount}}</td>
<td>{{if .IsActive}}<span class="ui green label">{{ctx.Locale.Tr "repo.licenses.active"}}</span>{{else}}<span class="ui grey label">{{ctx.Locale.Tr "repo.licenses.inactive"}}</span>{{end}}</td>
{{if $.IsRepoAdmin}}
<td class="tw-text-right">
<td class="tw-text-right tw-flex tw-gap-1 tw-justify-end">
<form method="post" action="{{$.RepoLink}}/licenses/keys/generate" class="tw-inline">
{{$.CsrfTokenHtml}}
<input type="hidden" name="package_id" value="{{.ID}}">
<button class="ui tiny primary button" type="submit">
{{svg "octicon-plus" 14}} {{ctx.Locale.Tr "repo.licenses.generate_key"}}
<button class="ui tiny primary button" type="submit" title="{{ctx.Locale.Tr "repo.licenses.generate_key"}}">
{{svg "octicon-plus" 14}}
</button>
</form>
<a class="ui tiny button" href="{{$.RepoLink}}/licenses/packages/{{.ID}}/edit" title="{{ctx.Locale.Tr "repo.licenses.edit_package"}}">
{{svg "octicon-pencil" 14}}
</a>
<form method="post" action="{{$.RepoLink}}/licenses/packages/{{.ID}}/delete" class="tw-inline" onsubmit="return confirm('Delete this package?')">
{{$.CsrfTokenHtml}}
<button class="ui tiny red button" type="submit" title="{{ctx.Locale.Tr "repo.licenses.delete_package"}}">
{{svg "octicon-trash" 14}}
</button>
</form>
</td>
+52
View File
@@ -0,0 +1,52 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content repository">
{{template "repo/header" .}}
<div class="ui container">
<h4 class="ui top attached header">
{{svg "octicon-pencil" 16}} {{ctx.Locale.Tr "repo.licenses.edit_package"}}
</h4>
<div class="ui attached segment">
<form class="ui form" method="post" action="{{.RepoLink}}/licenses/packages/{{.Package.ID}}/edit">
{{.CsrfTokenHtml}}
<div class="two fields">
<div class="required field">
<label>{{ctx.Locale.Tr "repo.licenses.package_name"}}</label>
<input name="name" required value="{{.Package.Name}}">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.description"}}</label>
<input name="description" value="{{.Package.Description}}">
</div>
</div>
<div class="three fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.duration"}} ({{ctx.Locale.Tr "repo.licenses.days"}})</label>
<input name="duration_days" type="number" value="{{.Package.DurationDays}}" min="0">
<p class="help">0 = {{ctx.Locale.Tr "repo.licenses.lifetime"}}</p>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.max_sites"}}</label>
<input name="max_sites" type="number" value="{{.Package.MaxSites}}" min="0">
<p class="help">0 = unlimited</p>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.channels"}}</label>
<input name="allowed_channels" value="{{.Package.AllowedChannels}}">
<p class="help">{{ctx.Locale.Tr "repo.licenses.channels_help"}}</p>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input name="is_active" type="checkbox" {{if .Package.IsActive}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.licenses.active"}}</label>
</div>
</div>
<div class="field tw-mt-4">
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "save"}}</button>
<a class="ui button" href="{{.RepoLink}}/licenses">{{ctx.Locale.Tr "cancel"}}</a>
</div>
</form>
</div>
</div>
</div>
{{template "base/footer" .}}