mirror of https://github.com/go-gitea/gitea.git
Compare commits
11 Commits
17462ede24
...
c6684ef8b1
Author | SHA1 | Date |
---|---|---|
AvengerMoJo | c6684ef8b1 | |
wxiaoguang | 5c236bd4c0 | |
Alex Lau(AvengerMoJo) | c4feb3d355 | |
Alex Lau(AvengerMoJo) | d68a821455 | |
Alex Lau(AvengerMoJo) | 325842db66 | |
Alex Lau(AvengerMoJo) | c4b923886c | |
Alex Lau(AvengerMoJo) | 32cb52662f | |
Alex Lau(AvengerMoJo) | c10d144ca7 | |
Alex Lau(AvengerMoJo) | d4fd5b553d | |
Alex Lau(AvengerMoJo) | 1fc998984f | |
Alex Lau(AvengerMoJo) | 5243490071 |
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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, ",")
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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("").
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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"`
|
||||
|
|
|
@ -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" .}}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
-->
|
|
@ -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)
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue