Compare commits

...

11 Commits

Author SHA1 Message Date
AvengerMoJo c6684ef8b1
Merge c4feb3d355 into 5c236bd4c0 2024-05-05 21:19:00 +08:00
wxiaoguang 5c236bd4c0
Fix issue/PR title edit (#30858)
1. "enter" doesn't work (I think it is the last enter support for #14843)
2. if a branch name contains something like `&`, then the branch selector doesn't update
2024-05-05 13:09:41 +00:00
Alex Lau(AvengerMoJo) c4feb3d355
Merge branch 'main' into wip_require_action_feature_fail_post 2024-05-05 17:50:20 +08:00
Alex Lau(AvengerMoJo) d68a821455
Fix the format
Signed-off-by: Alex Lau(AvengerMoJo) <avengermojo@gmail.com>
2024-04-29 15:03:35 +08:00
Alex Lau(AvengerMoJo) 325842db66
Merge branch 'main' into wip_require_action_feature_fail_post 2024-04-29 14:20:36 +08:00
Alex Lau(AvengerMoJo) c4b923886c
moving the js to a seperated file in web_src/js/features/
Signed-off-by: Alex Lau(AvengerMoJo) <avengermojo@gmail.com>
2024-04-22 13:40:37 +08:00
Alex Lau(AvengerMoJo) 32cb52662f
Remove formatting all the template
Signed-off-by: Alex Lau(AvengerMoJo) <avengermojo@gmail.com>
2024-04-11 14:15:16 +08:00
Alex Lau(AvengerMoJo) c10d144ca7
Remove duplicate error for the global enable flag remove formatting error.
Signed-off-by: Alex Lau(AvengerMoJo) <avengermojo@gmail.com>
2024-04-11 13:40:18 +08:00
Alex Lau(AvengerMoJo) d4fd5b553d
Add the delete function and remove formatting error.
Signed-off-by: Alex Lau(AvengerMoJo) <avengermojo@gmail.com>
2024-04-11 13:39:59 +08:00
Alex Lau(AvengerMoJo) 1fc998984f
Fix all the format of the commit
Signed-off-by: Alex Lau(AvengerMoJo) <avengermojo@gmail.com>
2024-04-10 17:07:46 +08:00
Alex Lau(AvengerMoJo) 5243490071
Enable the global workflow and require_action in org setting
Signed-off-by: Alex Lau(AvengerMoJo) <avengermojo@gmail.com>
2024-04-09 23:05:09 +08:00
25 changed files with 745 additions and 153 deletions

View File

@ -0,0 +1,82 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// WIP RequireAction
package actions
import (
"context"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/builder"
)
type RequireAction struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"index"`
RepoName string `xorm:"VARCHAR(255)"`
WorkflowName string `xorm:"VARCHAR(255) UNIQUE(require_action) NOT NULL"`
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
type GlobalWorkflow struct {
RepoName string
Filename string
}
func init() {
db.RegisterModel(new(RequireAction))
}
type FindRequireActionOptions struct {
db.ListOptions
RequireActionID int64
OrgID int64
RepoName string
}
func (opts FindRequireActionOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.OrgID > 0 {
cond = cond.And(builder.Eq{"org_id": opts.OrgID})
}
if opts.RequireActionID > 0 {
cond = cond.And(builder.Eq{"id": opts.RequireActionID})
}
if opts.RepoName != "" {
cond = cond.And(builder.Eq{"repo_name": opts.RepoName})
}
return cond
}
// LoadAttributes loads the attributes of the require action
func (r *RequireAction) LoadAttributes(ctx context.Context) error {
// place holder for now.
return nil
}
// if the workflow is removable
func (r *RequireAction) Removable(orgID int64) bool {
// everyone can remove for now
return r.OrgID == orgID
}
func AddRequireAction(ctx context.Context, orgID int64, repoName, workflowName string) (*RequireAction, error) {
ra := &RequireAction{
OrgID: orgID,
RepoName: repoName,
WorkflowName: workflowName,
}
return ra, db.Insert(ctx, ra)
}
func DeleteRequireAction(ctx context.Context, requireActionID int64) error {
if _, err := db.DeleteByID[RequireAction](ctx, requireActionID); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,22 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_23 //nolint
import (
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/xorm"
)
func AddRequireActionTable(x *xorm.Engine) error {
type RequireAction struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"index"`
RepoName string `xorm:"VARCHAR(255)"`
WorkflowName string `xorm:"VARCHAR(255) UNIQUE(require_action) NOT NULL"`
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
return x.Sync(new(RequireAction))
}

View File

@ -169,13 +169,30 @@ func (cfg *PullRequestsConfig) GetDefaultMergeStyle() MergeStyle {
}
type ActionsConfig struct {
DisabledWorkflows []string
DisabledWorkflows []string
EnabledGlobalWorkflows []string
}
func (cfg *ActionsConfig) EnableWorkflow(file string) {
cfg.DisabledWorkflows = util.SliceRemoveAll(cfg.DisabledWorkflows, file)
}
func (cfg *ActionsConfig) DisableGlobalWorkflow(file string) {
cfg.EnabledGlobalWorkflows = util.SliceRemoveAll(cfg.EnabledGlobalWorkflows, file)
}
func (cfg *ActionsConfig) IsGlobalWorkflowEnabled(file string) bool {
return slices.Contains(cfg.EnabledGlobalWorkflows, file)
}
func (cfg *ActionsConfig) EnableGlobalWorkflow(file string) {
cfg.EnabledGlobalWorkflows = append(cfg.EnabledGlobalWorkflows, file)
}
func (cfg *ActionsConfig) GetGlobalWorkflow() []string {
return cfg.EnabledGlobalWorkflows
}
func (cfg *ActionsConfig) ToString() string {
return strings.Join(cfg.DisabledWorkflows, ",")
}

View File

@ -3647,11 +3647,38 @@ runs.no_workflows.documentation = For more information on Gitea Actions, see <a
runs.no_runs = The workflow has no runs yet.
runs.empty_commit_message = (empty commit message)
require_action = Require Actions
require_action.require_action_manage_panel = Require Actions Management Panel
require_action.enable_global_workflow = How to Enable Global Workflow
require_action.id = ID
require_action.add = Add Global Workflow
require_action.add_require_action = Enable selected Workflow
require_action.new = Create New
require_action.status = Status
require_action.search = Search...
require_action.version = Version
require_action.repo = Repo Name
require_action.workflow = Workflow Filename
require_action.link = Link
require_action.remove = Remove
require_action.none = No Require Actions Available.
require_action.creation.failed = Create Global Require Action %s Failed.
require_action.creation.success = Create Global Require Action %s successfully.
require_action.deletion = Delete
require_action.deletion.description = Removing the Global Require Action is permanent and cannot be undone. Continue?
require_action.deletion.success = The Global Require Action has been removed.
workflow.disable = Disable Workflow
workflow.disable_success = Workflow '%s' disabled successfully.
workflow.enable = Enable Workflow
workflow.enable_success = Workflow '%s' enabled successfully.
workflow.disabled = Workflow is disabled.
workflow.global = Global
workflow.global_disable = Disable Global Require
workflow.global_disable_success = Global Require '%s' disabled successfully.
workflow.global_enable = Enable Global Require
workflow.global_enable_success = Global Require '%s' enabled successfully.
workflow.global_enabled = Global Require is disabled.
need_approval_desc = Need approval to run workflows for fork pull request.

View File

@ -0,0 +1,14 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// WIP RequireAction
package setting
import (
"code.gitea.io/gitea/services/context"
)
func RedirectToRepoSetting(ctx *context.Context) {
ctx.Redirect(ctx.Org.OrgLink + "/settings/actions/require_action")
}

View File

@ -145,6 +145,7 @@ func List(ctx *context.Context) {
workflow := ctx.FormString("workflow")
actorID := ctx.FormInt64("actor")
status := ctx.FormInt("status")
isGlobal := false
ctx.Data["CurWorkflow"] = workflow
actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
@ -153,6 +154,8 @@ func List(ctx *context.Context) {
if len(workflow) > 0 && ctx.Repo.IsAdmin() {
ctx.Data["AllowDisableOrEnableWorkflow"] = true
ctx.Data["CurWorkflowDisabled"] = actionsConfig.IsWorkflowDisabled(workflow)
ctx.Data["CurGlobalWorkflowEnable"] = actionsConfig.IsGlobalWorkflowEnabled(workflow)
isGlobal = actionsConfig.IsGlobalWorkflowEnabled(workflow)
}
// if status or actor query param is not given to frontend href, (href="/<repoLink>/actions")
@ -209,6 +212,9 @@ func List(ctx *context.Context) {
pager.AddParamString("workflow", workflow)
pager.AddParamString("actor", fmt.Sprint(actorID))
pager.AddParamString("status", fmt.Sprint(status))
if isGlobal {
pager.AddParamString("global", fmt.Sprint(isGlobal))
}
ctx.Data["Page"] = pager
ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0

View File

@ -685,33 +685,60 @@ func EnableWorkflowFile(ctx *context_module.Context) {
}
func disableOrEnableWorkflowFile(ctx *context_module.Context, isEnable bool) {
disableOrEnable(ctx, isEnable, false)
}
func disableOrEnable(ctx *context_module.Context, isEnable, isglobal bool) {
workflow := ctx.FormString("workflow")
if len(workflow) == 0 {
ctx.ServerError("workflow", nil)
return
}
cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
cfg := cfgUnit.ActionsConfig()
if isEnable {
cfg.EnableWorkflow(workflow)
if isglobal {
if isEnable {
cfg.DisableGlobalWorkflow(workflow)
} else {
cfg.EnableGlobalWorkflow(workflow)
}
} else {
cfg.DisableWorkflow(workflow)
if isEnable {
cfg.EnableWorkflow(workflow)
} else {
cfg.DisableWorkflow(workflow)
}
}
if err := repo_model.UpdateRepoUnit(ctx, cfgUnit); err != nil {
ctx.ServerError("UpdateRepoUnit", err)
return
}
if isEnable {
ctx.Flash.Success(ctx.Tr("actions.workflow.enable_success", workflow))
if isglobal {
if isEnable {
ctx.Flash.Success(ctx.Tr("actions.workflow.global_disable_success", workflow))
} else {
ctx.Flash.Success(ctx.Tr("actions.workflow.global_enable_success", workflow))
}
} else {
ctx.Flash.Success(ctx.Tr("actions.workflow.disable_success", workflow))
if isEnable {
ctx.Flash.Success(ctx.Tr("actions.workflow.enable_success", workflow))
} else {
ctx.Flash.Success(ctx.Tr("actions.workflow.disable_success", workflow))
}
}
redirectURL := fmt.Sprintf("%s/actions?workflow=%s&actor=%s&status=%s", ctx.Repo.RepoLink, url.QueryEscape(workflow),
url.QueryEscape(ctx.FormString("actor")), url.QueryEscape(ctx.FormString("status")))
ctx.JSONRedirect(redirectURL)
}
func DisableGlobalWorkflowFile(ctx *context_module.Context) {
disableOrEnableGlobalWorkflowFile(ctx, true)
}
func EnableGlobalWorkflowFile(ctx *context_module.Context) {
disableOrEnableGlobalWorkflowFile(ctx, false)
}
func disableOrEnableGlobalWorkflowFile(ctx *context_module.Context, isEnable bool) {
disableOrEnable(ctx, isEnable, true)
}

View File

@ -0,0 +1,87 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// WIP RequireAction
package setting
import (
"errors"
"net/http"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/base"
shared "code.gitea.io/gitea/routers/web/shared/actions"
"code.gitea.io/gitea/services/context"
)
const (
// let start with org WIP
tplOrgRequireAction base.TplName = "org/settings/actions"
)
type requireActionsCtx struct {
OrgID int64
IsOrg bool
RequireActionTemplate base.TplName
RedirectLink string
}
func getRequireActionCtx(ctx *context.Context) (*requireActionsCtx, error) {
if ctx.Data["PageIsOrgSettings"] == true {
return &requireActionsCtx{
OrgID: ctx.Org.Organization.ID,
IsOrg: true,
RequireActionTemplate: tplOrgRequireAction,
RedirectLink: ctx.Org.OrgLink + "/settings/actions/require_action",
}, nil
}
return nil, errors.New("unable to set Require Actions context")
}
// Listing all RequireAction
func RequireAction(ctx *context.Context) {
ctx.Data["ActionsTitle"] = ctx.Tr("actions.requires")
ctx.Data["PageType"] = "require_action"
ctx.Data["PageIsSharedSettingsRequireAction"] = true
vCtx, err := getRequireActionCtx(ctx)
if err != nil {
ctx.ServerError("getRequireActionCtx", err)
return
}
page := ctx.FormInt("page")
if page <= 1 {
page = 1
}
opts := actions_model.FindRequireActionOptions{
ListOptions: db.ListOptions{
Page: page,
PageSize: 10,
},
}
shared.SetRequireActionContext(ctx, opts)
ctx.Data["Link"] = vCtx.RedirectLink
shared.GlobalEnableWorkflow(ctx, ctx.Org.Organization.ID)
ctx.HTML(http.StatusOK, vCtx.RequireActionTemplate)
}
func RequireActionCreate(ctx *context.Context) {
vCtx, err := getRequireActionCtx(ctx)
if err != nil {
ctx.ServerError("getRequireActionCtx", err)
return
}
shared.CreateRequireAction(ctx, vCtx.OrgID, vCtx.RedirectLink)
}
func RequireActionDelete(ctx *context.Context) {
vCtx, err := getRequireActionCtx(ctx)
if err != nil {
ctx.ServerError("getRequireActionCtx", err)
return
}
shared.DeleteRequireAction(ctx, vCtx.RedirectLink)
}

View File

@ -0,0 +1,84 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// WIP RequireAction
package actions
import (
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
org_model "code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/web"
actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
// SetRequireActionDeletePost response for deleting a require action workflow
func SetRequireActionContext(ctx *context.Context, opts actions_model.FindRequireActionOptions) {
requireActions, count, err := db.FindAndCount[actions_model.RequireAction](ctx, opts)
if err != nil {
ctx.ServerError("CountRequireActions", err)
return
}
ctx.Data["RequireActions"] = requireActions
ctx.Data["Total"] = count
ctx.Data["OrgID"] = ctx.Org.Organization.ID
ctx.Data["OrgName"] = ctx.Org.Organization.Name
pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
ctx.Data["Page"] = pager
}
// get all the available enable global workflow in the org's repo
func GlobalEnableWorkflow(ctx *context.Context, orgID int64) {
var gwfList []actions_model.GlobalWorkflow
orgRepos, err := org_model.GetOrgRepositories(ctx, orgID)
if err != nil {
ctx.ServerError("GlobalEnableWorkflows get org repos: ", err)
return
}
for _, repo := range orgRepos {
err := repo.LoadUnits(ctx)
if err != nil {
ctx.ServerError("GlobalEnableWorkflows LoadUnits : ", err)
}
actionsConfig := repo.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
enabledWorkflows := actionsConfig.GetGlobalWorkflow()
for _, workflow := range enabledWorkflows {
gwf := actions_model.GlobalWorkflow{
RepoName: repo.Name,
Filename: workflow,
}
gwfList = append(gwfList, gwf)
}
}
ctx.Data["GlobalEnableWorkflows"] = gwfList
}
func CreateRequireAction(ctx *context.Context, orgID int64, redirectURL string) {
ctx.Data["OrgID"] = ctx.Org.Organization.ID
form := web.GetForm(ctx).(*forms.RequireActionForm)
v, err := actions_service.CreateRequireAction(ctx, orgID, form.RepoName, form.WorkflowName)
if err != nil {
log.Error("CreateRequireAction: %v", err)
ctx.JSONError(ctx.Tr("actions.require_action.creation.failed"))
return
}
ctx.Flash.Success(ctx.Tr("actions.require_action.creation.success", v.WorkflowName))
ctx.JSONRedirect(redirectURL)
}
func DeleteRequireAction(ctx *context.Context, redirectURL string) {
id := ctx.ParamsInt64(":require_action_id")
if err := actions_service.DeleteRequireActionByID(ctx, id); err != nil {
log.Error("Delete RequireAction [%d] failed: %v", id, err)
ctx.JSONError(ctx.Tr("actions.require_action.deletion.failed"))
return
}
ctx.Flash.Success(ctx.Tr("actions.require_action.deletion.success"))
ctx.JSONRedirect(redirectURL)
}

View File

@ -457,6 +457,15 @@ func registerRoutes(m *web.Route) {
})
}
// WIP RequireAction
addSettingsRequireActionRoutes := func() {
m.Group("/require_action", func() {
m.Get("", repo_setting.RequireAction)
m.Post("/add", web.Bind(forms.RequireActionForm{}), repo_setting.RequireActionCreate)
m.Post("/{require_action_id}/delete", repo_setting.RequireActionDelete)
})
}
// FIXME: not all routes need go through same middleware.
// Especially some AJAX requests, we can reduce middleware number to improve performance.
@ -628,6 +637,7 @@ func registerRoutes(m *web.Route) {
m.Group("/actions", func() {
m.Get("", user_setting.RedirectToDefaultSetting)
addSettingsRequireActionRoutes()
addSettingsRunnersRoutes()
addSettingsSecretsRoutes()
addSettingsVariablesRoutes()
@ -926,6 +936,7 @@ func registerRoutes(m *web.Route) {
m.Group("/actions", func() {
m.Get("", org_setting.RedirectToDefaultSetting)
addSettingsRequireActionRoutes()
addSettingsRunnersRoutes()
addSettingsSecretsRoutes()
addSettingsVariablesRoutes()
@ -1371,10 +1382,12 @@ func registerRoutes(m *web.Route) {
}, ignSignIn, context.RepoAssignment, reqRepoProjectsReader, repo.MustEnableRepoProjects)
// end "/{username}/{reponame}/projects"
m.Group("/{username}/{reponame}/actions", func() {
m.Group("/actions", func() {
m.Get("", actions.List)
m.Post("/disable", reqRepoAdmin, actions.DisableWorkflowFile)
m.Post("/enable", reqRepoAdmin, actions.EnableWorkflowFile)
m.Post("/global_disable", reqRepoAdmin, actions.DisableGlobalWorkflowFile)
m.Post("/global_enable", reqRepoAdmin, actions.EnableGlobalWorkflowFile)
m.Group("/runs/{run}", func() {
m.Combo("").

View File

@ -0,0 +1,22 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"context"
actions_model "code.gitea.io/gitea/models/actions"
)
func CreateRequireAction(ctx context.Context, orgID int64, repoName, workflowName string) (*actions_model.RequireAction, error) {
v, err := actions_model.AddRequireAction(ctx, orgID, repoName, workflowName)
if err != nil {
return nil, err
}
return v, nil
}
func DeleteRequireActionByID(ctx context.Context, requireActionID int64) error {
return actions_model.DeleteRequireAction(ctx, requireActionID)
}

View File

@ -344,6 +344,12 @@ func (f *EditVariableForm) Validate(req *http.Request, errs binding.Errors) bind
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
// WIP RequireAction create form
type RequireActionForm struct {
RepoName string `binding:"Required;MaxSize(255)"`
WorkflowName string `binding:"Required;MaxSize(255)"`
}
// NewAccessTokenForm form for creating access token
type NewAccessTokenForm struct {
Name string `binding:"Required;MaxSize(255)" locale:"settings.token_name"`

View File

@ -1,6 +1,8 @@
{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings actions")}}
<div class="org-setting-content">
{{if eq .PageType "runners"}}
{{if eq .PageType "require_action"}}
{{template "shared/actions/require_action_list" .}}
{{else if eq .PageType "runners"}}
{{template "shared/actions/runner_list" .}}
{{else if eq .PageType "secrets"}}
{{template "shared/secrets/add_list" .}}

View File

@ -29,6 +29,9 @@
<details class="item toggleable-item" {{if or .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables}}open{{end}}>
<summary>{{ctx.Locale.Tr "actions.actions"}}</summary>
<div class="menu">
<a class="{{if .PageIsSharedSettingsRequireAction}}active {{end}}item" href="{{.OrgLink}}/settings/actions/require_action">
{{ctx.Locale.Tr "actions.require_action"}}
</a>
<a class="{{if .PageIsSharedSettingsRunners}}active {{end}}item" href="{{.OrgLink}}/settings/actions/runners">
{{ctx.Locale.Tr "actions.runners"}}
</a>

View File

@ -20,6 +20,9 @@
{{if $.ActionsConfig.IsWorkflowDisabled .Entry.Name}}
<div class="ui red label">{{ctx.Locale.Tr "disabled"}}</div>
{{end}}
{{if $.ActionsConfig.IsGlobalWorkflowEnabled .Entry.Name}}
<div class="ui red label">{{ctx.Locale.Tr "Global Enabled"}}</div>
{{end}}
</a>
{{end}}
</div>
@ -65,6 +68,10 @@
</div>
</div>
<!-- IsGlobalWorkflowEnabled -->
<div class="ui dropdown jump item">
<span class="text">{{ctx.Locale.Tr "actions.workflow.global"}}</span>
</div>
{{if .AllowDisableOrEnableWorkflow}}
<button class="ui jump dropdown btn interact-bg tw-p-2">
{{svg "octicon-kebab-horizontal"}}
@ -72,6 +79,9 @@
<a class="item link-action" data-url="{{$.Link}}/{{if .CurWorkflowDisabled}}enable{{else}}disable{{end}}?workflow={{$.CurWorkflow}}&actor={{.CurActor}}&status={{$.CurStatus}}">
{{if .CurWorkflowDisabled}}{{ctx.Locale.Tr "actions.workflow.enable"}}{{else}}{{ctx.Locale.Tr "actions.workflow.disable"}}{{end}}
</a>
<a class="item link-action" data-url="{{$.Link}}/{{if .CurGlobalWorkflowEnable}}global_disable{{else}}global_enable{{end}}?workflow={{$.CurWorkflow}}&actor={{.CurActor}}&status={{$.CurStatus}}">
{{if .CurGlobalWorkflowEnable}}{{ctx.Locale.Tr "actions.workflow.global_disable"}}{{else}}{{ctx.Locale.Tr "actions.workflow.global_enable"}}{{end}}
</a>
</div>
</button>
{{end}}

View File

@ -4,29 +4,36 @@
</div>
{{end}}
<div class="issue-title-header">
<div class="issue-title" id="issue-title-wrapper">
{{$canEditIssueTitle := and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}
<div class="issue-title" id="issue-title-display">
<h1 class="gt-word-break">
<span id="issue-title">{{RenderIssueTitle $.Context .Issue.Title ($.Repository.ComposeMetas ctx) | RenderCodeBlock}} <span class="index">#{{.Issue.Index}}</span>
</span>
<div id="edit-title-input" class="ui input tw-flex-1 tw-hidden">
<input value="{{.Issue.Title}}" maxlength="255" autocomplete="off">
</div>
{{RenderIssueTitle $.Context .Issue.Title ($.Repository.ComposeMetas ctx) | RenderCodeBlock}}
<span class="index">#{{.Issue.Index}}</span>
</h1>
<div class="issue-title-buttons">
{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}
<button id="edit-title" class="ui small basic button edit-button not-in-edit{{if .Issue.IsPull}} tw-mr-0{{end}}">{{ctx.Locale.Tr "repo.issues.edit"}}</button>
{{if $canEditIssueTitle}}
<button id="issue-title-edit-show" class="ui small basic button">{{ctx.Locale.Tr "repo.issues.edit"}}</button>
{{end}}
{{if not .Issue.IsPull}}
<a role="button" class="ui small primary button new-issue-button tw-mr-0" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>
<a role="button" class="ui small primary button" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>
{{end}}
</div>
{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}
<div class="edit-buttons">
<button id="cancel-edit-title" class="ui small basic button in-edit tw-hidden">{{ctx.Locale.Tr "repo.issues.cancel"}}</button>
<button id="save-edit-title" class="ui small primary button in-edit tw-hidden tw-mr-0" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/title" {{if .Issue.IsPull}}data-target-update-url="{{$.RepoLink}}/pull/{{.Issue.Index}}/target_branch"{{end}}>{{ctx.Locale.Tr "repo.issues.save"}}</button>
</div>
{{end}}
</div>
{{if $canEditIssueTitle}}
<div class="ui form issue-title tw-hidden" id="issue-title-editor">
<div class="ui input tw-flex-1">
<input value="{{.Issue.Title}}" data-old-title="{{.Issue.Title}}" maxlength="255" autocomplete="off">
</div>
<div class="issue-title-buttons">
<button class="ui small basic cancel button">{{ctx.Locale.Tr "repo.issues.cancel"}}</button>
<button class="ui small primary button"
data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/title"
{{if .Issue.IsPull}}data-target-update-url="{{$.RepoLink}}/pull/{{.Issue.Index}}/target_branch"{{end}}>
{{ctx.Locale.Tr "repo.issues.save"}}
</button>
</div>
</div>
{{end}}
<div class="issue-title-meta">
{{if .HasMerged}}
<div class="ui purple label issue-state-label">{{svg "octicon-git-merge" 16 "tw-mr-1"}} {{if eq .Issue.PullRequest.Status 3}}{{ctx.Locale.Tr "repo.pulls.manually_merged"}}{{else}}{{ctx.Locale.Tr "repo.pulls.merged"}}{{end}}</div>
@ -63,14 +70,14 @@
{{end}}
{{else}}
{{if .Issue.OriginalAuthor}}
<span id="pull-desc" class="pull-desc">{{.Issue.OriginalAuthor}} {{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref}}</span>
<span id="pull-desc-display" class="pull-desc">{{.Issue.OriginalAuthor}} {{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref}}</span>
{{else}}
<span id="pull-desc" class="pull-desc">
<span id="pull-desc-display" class="pull-desc">
<a {{if gt .Issue.Poster.ID 0}}href="{{.Issue.Poster.HomeLink}}"{{end}}>{{.Issue.Poster.GetDisplayName}}</a>
{{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref}}
</span>
{{end}}
<span id="pull-desc-edit" class="tw-hidden flex-text-block">
<span id="pull-desc-editor" class="tw-hidden flex-text-block">
<div class="ui floating filter dropdown">
<div class="ui basic small button tw-mr-0">
<span class="text">{{ctx.Locale.Tr "repo.pulls.compare_compare"}}: {{$.HeadTarget}}</span>

View File

@ -0,0 +1,147 @@
<div class="require-actions-container">
<h4 class="ui top attached header">
{{ctx.Locale.Tr "actions.require_action.require_action_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}})
<div class="ui right">
<div class="ui top right">
<button class="ui primary tiny button show-modal"
data-modal="#add-require-actions-modal"
data-modal-form.action="{{.Link}}/add"
data-modal-header="{{ctx.Locale.Tr "actions.require_action.add"}}">
{{ctx.Locale.Tr "actions.require_action.add"}}
</button>
</div>
</div>
</h4>
<div class="ui attached segment">
<form class="ui form ignore-dirty" id="require-action-list-search-form" action="{{$.Link}}">
<!-- Search Text -->
{{template "shared/search/combo" dict "Value" .Keyword}}
<button class="ui primary button">{{ctx.Locale.Tr "actions.require_action.search"}}</button>
</form>
</div>
<div class="ui attached table segment">
<table class="ui very basic striped table unstackable">
<thead>
<tr>
<th data-sortt-asc="newest" data-sortt-desc="oldest">
{{ctx.Locale.Tr "actions.require_action.id"}}
</th>
<th data-sortt-asc="alphabetically" data-sortt-desc="reversealphabetically">
{{ctx.Locale.Tr "actions.require_action.workflow"}}
</th>
<th>{{ctx.Locale.Tr "actions.require_action.repo"}}</th>
<th>{{ctx.Locale.Tr "actions.require_action.link"}}</th>
<th>{{ctx.Locale.Tr "actions.require_action.remove"}}</th>
</tr>
</thead>
<tbody>
{{if .RequireActions}}
{{range .RequireActions}}
<tr>
<td>{{.ID}}</td>
<td><p data-tooltip-content="{{.RepoName}}">{{.WorkflowName}}</p></td>
<td>{{.RepoName}}</td>
<td><a href="/{{$.OrgName}}/{{.RepoName}}">Workflow Link</a></td>
<td class="require_action-ops">
{{if .Removable $.OrgID}}
<button class="btn interact-bg tw-p-2 link-action"
data-tooltip-content="{{ctx.Locale.Tr "actions.require_action.deletion"}}"
data-url="{{$.Link}}/{{.ID}}/delete"
data-modal-confirm="{{ctx.Locale.Tr "actions.require_action.deletion.description"}}"
>
{{svg "octicon-trash"}}
</button>
<!-- <a href="{{$.Link}}/{{.ID}}/delete">{{svg "octicon-x-circle-fill"}}</a>-->
{{end}}
</td>
</tr>
{{end}}
{{else}}
<tr>
<td class="center aligned" colspan="8">{{ctx.Locale.Tr "actions.require_action.none"}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{template "base/paginate"}}
</div>
{{/* Add RequireAction dialog */}}
<div class="ui small modal" id="add-require-actions-modal">
<div class="header">
<span id="actions-modal-header">Enable Workflows</span>
</div>
<form class="ui form form-fetch-action" method="post">
<div class="content">
<div class="item">
<a href="https://docs.gitea.com/usage/actions/require-action">{{ctx.Locale.Tr "actions.require_action.enable_global_workflow"}}</a>
</div>
<div class="divider"></div>
<table class="ui very basic striped table unstackable">
<thead>
<tr>
<th data-sortt-asc="alphabetically" data-sortt-desc="reversealphabetically">
{{ctx.Locale.Tr "actions.require_action.workflow"}}
</th>
<th>
{{ctx.Locale.Tr "actions.require_action.repo"}}
</th>
</tr>
</thead>
<tbody>
{{if .GlobalEnableWorkflows}}
{{range .GlobalEnableWorkflows}}
<tr>
<td><div class="field">
<div class="ui radio checkbox">
<input class="select-org-radio" name="workflow_name" type="radio" value="{{.Filename}}">
<label>{{.Filename}}</label>
</div>
<input name="repo_name" type="hidden" value="{{.RepoName}}">
</div></td>
<td><div class="field">
<a href="/{{$.OrgName}}/{{.RepoName}}">
<label>{{.RepoName}}</label>
</a>
</div></td>
</tr>
{{end}}
{{else}}
<tr>
<td class="center aligned" colspan="8">{{ctx.Locale.Tr "actions.require_action.none"}}</td>
</tr>
{{end}}
</tbody>
</table>
<div class="divider"></div>
<div class="item">
<a href="{{$.Link}}/add">{{ctx.Locale.Tr "actions.require_action.add_require_action"}}</a>
</div>
</div>
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
</form>
</div>
<!--
<script>
window.addEventListener('DOMContentLoaded', function() {
var checkboxes = document.querySelectorAll('.ui.radio.checkbox');
checkboxes.forEach(function(checkbox) {
checkbox.addEventListener('change', function() {
var hiddenInput = this.nextElementSibling;
var isChecked = this.querySelector('input[type="radio"]').checked;
hiddenInput.disabled = !isChecked;
// Disable other hidden inputs
checkboxes.forEach(function(otherCheckbox) {
var otherHiddenInput = otherCheckbox.nextElementSibling;
if (otherCheckbox !== checkbox) {
otherHiddenInput.disabled = isChecked;
}
});
});
});
});
</script>
-->

View File

@ -144,7 +144,7 @@ func testNewIssue(t *testing.T, session *TestSession, user, repo, title, content
resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc = NewHTMLParser(t, resp.Body)
val := htmlDoc.doc.Find("#issue-title").Text()
val := htmlDoc.doc.Find("#issue-title-display").Text()
assert.Contains(t, val, title)
val = htmlDoc.doc.Find(".comment .render-content p").First().Text()
assert.Equal(t, content, val)

View File

@ -125,7 +125,7 @@ func TestPullCreate_TitleEscape(t *testing.T) {
req := NewRequest(t, "GET", url)
resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
editTestTitleURL, exists := htmlDoc.doc.Find("#save-edit-title").First().Attr("data-update-url")
editTestTitleURL, exists := htmlDoc.doc.Find(".issue-title-buttons button[data-update-url]").First().Attr("data-update-url")
assert.True(t, exists, "The template has changed")
req = NewRequestWithValues(t, "POST", editTestTitleURL, map[string]string{

View File

@ -575,34 +575,7 @@ td .commit-summary {
display: inline-block;
}
.issue-title-header {
width: 100%;
padding-bottom: 4px;
margin-bottom: 1rem;
}
.issue-title-meta {
display: flex;
align-items: center;
}
.repository.view.issue .issue-title-buttons,
.repository.view.issue .edit-buttons {
display: flex;
}
@media (max-width: 767.98px) {
.repository.view.issue .issue-title {
flex-direction: column;
}
.repository.view.issue .issue-title-buttons,
.repository.view.issue .edit-buttons {
width: 100%;
justify-content: space-between;
}
.repository.view.issue .edit-buttons {
margin-top: .5rem;
}
.comment.form .issue-content-left .avatar {
display: none;
}
@ -617,15 +590,37 @@ td .commit-summary {
}
}
/* issue title & meta & edit */
.issue-title-header {
width: 100%;
padding-bottom: 4px;
margin-bottom: 1rem;
}
.issue-title-meta {
display: flex;
align-items: center;
}
.repository.view.issue .issue-title-buttons {
display: flex;
gap: 0.5em;
}
.repository.view.issue .issue-title-buttons > .ui.button {
margin: 0;
height: 35px;
}
.repository.view.issue .issue-title {
display: flex;
align-items: center;
gap: 0.5em;
margin-bottom: 8px;
min-height: 40px; /* avoid layout shift on edit */
}
.repository.view.issue .issue-title h1 {
display: flex;
align-items: center;
flex: 1;
width: 100%;
font-weight: var(--font-weight-normal);
@ -633,14 +628,24 @@ td .commit-summary {
line-height: 40px;
margin: 0;
padding-right: 0.25rem;
min-height: 41px; /* avoid layout shift on edit */
}
.repository.view.issue .issue-title h1 .ui.input {
font-size: 0.5em;
@media (max-width: 767.98px) {
.repository.view.issue .issue-title {
flex-direction: column;
}
.repository.view.issue .issue-title-buttons {
width: 100%;
justify-content: space-between;
}
}
.repository.view.issue .issue-title h1 .ui.input input {
.repository.view.issue .issue-title .ui.input {
width: 100%;
height: 35px;
}
.repository.view.issue .issue-title .ui.input input {
font-size: 1.5em;
padding: 2px .5rem;
}
@ -653,10 +658,6 @@ td .commit-summary {
margin-right: 10px;
}
.issue-title .edit-zone {
margin-top: 10px;
}
.issue-state-label {
display: flex !important;
align-items: center !important;

View File

@ -47,10 +47,18 @@ export function initFootLanguageMenu() {
export function initGlobalEnterQuickSubmit() {
document.addEventListener('keydown', (e) => {
const isQuickSubmitEnter = ((e.ctrlKey && !e.altKey) || e.metaKey) && (e.key === 'Enter');
if (isQuickSubmitEnter && e.target.matches('textarea')) {
e.preventDefault();
handleGlobalEnterQuickSubmit(e.target);
if (e.key !== 'Enter') return;
const hasCtrlOrMeta = ((e.ctrlKey || e.metaKey) && !e.altKey);
if (hasCtrlOrMeta && e.target.matches('textarea')) {
if (handleGlobalEnterQuickSubmit(e.target)) {
e.preventDefault();
}
} else if (e.target.matches('input') && !e.target.closest('form')) {
// input in a normal form could handle Enter key by default, so we only handle the input outside a form
// eslint-disable-next-line unicorn/no-lonely-if
if (handleGlobalEnterQuickSubmit(e.target)) {
e.preventDefault();
}
}
});
}

View File

@ -3,16 +3,17 @@ export function handleGlobalEnterQuickSubmit(target) {
if (form) {
if (!form.checkValidity()) {
form.reportValidity();
return;
} else {
// here use the event to trigger the submit event (instead of calling `submit()` method directly)
// otherwise the `areYouSure` handler won't be executed, then there will be an annoying "confirm to leave" dialog
form.dispatchEvent(new SubmitEvent('submit', {bubbles: true, cancelable: true}));
}
// here use the event to trigger the submit event (instead of calling `submit()` method directly)
// otherwise the `areYouSure` handler won't be executed, then there will be an annoying "confirm to leave" dialog
form.dispatchEvent(new SubmitEvent('submit', {bubbles: true, cancelable: true}));
return;
return true;
}
form = target.closest('.ui.form');
if (form) {
form.querySelector('.ui.primary.button')?.click();
return true;
}
return false;
}

View File

@ -7,6 +7,7 @@ import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkd
import {toAbsoluteUrl} from '../utils.js';
import {initDropzone} from './common-global.js';
import {POST, GET} from '../modules/fetch.js';
import {showErrorToast} from '../modules/toast.js';
const {appSubUrl} = window.config;
@ -602,85 +603,69 @@ export function initRepoIssueWipToggle() {
});
}
async function pullrequest_targetbranch_change(update_url) {
const targetBranch = $('#pull-target-branch').data('branch');
const $branchTarget = $('#branch_target');
if (targetBranch === $branchTarget.text()) {
window.location.reload();
return false;
}
try {
await POST(update_url, {data: new URLSearchParams({target_branch: targetBranch})});
} catch (error) {
console.error(error);
} finally {
window.location.reload();
}
}
export function initRepoIssueTitleEdit() {
// Edit issue title
const $issueTitle = $('#issue-title');
const $editInput = $('#edit-title-input input');
const issueTitleDisplay = document.querySelector('#issue-title-display');
const issueTitleEditor = document.querySelector('#issue-title-editor');
if (!issueTitleEditor) return;
const editTitleToggle = function () {
toggleElem($issueTitle);
toggleElem('.not-in-edit');
toggleElem('#edit-title-input');
toggleElem('#pull-desc');
toggleElem('#pull-desc-edit');
toggleElem('.in-edit');
toggleElem('.new-issue-button');
document.getElementById('issue-title-wrapper')?.classList.toggle('edit-active');
$editInput[0].focus();
$editInput[0].select();
return false;
};
$('#edit-title').on('click', editTitleToggle);
$('#cancel-edit-title').on('click', editTitleToggle);
$('#save-edit-title').on('click', editTitleToggle).on('click', async function () {
const pullrequest_target_update_url = this.getAttribute('data-target-update-url');
if (!$editInput.val().length || $editInput.val() === $issueTitle.text()) {
$editInput.val($issueTitle.text());
await pullrequest_targetbranch_change(pullrequest_target_update_url);
} else {
try {
const params = new URLSearchParams();
params.append('title', $editInput.val());
const response = await POST(this.getAttribute('data-update-url'), {data: params});
const data = await response.json();
$editInput.val(data.title);
$issueTitle.text(data.title);
if (pullrequest_target_update_url) {
await pullrequest_targetbranch_change(pullrequest_target_update_url); // it will reload the window
} else {
window.location.reload();
}
} catch (error) {
console.error(error);
}
const issueTitleInput = issueTitleEditor.querySelector('input');
const oldTitle = issueTitleInput.getAttribute('data-old-title');
issueTitleDisplay.querySelector('#issue-title-edit-show').addEventListener('click', () => {
hideElem(issueTitleDisplay);
hideElem('#pull-desc-display');
showElem(issueTitleEditor);
showElem('#pull-desc-editor');
if (!issueTitleInput.value.trim()) {
issueTitleInput.value = oldTitle;
}
issueTitleInput.focus();
});
issueTitleEditor.querySelector('.ui.cancel.button').addEventListener('click', () => {
hideElem(issueTitleEditor);
hideElem('#pull-desc-editor');
showElem(issueTitleDisplay);
showElem('#pull-desc-display');
});
const editSaveButton = issueTitleEditor.querySelector('.ui.primary.button');
editSaveButton.addEventListener('click', async () => {
const prTargetUpdateUrl = editSaveButton.getAttribute('data-target-update-url');
const newTitle = issueTitleInput.value.trim();
try {
if (newTitle && newTitle !== oldTitle) {
const resp = await POST(editSaveButton.getAttribute('data-update-url'), {data: new URLSearchParams({title: newTitle})});
if (!resp.ok) {
throw new Error(`Failed to update issue title: ${resp.statusText}`);
}
}
if (prTargetUpdateUrl) {
const newTargetBranch = document.querySelector('#pull-target-branch').getAttribute('data-branch');
const oldTargetBranch = document.querySelector('#branch_target').textContent;
if (newTargetBranch !== oldTargetBranch) {
const resp = await POST(prTargetUpdateUrl, {data: new URLSearchParams({target_branch: newTargetBranch})});
if (!resp.ok) {
throw new Error(`Failed to update PR target branch: ${resp.statusText}`);
}
}
}
window.location.reload();
} catch (error) {
console.error(error);
showErrorToast(error.message);
}
return false;
});
}
export function initRepoIssueBranchSelect() {
const changeBranchSelect = function () {
const $selectionTextField = $('#pull-target-branch');
const baseName = $selectionTextField.data('basename');
const branchNameNew = $(this).data('branch');
const branchNameOld = $selectionTextField.data('branch');
// Replace branch name to keep translation from HTML template
$selectionTextField.html($selectionTextField.html().replace(
`${baseName}:${branchNameOld}`,
`${baseName}:${branchNameNew}`,
));
$selectionTextField.data('branch', branchNameNew); // update branch name in setting
};
$('#branch-select > .item').on('click', changeBranchSelect);
document.querySelector('#branch-select')?.addEventListener('click', (e) => {
const el = e.target.closest('.item[data-branch]');
if (!el) return;
const pullTargetBranch = document.querySelector('#pull-target-branch');
const baseName = pullTargetBranch.getAttribute('data-basename');
const branchNameNew = el.getAttribute('data-branch');
const branchNameOld = pullTargetBranch.getAttribute('data-branch');
pullTargetBranch.textContent = pullTargetBranch.textContent.replace(`${baseName}:${branchNameOld}`, `${baseName}:${branchNameNew}`);
pullTargetBranch.setAttribute('data-branch', branchNameNew);
});
}
export function initSingleCommentEditor($commentForm) {

View File

@ -0,0 +1,19 @@
export function initRequireActionsSelect() {
const raselect = document.getElementById('add-require-actions-modal');
if (!raselect) return;
const checkboxes = document.querySelectorAll('.ui.radio.checkbox');
for (const box of checkboxes) {
box.addEventListener('change', function() {
const hiddenInput = this.nextElementSibling;
const isChecked = this.querySelector('input[type="radio"]').checked;
hiddenInput.disabled = !isChecked;
// Disable other hidden inputs
for (const otherbox of checkboxes) {
const otherHiddenInput = otherbox.nextElementSibling;
if (otherbox !== box) {
otherHiddenInput.disabled = isChecked;
}
}
});
}
}

View File

@ -54,6 +54,7 @@ import {initRepoCodeView} from './features/repo-code.js';
import {initSshKeyFormParser} from './features/sshkey-helper.js';
import {initUserSettings} from './features/user-settings.js';
import {initRepoArchiveLinks} from './features/repo-common.js';
import {initRequireActionsSelect} from './features/require-actions-select.js';
import {initRepoMigrationStatusChecker} from './features/repo-migrate.js';
import {
initRepoSettingGitHook,
@ -143,6 +144,7 @@ onDomReady(() => {
initRepoActivityTopAuthorsChart();
initRepoArchiveLinks();
initRequireActionsSelect();
initRepoBranchButton();
initRepoCodeView();
initRepoCommentForm();