Merge branch 'main' into add-issue-planned-time

This commit is contained in:
stuzer05 2024-03-29 10:38:46 +02:00
commit 93d09fa8fc
No known key found for this signature in database
GPG Key ID: A6ABAAA9268F9F4F
76 changed files with 2928 additions and 2106 deletions

View File

@ -14,7 +14,7 @@ _test
# MS VSCode
.vscode
__debug_bin
__debug_bin*
# Architecture specific extensions/prefixes
*.[568vq]
@ -78,7 +78,6 @@ cpu.out
/public/assets/css
/public/assets/fonts
/public/assets/img/avatar
/public/assets/img/webpack
/vendor
/web_src/fomantic/node_modules
/web_src/fomantic/build/*

1
.gitignore vendored
View File

@ -77,7 +77,6 @@ cpu.out
/public/assets/css
/public/assets/fonts
/public/assets/licenses.txt
/public/assets/img/webpack
/vendor
/web_src/fomantic/node_modules
/web_src/fomantic/build/*

View File

@ -119,7 +119,7 @@ FOMANTIC_WORK_DIR := web_src/fomantic
WEBPACK_SOURCES := $(shell find web_src/js web_src/css -type f)
WEBPACK_CONFIGS := webpack.config.js tailwind.config.js
WEBPACK_DEST := public/assets/js/index.js public/assets/css/index.css
WEBPACK_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts public/assets/img/webpack
WEBPACK_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts
BINDATA_DEST := modules/public/bindata.go modules/options/bindata.go modules/templates/bindata.go
BINDATA_HASH := $(addsuffix .hash,$(BINDATA_DEST))

View File

@ -6,13 +6,11 @@ package actions
import (
"context"
"errors"
"fmt"
"strings"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
)
@ -55,24 +53,24 @@ type FindVariablesOpts struct {
db.ListOptions
OwnerID int64
RepoID int64
Name string
}
func (opts FindVariablesOpts) ToConds() builder.Cond {
cond := builder.NewCond()
// Since we now support instance-level variables,
// there is no need to check for null values for `owner_id` and `repo_id`
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
if opts.Name != "" {
cond = cond.And(builder.Eq{"name": strings.ToUpper(opts.Name)})
}
return cond
}
func GetVariableByID(ctx context.Context, variableID int64) (*ActionVariable, error) {
var variable ActionVariable
has, err := db.GetEngine(ctx).Where("id=?", variableID).Get(&variable)
if err != nil {
return nil, err
} else if !has {
return nil, fmt.Errorf("variable with id %d: %w", variableID, util.ErrNotExist)
}
return &variable, nil
func FindVariables(ctx context.Context, opts FindVariablesOpts) ([]*ActionVariable, error) {
return db.Find[ActionVariable](ctx, opts)
}
func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error) {
@ -84,6 +82,13 @@ func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error)
return count != 0, err
}
func DeleteVariable(ctx context.Context, id int64) error {
if _, err := db.DeleteByID[ActionVariable](ctx, id); err != nil {
return err
}
return nil
}
func GetVariablesOfRun(ctx context.Context, run *ActionRun) (map[string]string, error) {
variables := map[string]string{}

View File

@ -66,6 +66,23 @@ func (err ErrNotValidReviewRequest) Unwrap() error {
return util.ErrInvalidArgument
}
// ErrReviewRequestOnClosedPR represents an error when an user tries to request a re-review on a closed or merged PR.
type ErrReviewRequestOnClosedPR struct{}
// IsErrReviewRequestOnClosedPR checks if an error is an ErrReviewRequestOnClosedPR.
func IsErrReviewRequestOnClosedPR(err error) bool {
_, ok := err.(ErrReviewRequestOnClosedPR)
return ok
}
func (err ErrReviewRequestOnClosedPR) Error() string {
return "cannot request a re-review on a closed or merged PR"
}
func (err ErrReviewRequestOnClosedPR) Unwrap() error {
return util.ErrPermissionDenied
}
// ReviewType defines the sort of feedback a review gives
type ReviewType int
@ -618,9 +635,24 @@ func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_mo
return nil, err
}
// skip it when reviewer hase been request to review
if review != nil && review.Type == ReviewTypeRequest {
return nil, committer.Commit() // still commit the transaction, or committer.Close() will rollback it, even if it's a reused transaction.
if review != nil {
// skip it when reviewer hase been request to review
if review.Type == ReviewTypeRequest {
return nil, committer.Commit() // still commit the transaction, or committer.Close() will rollback it, even if it's a reused transaction.
}
if issue.IsClosed {
return nil, ErrReviewRequestOnClosedPR{}
}
if issue.IsPull {
if err := issue.LoadPullRequest(ctx); err != nil {
return nil, err
}
if issue.PullRequest.HasMerged {
return nil, ErrReviewRequestOnClosedPR{}
}
}
}
// if the reviewer is an official reviewer,

View File

@ -288,3 +288,33 @@ func TestDeleteDismissedReview(t *testing.T) {
assert.NoError(t, issues_model.DeleteReview(db.DefaultContext, review))
unittest.AssertNotExistsBean(t, &issues_model.Comment{ID: comment.ID})
}
func TestAddReviewRequest(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
assert.NoError(t, pull.LoadIssue(db.DefaultContext))
issue := pull.Issue
assert.NoError(t, issue.LoadRepo(db.DefaultContext))
reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
_, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{
Issue: issue,
Reviewer: reviewer,
Type: issues_model.ReviewTypeReject,
})
assert.NoError(t, err)
pull.HasMerged = false
assert.NoError(t, pull.UpdateCols(db.DefaultContext, "has_merged"))
issue.IsClosed = true
_, err = issues_model.AddReviewRequest(db.DefaultContext, issue, reviewer, &user_model.User{})
assert.Error(t, err)
assert.True(t, issues_model.IsErrReviewRequestOnClosedPR(err))
pull.HasMerged = true
assert.NoError(t, pull.UpdateCols(db.DefaultContext, "has_merged"))
issue.IsClosed = false
_, err = issues_model.AddReviewRequest(db.DefaultContext, issue, reviewer, &user_model.User{})
assert.Error(t, err)
assert.True(t, issues_model.IsErrReviewRequestOnClosedPR(err))
}

View File

@ -569,8 +569,10 @@ var migrations = []Migration{
// v291 -> v292
NewMigration("Add Index to attachment.comment_id", v1_22.AddCommentIDIndexofAttachment),
// v292 -> v293
NewMigration("Ensure every project has exactly one default column - No Op", noopMigration),
// v293 -> v294
NewMigration("Ensure every project has exactly one default column", v1_22.CheckProjectColumnsConsistency),
// v283 -> v294
// v284 -> v295
NewMigration("Add TimeEstimate to issue table", v1_22.AddTimeEstimateColumnToIssueTable),
}

View File

@ -3,83 +3,7 @@
package v1_22 //nolint
import (
"code.gitea.io/gitea/models/project"
"code.gitea.io/gitea/modules/setting"
"xorm.io/builder"
"xorm.io/xorm"
)
// CheckProjectColumnsConsistency ensures there is exactly one default board per project present
func CheckProjectColumnsConsistency(x *xorm.Engine) error {
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
limit := setting.Database.IterateBufferSize
if limit <= 0 {
limit = 50
}
start := 0
for {
var projects []project.Project
if err := sess.SQL("SELECT DISTINCT `p`.`id`, `p`.`creator_id` FROM `project` `p` WHERE (SELECT COUNT(*) FROM `project_board` `pb` WHERE `pb`.`project_id` = `p`.`id` AND `pb`.`default` = ?) != 1", true).
Limit(limit, start).
Find(&projects); err != nil {
return err
}
if len(projects) == 0 {
break
}
start += len(projects)
for _, p := range projects {
var boards []project.Board
if err := sess.Where("project_id=? AND `default` = ?", p.ID, true).OrderBy("sorting").Find(&boards); err != nil {
return err
}
if len(boards) == 0 {
if _, err := sess.Insert(project.Board{
ProjectID: p.ID,
Default: true,
Title: "Uncategorized",
CreatorID: p.CreatorID,
}); err != nil {
return err
}
continue
}
var boardsToUpdate []int64
for id, b := range boards {
if id > 0 {
boardsToUpdate = append(boardsToUpdate, b.ID)
}
}
if _, err := sess.Where(builder.Eq{"project_id": p.ID}.And(builder.In("id", boardsToUpdate))).
Cols("`default`").Update(&project.Board{Default: false}); err != nil {
return err
}
}
if start%1000 == 0 {
if err := sess.Commit(); err != nil {
return err
}
if err := sess.Begin(); err != nil {
return err
}
}
}
return sess.Commit()
}
// NOTE: noop the original migration has bug which some projects will be skip, so
// these projects will have no default board.
// So that this migration will be skipped and go to v293.go
// This file is a placeholder so that readers can know what happened

View File

@ -1,16 +1,108 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_22 //nolint
import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/xorm"
)
func AddTimeEstimateColumnToIssueTable(x *xorm.Engine) error {
type Issue struct {
TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"`
// CheckProjectColumnsConsistency ensures there is exactly one default board per project present
func CheckProjectColumnsConsistency(x *xorm.Engine) error {
sess := x.NewSession()
defer sess.Close()
limit := setting.Database.IterateBufferSize
if limit <= 0 {
limit = 50
}
return x.Sync(new(Issue))
type Project struct {
ID int64
CreatorID int64
BoardID int64
}
type ProjectBoard struct {
ID int64 `xorm:"pk autoincr"`
Title string
Default bool `xorm:"NOT NULL DEFAULT false"` // issues not assigned to a specific board will be assigned to this board
Sorting int8 `xorm:"NOT NULL DEFAULT 0"`
Color string `xorm:"VARCHAR(7)"`
ProjectID int64 `xorm:"INDEX NOT NULL"`
CreatorID int64 `xorm:"NOT NULL"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
for {
if err := sess.Begin(); err != nil {
return err
}
// all these projects without defaults will be fixed in the same loop, so
// we just need to always get projects without defaults until no such project
var projects []*Project
if err := sess.Select("project.id as id, project.creator_id, project_board.id as board_id").
Join("LEFT", "project_board", "project_board.project_id = project.id AND project_board.`default`=?", true).
Where("project_board.id is NULL OR project_board.id = 0").
Limit(limit).
Find(&projects); err != nil {
return err
}
for _, p := range projects {
if _, err := sess.Insert(ProjectBoard{
ProjectID: p.ID,
Default: true,
Title: "Uncategorized",
CreatorID: p.CreatorID,
}); err != nil {
return err
}
}
if err := sess.Commit(); err != nil {
return err
}
if len(projects) == 0 {
break
}
}
sess.Close()
return removeDuplicatedBoardDefault(x)
}
func removeDuplicatedBoardDefault(x *xorm.Engine) error {
type ProjectInfo struct {
ProjectID int64
DefaultNum int
}
var projects []ProjectInfo
if err := x.Select("project_id, count(*) AS default_num").
Table("project_board").
Where("`default` = ?", true).
GroupBy("project_id").
Having("count(*) > 1").
Find(&projects); err != nil {
return err
}
for _, project := range projects {
if _, err := x.Where("project_id=?", project.ProjectID).
Table("project_board").
Limit(project.DefaultNum - 1).
Update(map[string]bool{
"`default`": false,
}); err != nil {
return err
}
}
return nil
}

View File

@ -31,14 +31,14 @@ func Test_CheckProjectColumnsConsistency(t *testing.T) {
assert.Equal(t, int64(1), defaultBoard.ProjectID)
assert.True(t, defaultBoard.Default)
// check if multiple defaults were removed
// check if multiple defaults, previous were removed and last will be kept
expectDefaultBoard, err := project.GetBoard(db.DefaultContext, 2)
assert.NoError(t, err)
assert.Equal(t, int64(2), expectDefaultBoard.ProjectID)
assert.True(t, expectDefaultBoard.Default)
assert.False(t, expectDefaultBoard.Default)
expectNonDefaultBoard, err := project.GetBoard(db.DefaultContext, 3)
assert.NoError(t, err)
assert.Equal(t, int64(2), expectNonDefaultBoard.ProjectID)
assert.False(t, expectNonDefaultBoard.Default)
assert.True(t, expectNonDefaultBoard.Default)
}

View File

@ -0,0 +1,16 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_22 //nolint
import (
"xorm.io/xorm"
)
func AddTimeEstimateColumnToIssueTable(x *xorm.Engine) error {
type Issue struct {
TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"`
}
return x.Sync(new(Issue))
}

View File

@ -209,7 +209,6 @@ func deleteBoardByProjectID(ctx context.Context, projectID int64) error {
// GetBoard fetches the current board of a project
func GetBoard(ctx context.Context, boardID int64) (*Board, error) {
board := new(Board)
has, err := db.GetEngine(ctx).ID(boardID).Get(board)
if err != nil {
return nil, err
@ -260,71 +259,62 @@ func (p *Project) GetBoards(ctx context.Context) (BoardList, error) {
// getDefaultBoard return default board and ensure only one exists
func (p *Project) getDefaultBoard(ctx context.Context) (*Board, error) {
var boards []Board
if err := db.GetEngine(ctx).Where("project_id=? AND `default` = ?", p.ID, true).OrderBy("sorting").Find(&boards); err != nil {
var board Board
has, err := db.GetEngine(ctx).
Where("project_id=? AND `default` = ?", p.ID, true).
Desc("id").Get(&board)
if err != nil {
return nil, err
}
// create a default board if none is found
if len(boards) == 0 {
board := Board{
ProjectID: p.ID,
Default: true,
Title: "Uncategorized",
CreatorID: p.CreatorID,
}
if _, err := db.GetEngine(ctx).Insert(); err != nil {
return nil, err
}
if has {
return &board, nil
}
// unset default boards where too many default boards exist
if len(boards) > 1 {
var boardsToUpdate []int64
for id, b := range boards {
if id > 0 {
boardsToUpdate = append(boardsToUpdate, b.ID)
}
}
if _, err := db.GetEngine(ctx).Where(builder.Eq{"project_id": p.ID}.And(builder.In("id", boardsToUpdate))).
Cols("`default`").Update(&Board{Default: false}); err != nil {
return nil, err
}
// create a default board if none is found
board = Board{
ProjectID: p.ID,
Default: true,
Title: "Uncategorized",
CreatorID: p.CreatorID,
}
return &boards[0], nil
if _, err := db.GetEngine(ctx).Insert(&board); err != nil {
return nil, err
}
return &board, nil
}
// SetDefaultBoard represents a board for issues not assigned to one
func SetDefaultBoard(ctx context.Context, projectID, boardID int64) error {
if _, err := GetBoard(ctx, boardID); err != nil {
return db.WithTx(ctx, func(ctx context.Context) error {
if _, err := GetBoard(ctx, boardID); err != nil {
return err
}
if _, err := db.GetEngine(ctx).Where(builder.Eq{
"project_id": projectID,
"`default`": true,
}).Cols("`default`").Update(&Board{Default: false}); err != nil {
return err
}
_, err := db.GetEngine(ctx).ID(boardID).
Where(builder.Eq{"project_id": projectID}).
Cols("`default`").Update(&Board{Default: true})
return err
}
if _, err := db.GetEngine(ctx).Where(builder.Eq{
"project_id": projectID,
"`default`": true,
}).Cols("`default`").Update(&Board{Default: false}); err != nil {
return err
}
_, err := db.GetEngine(ctx).ID(boardID).Where(builder.Eq{"project_id": projectID}).
Cols("`default`").Update(&Board{Default: true})
return err
})
}
// UpdateBoardSorting update project board sorting
func UpdateBoardSorting(ctx context.Context, bs BoardList) error {
for i := range bs {
_, err := db.GetEngine(ctx).ID(bs[i].ID).Cols(
"sorting",
).Update(bs[i])
if err != nil {
return err
return db.WithTx(ctx, func(ctx context.Context) error {
for i := range bs {
if _, err := db.GetEngine(ctx).ID(bs[i].ID).Cols(
"sorting",
).Update(bs[i]); err != nil {
return err
}
}
}
return nil
return nil
})
}

View File

@ -31,8 +31,12 @@ func TestGetDefaultBoard(t *testing.T) {
board, err = projectWithMultipleDefaults.getDefaultBoard(db.DefaultContext)
assert.NoError(t, err)
assert.Equal(t, int64(6), board.ProjectID)
assert.Equal(t, int64(8), board.ID)
assert.Equal(t, int64(9), board.ID)
// set 8 as default board
assert.NoError(t, SetDefaultBoard(db.DefaultContext, board.ProjectID, 8))
// then 9 will become a non-default board
board, err = GetBoard(db.DefaultContext, 9)
assert.NoError(t, err)
assert.Equal(t, int64(6), board.ProjectID)

View File

@ -178,7 +178,7 @@ type FindTopicOptions struct {
Keyword string
}
func (opts *FindTopicOptions) toConds() builder.Cond {
func (opts *FindTopicOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.RepoID > 0 {
cond = cond.And(builder.Eq{"repo_topic.repo_id": opts.RepoID})
@ -191,29 +191,24 @@ func (opts *FindTopicOptions) toConds() builder.Cond {
return cond
}
// FindTopics retrieves the topics via FindTopicOptions
func FindTopics(ctx context.Context, opts *FindTopicOptions) ([]*Topic, int64, error) {
sess := db.GetEngine(ctx).Select("topic.*").Where(opts.toConds())
func (opts *FindTopicOptions) ToOrders() string {
orderBy := "topic.repo_count DESC"
if opts.RepoID > 0 {
sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id")
orderBy = "topic.name" // when render topics for a repo, it's better to sort them by name, to get consistent result
}
if opts.PageSize != 0 && opts.Page != 0 {
sess = db.SetSessionPagination(sess, opts)
}
topics := make([]*Topic, 0, 10)
total, err := sess.OrderBy(orderBy).FindAndCount(&topics)
return topics, total, err
return orderBy
}
// CountTopics counts the number of topics matching the FindTopicOptions
func CountTopics(ctx context.Context, opts *FindTopicOptions) (int64, error) {
sess := db.GetEngine(ctx).Where(opts.toConds())
if opts.RepoID > 0 {
sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id")
func (opts *FindTopicOptions) ToJoins() []db.JoinFunc {
if opts.RepoID <= 0 {
return nil
}
return []db.JoinFunc{
func(e db.Engine) error {
e.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id")
return nil
},
}
return sess.Count(new(Topic))
}
// GetRepoTopicByName retrieves topic from name for a repo if it exist
@ -283,7 +278,7 @@ func DeleteTopic(ctx context.Context, repoID int64, topicName string) (*Topic, e
// SaveTopics save topics to a repository
func SaveTopics(ctx context.Context, repoID int64, topicNames ...string) error {
topics, _, err := FindTopics(ctx, &FindTopicOptions{
topics, err := db.Find[Topic](ctx, &FindTopicOptions{
RepoID: repoID,
})
if err != nil {

View File

@ -19,18 +19,18 @@ func TestAddTopic(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
topics, _, err := repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{})
topics, err := db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{})
assert.NoError(t, err)
assert.Len(t, topics, totalNrOfTopics)
topics, total, err := repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{
topics, total, err := db.FindAndCount[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{
ListOptions: db.ListOptions{Page: 1, PageSize: 2},
})
assert.NoError(t, err)
assert.Len(t, topics, 2)
assert.EqualValues(t, 6, total)
topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{
topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{
RepoID: 1,
})
assert.NoError(t, err)
@ -38,11 +38,11 @@ func TestAddTopic(t *testing.T) {
assert.NoError(t, repo_model.SaveTopics(db.DefaultContext, 2, "golang"))
repo2NrOfTopics := 1
topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{})
topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{})
assert.NoError(t, err)
assert.Len(t, topics, totalNrOfTopics)
topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{
topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{
RepoID: 2,
})
assert.NoError(t, err)
@ -55,11 +55,11 @@ func TestAddTopic(t *testing.T) {
assert.NoError(t, err)
assert.EqualValues(t, 1, topic.RepoCount)
topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{})
topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{})
assert.NoError(t, err)
assert.Len(t, topics, totalNrOfTopics)
topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{
topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{
RepoID: 2,
})
assert.NoError(t, err)

View File

@ -0,0 +1,37 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package structs
// CreateVariableOption the option when creating variable
// swagger:model
type CreateVariableOption struct {
// Value of the variable to create
//
// required: true
Value string `json:"value" binding:"Required"`
}
// UpdateVariableOption the option when updating variable
// swagger:model
type UpdateVariableOption struct {
// New name for the variable. If the field is empty, the variable name won't be updated.
Name string `json:"name"`
// Value of the variable to update
//
// required: true
Value string `json:"value" binding:"Required"`
}
// ActionVariable return value of the query API
// swagger:model
type ActionVariable struct {
// the owner to which the variable belongs
OwnerID int64 `json:"owner_id"`
// the repository to which the variable belongs
RepoID int64 `json:"repo_id"`
// the name of the variable
Name string `json:"name"`
// the value of the variable
Data string `json:"data"`
}

View File

@ -41,7 +41,7 @@ func RenderCommitMessage(ctx context.Context, msg string, metas map[string]strin
if len(msgLines) == 0 {
return template.HTML("")
}
return template.HTML(msgLines[0])
return RenderCodeBlock(template.HTML(msgLines[0]))
}
// RenderCommitMessageLinkSubject renders commit message as a XSS-safe link to
@ -68,7 +68,7 @@ func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlDefault string,
log.Error("RenderCommitMessageSubject: %v", err)
return template.HTML("")
}
return template.HTML(renderedMessage)
return RenderCodeBlock(template.HTML(renderedMessage))
}
// RenderCommitBody extracts the body of a commit message without its title.

View File

@ -221,3 +221,12 @@ func IfZero[T comparable](v, def T) T {
}
return v
}
func ReserveLineBreakForTextarea(input string) string {
// Since the content is from a form which is a textarea, the line endings are \r\n.
// It's a standard behavior of HTML.
// But we want to store them as \n like what GitHub does.
// And users are unlikely to really need to keep the \r.
// Other than this, we should respect the original content, even leading or trailing spaces.
return strings.ReplaceAll(input, "\r\n", "\n")
}

View File

@ -235,3 +235,8 @@ func TestToPointer(t *testing.T) {
val123 := 123
assert.False(t, &val123 == ToPointer(val123))
}
func TestReserveLineBreakForTextarea(t *testing.T) {
assert.Equal(t, ReserveLineBreakForTextarea("test\r\ndata"), "test\ndata")
assert.Equal(t, ReserveLineBreakForTextarea("test\r\ndata\r\n"), "test\ndata\n")
}

23
package-lock.json generated
View File

@ -9,11 +9,11 @@
"@citation-js/plugin-bibtex": "0.7.9",
"@citation-js/plugin-csl": "0.7.9",
"@citation-js/plugin-software-formats": "0.6.1",
"@claviska/jquery-minicolors": "2.3.6",
"@github/markdown-toolbar-element": "2.2.3",
"@github/relative-time-element": "4.3.1",
"@github/relative-time-element": "4.4.0",
"@github/text-expander-element": "2.6.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@melloware/coloris": "0.23.0",
"@primer/octicons": "19.9.0",
"add-asset-webpack-plugin": "2.0.1",
"ansi_up": "6.0.2",
@ -394,14 +394,6 @@
"node": ">=14.0.0"
}
},
"node_modules/@claviska/jquery-minicolors": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/@claviska/jquery-minicolors/-/jquery-minicolors-2.3.6.tgz",
"integrity": "sha512-8Ro6D4GCrmOl41+6w4NFhEOpx8vjxwVRI69bulXsFDt49uVRKhLU5TnzEV7AmOJrylkVq+ugnYNMiGHBieeKUQ==",
"peerDependencies": {
"jquery": ">= 1.7.x"
}
},
"node_modules/@csstools/css-parser-algorithms": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.1.tgz",
@ -1004,9 +996,9 @@
"integrity": "sha512-AlquKGee+IWiAMYVB0xyHFZRMnu4n3X4HTvJHu79GiVJ1ojTukCWyxMlF5NMsecoLcBKsuBhx3QPv2vkE/zQ0A=="
},
"node_modules/@github/relative-time-element": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.3.1.tgz",
"integrity": "sha512-zL79nlhZVCg7x2Pf/HT5MB0mowmErE71VXpF10/3Wy8dQwkninNO1M9aOizh2wKC5LkSpDXqNYjDZwbH0/bcSg=="
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.4.0.tgz",
"integrity": "sha512-CrI6oAecoahG7PF5dsgjdvlF5kCtusVMjg810EULD81TvnDsP+k/FRi/ClFubWLgBo4EGpr2EfvmumtqQFo7ow=="
},
"node_modules/@github/text-expander-element": {
"version": "2.6.1",
@ -1297,6 +1289,11 @@
"@mcaptcha/core-glue": "^0.1.0-alpha-5"
}
},
"node_modules/@melloware/coloris": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@melloware/coloris/-/coloris-0.23.0.tgz",
"integrity": "sha512-VGIjI9+IQwg6BHjIE10yl0K2ARYz5bsjn6BgFEs1y1ErPAQymgdoxwVcSVL4Ai5t9OVs8xaCB7JKHqFu2N96Ow=="
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",

View File

@ -8,11 +8,11 @@
"@citation-js/plugin-bibtex": "0.7.9",
"@citation-js/plugin-csl": "0.7.9",
"@citation-js/plugin-software-formats": "0.6.1",
"@claviska/jquery-minicolors": "2.3.6",
"@github/markdown-toolbar-element": "2.2.3",
"@github/relative-time-element": "4.3.1",
"@github/relative-time-element": "4.4.0",
"@github/text-expander-element": "2.6.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@melloware/coloris": "0.23.0",
"@primer/octicons": "19.9.0",
"add-asset-webpack-plugin": "2.0.1",
"ansi_up": "6.0.2",

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

@ -955,6 +955,15 @@ func Routes() *web.Route {
Delete(user.DeleteSecret)
})
m.Group("/variables", func() {
m.Get("", user.ListVariables)
m.Combo("/{variablename}").
Get(user.GetVariable).
Delete(user.DeleteVariable).
Post(bind(api.CreateVariableOption{}), user.CreateVariable).
Put(bind(api.UpdateVariableOption{}), user.UpdateVariable)
})
m.Group("/runners", func() {
m.Get("/registration-token", reqToken(), user.GetRegistrationToken)
})
@ -1073,6 +1082,15 @@ func Routes() *web.Route {
Delete(reqToken(), reqOwner(), repo.DeleteSecret)
})
m.Group("/variables", func() {
m.Get("", reqToken(), reqOwner(), repo.ListVariables)
m.Combo("/{variablename}").
Get(reqToken(), reqOwner(), repo.GetVariable).
Delete(reqToken(), reqOwner(), repo.DeleteVariable).
Post(reqToken(), reqOwner(), bind(api.CreateVariableOption{}), repo.CreateVariable).
Put(reqToken(), reqOwner(), bind(api.UpdateVariableOption{}), repo.UpdateVariable)
})
m.Group("/runners", func() {
m.Get("/registration-token", reqToken(), reqOwner(), repo.GetRegistrationToken)
})
@ -1452,6 +1470,15 @@ func Routes() *web.Route {
Delete(reqToken(), reqOrgOwnership(), org.DeleteSecret)
})
m.Group("/variables", func() {
m.Get("", reqToken(), reqOrgOwnership(), org.ListVariables)
m.Combo("/{variablename}").
Get(reqToken(), reqOrgOwnership(), org.GetVariable).
Delete(reqToken(), reqOrgOwnership(), org.DeleteVariable).
Post(reqToken(), reqOrgOwnership(), bind(api.CreateVariableOption{}), org.CreateVariable).
Put(reqToken(), reqOrgOwnership(), bind(api.UpdateVariableOption{}), org.UpdateVariable)
})
m.Group("/runners", func() {
m.Get("/registration-token", reqToken(), reqOrgOwnership(), org.GetRegistrationToken)
})

View File

@ -0,0 +1,291 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"errors"
"net/http"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context"
)
// ListVariables list org-level variables
func ListVariables(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/variables organization getOrgVariablesList
// ---
// summary: Get an org-level variables list
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/VariableList"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{
OwnerID: ctx.Org.Organization.ID,
ListOptions: utils.GetListOptions(ctx),
})
if err != nil {
ctx.Error(http.StatusInternalServerError, "FindVariables", err)
return
}
variables := make([]*api.ActionVariable, len(vars))
for i, v := range vars {
variables[i] = &api.ActionVariable{
OwnerID: v.OwnerID,
RepoID: v.RepoID,
Name: v.Name,
Data: v.Data,
}
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, variables)
}
// GetVariable get an org-level variable
func GetVariable(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/variables/{variablename} organization getOrgVariable
// ---
// summary: Get an org-level variable
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: variablename
// in: path
// description: name of the variable
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/ActionVariable"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
OwnerID: ctx.Org.Organization.ID,
Name: ctx.Params("variablename"),
})
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "GetVariable", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetVariable", err)
}
return
}
variable := &api.ActionVariable{
OwnerID: v.OwnerID,
RepoID: v.RepoID,
Name: v.Name,
Data: v.Data,
}
ctx.JSON(http.StatusOK, variable)
}
// DeleteVariable delete an org-level variable
func DeleteVariable(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/actions/variables/{variablename} organization deleteOrgVariable
// ---
// summary: Delete an org-level variable
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: variablename
// in: path
// description: name of the variable
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/ActionVariable"
// "201":
// description: response when deleting a variable
// "204":
// description: response when deleting a variable
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
if err := actions_service.DeleteVariableByName(ctx, ctx.Org.Organization.ID, 0, ctx.Params("variablename")); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "DeleteVariableByName", err)
} else if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "DeleteVariableByName", err)
} else {
ctx.Error(http.StatusInternalServerError, "DeleteVariableByName", err)
}
return
}
ctx.Status(http.StatusNoContent)
}
// CreateVariable create an org-level variable
func CreateVariable(ctx *context.APIContext) {
// swagger:operation POST /orgs/{org}/actions/variables/{variablename} organization createOrgVariable
// ---
// summary: Create an org-level variable
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: variablename
// in: path
// description: name of the variable
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateVariableOption"
// responses:
// "201":
// description: response when creating an org-level variable
// "204":
// description: response when creating an org-level variable
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
opt := web.GetForm(ctx).(*api.CreateVariableOption)
ownerID := ctx.Org.Organization.ID
variableName := ctx.Params("variablename")
v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
OwnerID: ownerID,
Name: variableName,
})
if err != nil && !errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusInternalServerError, "GetVariable", err)
return
}
if v != nil && v.ID > 0 {
ctx.Error(http.StatusConflict, "VariableNameAlreadyExists", util.NewAlreadyExistErrorf("variable name %s already exists", variableName))
return
}
if _, err := actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "CreateVariable", err)
} else {
ctx.Error(http.StatusInternalServerError, "CreateVariable", err)
}
return
}
ctx.Status(http.StatusNoContent)
}
// UpdateVariable update an org-level variable
func UpdateVariable(ctx *context.APIContext) {
// swagger:operation PUT /orgs/{org}/actions/variables/{variablename} organization updateOrgVariable
// ---
// summary: Update an org-level variable
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: variablename
// in: path
// description: name of the variable
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/UpdateVariableOption"
// responses:
// "201":
// description: response when updating an org-level variable
// "204":
// description: response when updating an org-level variable
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
opt := web.GetForm(ctx).(*api.UpdateVariableOption)
v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
OwnerID: ctx.Org.Organization.ID,
Name: ctx.Params("variablename"),
})
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "GetVariable", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetVariable", err)
}
return
}
if opt.Name == "" {
opt.Name = ctx.Params("variablename")
}
if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
} else {
ctx.Error(http.StatusInternalServerError, "UpdateVariable", err)
}
return
}
ctx.Status(http.StatusNoContent)
}

View File

@ -7,9 +7,13 @@ import (
"errors"
"net/http"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context"
secret_service "code.gitea.io/gitea/services/secrets"
)
@ -127,3 +131,295 @@ func DeleteSecret(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent)
}
// GetVariable get a repo-level variable
func GetVariable(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/variables/{variablename} repository getRepoVariable
// ---
// summary: Get a repo-level variable
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: name of the owner
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repository
// type: string
// required: true
// - name: variablename
// in: path
// description: name of the variable
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/ActionVariable"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
RepoID: ctx.Repo.Repository.ID,
Name: ctx.Params("variablename"),
})
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "GetVariable", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetVariable", err)
}
return
}
variable := &api.ActionVariable{
OwnerID: v.OwnerID,
RepoID: v.RepoID,
Name: v.Name,
Data: v.Data,
}
ctx.JSON(http.StatusOK, variable)
}
// DeleteVariable delete a repo-level variable
func DeleteVariable(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/actions/variables/{variablename} repository deleteRepoVariable
// ---
// summary: Delete a repo-level variable
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: name of the owner
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repository
// type: string
// required: true
// - name: variablename
// in: path
// description: name of the variable
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/ActionVariable"
// "201":
// description: response when deleting a variable
// "204":
// description: response when deleting a variable
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
if err := actions_service.DeleteVariableByName(ctx, 0, ctx.Repo.Repository.ID, ctx.Params("variablename")); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "DeleteVariableByName", err)
} else if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "DeleteVariableByName", err)
} else {
ctx.Error(http.StatusInternalServerError, "DeleteVariableByName", err)
}
return
}
ctx.Status(http.StatusNoContent)
}
// CreateVariable create a repo-level variable
func CreateVariable(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/actions/variables/{variablename} repository createRepoVariable
// ---
// summary: Create a repo-level variable
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: name of the owner
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repository
// type: string
// required: true
// - name: variablename
// in: path
// description: name of the variable
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateVariableOption"
// responses:
// "201":
// description: response when creating a repo-level variable
// "204":
// description: response when creating a repo-level variable
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
opt := web.GetForm(ctx).(*api.CreateVariableOption)
repoID := ctx.Repo.Repository.ID
variableName := ctx.Params("variablename")
v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
RepoID: repoID,
Name: variableName,
})
if err != nil && !errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusInternalServerError, "GetVariable", err)
return
}
if v != nil && v.ID > 0 {
ctx.Error(http.StatusConflict, "VariableNameAlreadyExists", util.NewAlreadyExistErrorf("variable name %s already exists", variableName))
return
}
if _, err := actions_service.CreateVariable(ctx, 0, repoID, variableName, opt.Value); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "CreateVariable", err)
} else {
ctx.Error(http.StatusInternalServerError, "CreateVariable", err)
}
return
}
ctx.Status(http.StatusNoContent)
}
// UpdateVariable update a repo-level variable
func UpdateVariable(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/actions/variables/{variablename} repository updateRepoVariable
// ---
// summary: Update a repo-level variable
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: name of the owner
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repository
// type: string
// required: true
// - name: variablename
// in: path
// description: name of the variable
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/UpdateVariableOption"
// responses:
// "201":
// description: response when updating a repo-level variable
// "204":
// description: response when updating a repo-level variable
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
opt := web.GetForm(ctx).(*api.UpdateVariableOption)
v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
RepoID: ctx.Repo.Repository.ID,
Name: ctx.Params("variablename"),
})
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "GetVariable", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetVariable", err)
}
return
}
if opt.Name == "" {
opt.Name = ctx.Params("variablename")
}
if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
} else {
ctx.Error(http.StatusInternalServerError, "UpdateVariable", err)
}
return
}
ctx.Status(http.StatusNoContent)
}
// ListVariables list repo-level variables
func ListVariables(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/variables repository getRepoVariablesList
// ---
// summary: Get repo-level variables list
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: name of the owner
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repository
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/VariableList"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{
RepoID: ctx.Repo.Repository.ID,
ListOptions: utils.GetListOptions(ctx),
})
if err != nil {
ctx.Error(http.StatusInternalServerError, "FindVariables", err)
return
}
variables := make([]*api.ActionVariable, len(vars))
for i, v := range vars {
variables[i] = &api.ActionVariable{
OwnerID: v.OwnerID,
RepoID: v.RepoID,
Name: v.Name,
}
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, variables)
}

View File

@ -640,6 +640,8 @@ func DeleteReviewRequests(ctx *context.APIContext) {
// "$ref": "#/responses/empty"
// "422":
// "$ref": "#/responses/validationError"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
opts := web.GetForm(ctx).(*api.PullReviewRequestOptions)
@ -708,6 +710,10 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
for _, reviewer := range reviewers {
comment, err := issue_service.ReviewRequest(ctx, pr.Issue, ctx.Doer, reviewer, isAdd)
if err != nil {
if issues_model.IsErrReviewRequestOnClosedPR(err) {
ctx.Error(http.StatusForbidden, "", err)
return
}
ctx.Error(http.StatusInternalServerError, "ReviewRequest", err)
return
}
@ -874,7 +880,7 @@ func dismissReview(ctx *context.APIContext, msg string, isDismiss, dismissPriors
ctx.Error(http.StatusForbidden, "", "Must be repo admin")
return
}
review, pr, isWrong := prepareSingleReview(ctx)
review, _, isWrong := prepareSingleReview(ctx)
if isWrong {
return
}
@ -884,13 +890,12 @@ func dismissReview(ctx *context.APIContext, msg string, isDismiss, dismissPriors
return
}
if pr.Issue.IsClosed {
ctx.Error(http.StatusForbidden, "", "not need to dismiss this review because this pr is closed")
return
}
_, err := pull_service.DismissReview(ctx, review.ID, ctx.Repo.Repository.ID, msg, ctx.Doer, isDismiss, dismissPriors)
if err != nil {
if pull_service.IsErrDismissRequestOnClosedPR(err) {
ctx.Error(http.StatusForbidden, "", err)
return
}
ctx.Error(http.StatusInternalServerError, "pull_service.DismissReview", err)
return
}

View File

@ -7,6 +7,7 @@ import (
"net/http"
"strings"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
@ -53,7 +54,7 @@ func ListTopics(ctx *context.APIContext) {
RepoID: ctx.Repo.Repository.ID,
}
topics, total, err := repo_model.FindTopics(ctx, opts)
topics, total, err := db.FindAndCount[repo_model.Topic](ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
@ -172,7 +173,7 @@ func AddTopic(ctx *context.APIContext) {
}
// Prevent adding more topics than allowed to repo
count, err := repo_model.CountTopics(ctx, &repo_model.FindTopicOptions{
count, err := db.Count[repo_model.Topic](ctx, &repo_model.FindTopicOptions{
RepoID: ctx.Repo.Repository.ID,
})
if err != nil {
@ -287,7 +288,7 @@ func TopicSearch(ctx *context.APIContext) {
ListOptions: utils.GetListOptions(ctx),
}
topics, total, err := repo_model.FindTopics(ctx, opts)
topics, total, err := db.FindAndCount[repo_model.Topic](ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return

View File

@ -18,3 +18,17 @@ type swaggerResponseSecret struct {
// in:body
Body api.Secret `json:"body"`
}
// ActionVariable
// swagger:response ActionVariable
type swaggerResponseActionVariable struct {
// in:body
Body api.ActionVariable `json:"body"`
}
// VariableList
// swagger:response VariableList
type swaggerResponseVariableList struct {
// in:body
Body []api.ActionVariable `json:"body"`
}

View File

@ -193,4 +193,10 @@ type swaggerParameterBodies struct {
// in:body
UserBadgeOption api.UserBadgeOption
// in:body
CreateVariableOption api.CreateVariableOption
// in:body
UpdateVariableOption api.UpdateVariableOption
}

View File

@ -7,9 +7,13 @@ import (
"errors"
"net/http"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context"
secret_service "code.gitea.io/gitea/services/secrets"
)
@ -101,3 +105,249 @@ func DeleteSecret(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent)
}
// CreateVariable create a user-level variable
func CreateVariable(ctx *context.APIContext) {
// swagger:operation POST /user/actions/variables/{variablename} user createUserVariable
// ---
// summary: Create a user-level variable
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: variablename
// in: path
// description: name of the variable
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateVariableOption"
// responses:
// "201":
// description: response when creating a variable
// "204":
// description: response when creating a variable
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
opt := web.GetForm(ctx).(*api.CreateVariableOption)
ownerID := ctx.Doer.ID
variableName := ctx.Params("variablename")
v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
OwnerID: ownerID,
Name: variableName,
})
if err != nil && !errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusInternalServerError, "GetVariable", err)
return
}
if v != nil && v.ID > 0 {
ctx.Error(http.StatusConflict, "VariableNameAlreadyExists", util.NewAlreadyExistErrorf("variable name %s already exists", variableName))
return
}
if _, err := actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "CreateVariable", err)
} else {
ctx.Error(http.StatusInternalServerError, "CreateVariable", err)
}
return
}
ctx.Status(http.StatusNoContent)
}
// UpdateVariable update a user-level variable which is created by current doer
func UpdateVariable(ctx *context.APIContext) {
// swagger:operation PUT /user/actions/variables/{variablename} user updateUserVariable
// ---
// summary: Update a user-level variable which is created by current doer
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: variablename
// in: path
// description: name of the variable
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/UpdateVariableOption"
// responses:
// "201":
// description: response when updating a variable
// "204":
// description: response when updating a variable
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
opt := web.GetForm(ctx).(*api.UpdateVariableOption)
v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
OwnerID: ctx.Doer.ID,
Name: ctx.Params("variablename"),
})
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "GetVariable", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetVariable", err)
}
return
}
if opt.Name == "" {
opt.Name = ctx.Params("variablename")
}
if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
} else {
ctx.Error(http.StatusInternalServerError, "UpdateVariable", err)
}
return
}
ctx.Status(http.StatusNoContent)
}
// DeleteVariable delete a user-level variable which is created by current doer
func DeleteVariable(ctx *context.APIContext) {
// swagger:operation DELETE /user/actions/variables/{variablename} user deleteUserVariable
// ---
// summary: Delete a user-level variable which is created by current doer
// produces:
// - application/json
// parameters:
// - name: variablename
// in: path
// description: name of the variable
// type: string
// required: true
// responses:
// "201":
// description: response when deleting a variable
// "204":
// description: response when deleting a variable
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
if err := actions_service.DeleteVariableByName(ctx, ctx.Doer.ID, 0, ctx.Params("variablename")); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "DeleteVariableByName", err)
} else if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "DeleteVariableByName", err)
} else {
ctx.Error(http.StatusInternalServerError, "DeleteVariableByName", err)
}
return
}
ctx.Status(http.StatusNoContent)
}
// GetVariable get a user-level variable which is created by current doer
func GetVariable(ctx *context.APIContext) {
// swagger:operation GET /user/actions/variables/{variablename} user getUserVariable
// ---
// summary: Get a user-level variable which is created by current doer
// produces:
// - application/json
// parameters:
// - name: variablename
// in: path
// description: name of the variable
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/ActionVariable"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
OwnerID: ctx.Doer.ID,
Name: ctx.Params("variablename"),
})
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "GetVariable", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetVariable", err)
}
return
}
variable := &api.ActionVariable{
OwnerID: v.OwnerID,
RepoID: v.RepoID,
Name: v.Name,
Data: v.Data,
}
ctx.JSON(http.StatusOK, variable)
}
// ListVariables list user-level variables
func ListVariables(ctx *context.APIContext) {
// swagger:operation GET /user/actions/variables user getUserVariablesList
// ---
// summary: Get the user-level list of variables which is created by current doer
// produces:
// - application/json
// parameters:
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/VariableList"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{
OwnerID: ctx.Doer.ID,
ListOptions: utils.GetListOptions(ctx),
})
if err != nil {
ctx.Error(http.StatusInternalServerError, "FindVariables", err)
return
}
variables := make([]*api.ActionVariable, len(vars))
for i, v := range vars {
variables[i] = &api.ActionVariable{
OwnerID: v.OwnerID,
RepoID: v.RepoID,
Name: v.Name,
Data: v.Data,
}
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, variables)
}

View File

@ -23,7 +23,7 @@ func TopicSearch(ctx *context.Context) {
},
}
topics, total, err := repo_model.FindTopics(ctx, opts)
topics, total, err := db.FindAndCount[repo_model.Topic](ctx, opts)
if err != nil {
ctx.Error(http.StatusInternalServerError)
return

View File

@ -2553,6 +2553,10 @@ func UpdatePullReviewRequest(ctx *context.Context) {
_, err = issue_service.ReviewRequest(ctx, issue, ctx.Doer, reviewer, action == "attach")
if err != nil {
if issues_model.IsErrReviewRequestOnClosedPR(err) {
ctx.Status(http.StatusForbidden)
return
}
ctx.ServerError("ReviewRequest", err)
return
}

View File

@ -277,6 +277,10 @@ func DismissReview(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.DismissReviewForm)
comm, err := pull_service.DismissReview(ctx, form.ReviewID, ctx.Repo.Repository.ID, form.Message, ctx.Doer, true, true)
if err != nil {
if pull_service.IsErrDismissRequestOnClosedPR(err) {
ctx.Status(http.StatusForbidden)
return
}
ctx.ServerError("pull_service.DismissReview", err)
return
}

View File

@ -899,7 +899,7 @@ func renderLanguageStats(ctx *context.Context) {
}
func renderRepoTopics(ctx *context.Context) {
topics, _, err := repo_model.FindTopics(ctx, &repo_model.FindTopicOptions{
topics, err := db.Find[repo_model.Topic](ctx, &repo_model.FindTopicOptions{
RepoID: ctx.Repo.Repository.ID,
})
if err != nil {

View File

@ -4,17 +4,13 @@
package actions
import (
"errors"
"regexp"
"strings"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
"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"
secret_service "code.gitea.io/gitea/services/secrets"
)
func SetVariablesContext(ctx *context.Context, ownerID, repoID int64) {
@ -29,41 +25,16 @@ func SetVariablesContext(ctx *context.Context, ownerID, repoID int64) {
ctx.Data["Variables"] = variables
}
// some regular expression of `variables` and `secrets`
// reference to:
// https://docs.github.com/en/actions/learn-github-actions/variables#naming-conventions-for-configuration-variables
// https://docs.github.com/en/actions/security-guides/encrypted-secrets#naming-your-secrets
var (
forbiddenEnvNameCIRx = regexp.MustCompile("(?i)^CI")
)
func envNameCIRegexMatch(name string) error {
if forbiddenEnvNameCIRx.MatchString(name) {
log.Error("Env Name cannot be ci")
return errors.New("env name cannot be ci")
}
return nil
}
func CreateVariable(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
form := web.GetForm(ctx).(*forms.EditVariableForm)
if err := secret_service.ValidateName(form.Name); err != nil {
ctx.JSONError(err.Error())
return
}
if err := envNameCIRegexMatch(form.Name); err != nil {
ctx.JSONError(err.Error())
return
}
v, err := actions_model.InsertVariable(ctx, ownerID, repoID, form.Name, ReserveLineBreakForTextarea(form.Data))
v, err := actions_service.CreateVariable(ctx, ownerID, repoID, form.Name, form.Data)
if err != nil {
log.Error("InsertVariable error: %v", err)
log.Error("CreateVariable: %v", err)
ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
return
}
ctx.Flash.Success(ctx.Tr("actions.variables.creation.success", v.Name))
ctx.JSONRedirect(redirectURL)
}
@ -72,23 +43,8 @@ func UpdateVariable(ctx *context.Context, redirectURL string) {
id := ctx.ParamsInt64(":variable_id")
form := web.GetForm(ctx).(*forms.EditVariableForm)
if err := secret_service.ValidateName(form.Name); err != nil {
ctx.JSONError(err.Error())
return
}
if err := envNameCIRegexMatch(form.Name); err != nil {
ctx.JSONError(err.Error())
return
}
ok, err := actions_model.UpdateVariable(ctx, &actions_model.ActionVariable{
ID: id,
Name: strings.ToUpper(form.Name),
Data: ReserveLineBreakForTextarea(form.Data),
})
if err != nil || !ok {
log.Error("UpdateVariable error: %v", err)
if ok, err := actions_service.UpdateVariable(ctx, id, form.Name, form.Data); err != nil || !ok {
log.Error("UpdateVariable: %v", err)
ctx.JSONError(ctx.Tr("actions.variables.update.failed"))
return
}
@ -99,7 +55,7 @@ func UpdateVariable(ctx *context.Context, redirectURL string) {
func DeleteVariable(ctx *context.Context, redirectURL string) {
id := ctx.ParamsInt64(":variable_id")
if _, err := db.DeleteByBean(ctx, &actions_model.ActionVariable{ID: id}); err != nil {
if err := actions_service.DeleteVariableByID(ctx, id); err != nil {
log.Error("Delete variable [%d] failed: %v", id, err)
ctx.JSONError(ctx.Tr("actions.variables.deletion.failed"))
return
@ -107,12 +63,3 @@ func DeleteVariable(ctx *context.Context, redirectURL string) {
ctx.Flash.Success(ctx.Tr("actions.variables.deletion.success"))
ctx.JSONRedirect(redirectURL)
}
func ReserveLineBreakForTextarea(input string) string {
// Since the content is from a form which is a textarea, the line endings are \r\n.
// It's a standard behavior of HTML.
// But we want to store them as \n like what GitHub does.
// And users are unlikely to really need to keep the \r.
// Other than this, we should respect the original content, even leading or trailing spaces.
return strings.ReplaceAll(input, "\r\n", "\n")
}

View File

@ -7,8 +7,8 @@ import (
"code.gitea.io/gitea/models/db"
secret_model "code.gitea.io/gitea/models/secret"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/web/shared/actions"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
secret_service "code.gitea.io/gitea/services/secrets"
@ -27,7 +27,7 @@ func SetSecretsContext(ctx *context.Context, ownerID, repoID int64) {
func PerformSecretsPost(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
form := web.GetForm(ctx).(*forms.AddSecretForm)
s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, actions.ReserveLineBreakForTextarea(form.Data))
s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, util.ReserveLineBreakForTextarea(form.Data))
if err != nil {
log.Error("CreateOrUpdateSecret failed: %v", err)
ctx.JSONError(ctx.Tr("secrets.creation.failed"))

View File

@ -0,0 +1,100 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"context"
"regexp"
"strings"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
secret_service "code.gitea.io/gitea/services/secrets"
)
func CreateVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*actions_model.ActionVariable, error) {
if err := secret_service.ValidateName(name); err != nil {
return nil, err
}
if err := envNameCIRegexMatch(name); err != nil {
return nil, err
}
v, err := actions_model.InsertVariable(ctx, ownerID, repoID, name, util.ReserveLineBreakForTextarea(data))
if err != nil {
return nil, err
}
return v, nil
}
func UpdateVariable(ctx context.Context, variableID int64, name, data string) (bool, error) {
if err := secret_service.ValidateName(name); err != nil {
return false, err
}
if err := envNameCIRegexMatch(name); err != nil {
return false, err
}
return actions_model.UpdateVariable(ctx, &actions_model.ActionVariable{
ID: variableID,
Name: strings.ToUpper(name),
Data: util.ReserveLineBreakForTextarea(data),
})
}
func DeleteVariableByID(ctx context.Context, variableID int64) error {
return actions_model.DeleteVariable(ctx, variableID)
}
func DeleteVariableByName(ctx context.Context, ownerID, repoID int64, name string) error {
if err := secret_service.ValidateName(name); err != nil {
return err
}
if err := envNameCIRegexMatch(name); err != nil {
return err
}
v, err := GetVariable(ctx, actions_model.FindVariablesOpts{
OwnerID: ownerID,
RepoID: repoID,
Name: name,
})
if err != nil {
return err
}
return actions_model.DeleteVariable(ctx, v.ID)
}
func GetVariable(ctx context.Context, opts actions_model.FindVariablesOpts) (*actions_model.ActionVariable, error) {
vars, err := actions_model.FindVariables(ctx, opts)
if err != nil {
return nil, err
}
if len(vars) != 1 {
return nil, util.NewNotExistErrorf("variable not found")
}
return vars[0], nil
}
// some regular expression of `variables` and `secrets`
// reference to:
// https://docs.github.com/en/actions/learn-github-actions/variables#naming-conventions-for-configuration-variables
// https://docs.github.com/en/actions/security-guides/encrypted-secrets#naming-your-secrets
var (
forbiddenEnvNameCIRx = regexp.MustCompile("(?i)^CI")
)
func envNameCIRegexMatch(name string) error {
if forbiddenEnvNameCIRx.MatchString(name) {
log.Error("Env Name cannot be ci")
return util.NewInvalidArgumentErrorf("env name cannot be ci")
}
return nil
}

View File

@ -20,11 +20,29 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
notify_service "code.gitea.io/gitea/services/notify"
)
var notEnoughLines = regexp.MustCompile(`fatal: file .* has only \d+ lines?`)
// ErrDismissRequestOnClosedPR represents an error when an user tries to dismiss a review associated to a closed or merged PR.
type ErrDismissRequestOnClosedPR struct{}
// IsErrDismissRequestOnClosedPR checks if an error is an ErrDismissRequestOnClosedPR.
func IsErrDismissRequestOnClosedPR(err error) bool {
_, ok := err.(ErrDismissRequestOnClosedPR)
return ok
}
func (err ErrDismissRequestOnClosedPR) Error() string {
return "can't dismiss a review associated to a closed or merged PR"
}
func (err ErrDismissRequestOnClosedPR) Unwrap() error {
return util.ErrPermissionDenied
}
// checkInvalidation checks if the line of code comment got changed by another commit.
// If the line got changed the comment is going to be invalidated.
func checkInvalidation(ctx context.Context, c *issues_model.Comment, doer *user_model.User, repo *git.Repository, branch string) error {
@ -382,6 +400,21 @@ func DismissReview(ctx context.Context, reviewID, repoID int64, message string,
return nil, fmt.Errorf("reviews's repository is not the same as the one we expect")
}
issue := review.Issue
if issue.IsClosed {
return nil, ErrDismissRequestOnClosedPR{}
}
if issue.IsPull {
if err := issue.LoadPullRequest(ctx); err != nil {
return nil, err
}
if issue.PullRequest.HasMerged {
return nil, ErrDismissRequestOnClosedPR{}
}
}
if err := issues_model.DismissReview(ctx, review, isDismiss); err != nil {
return nil, err
}

View File

@ -0,0 +1,48 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pull_test
import (
"testing"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
pull_service "code.gitea.io/gitea/services/pull"
"github.com/stretchr/testify/assert"
)
func TestDismissReview(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{})
assert.NoError(t, pull.LoadIssue(db.DefaultContext))
issue := pull.Issue
assert.NoError(t, issue.LoadRepo(db.DefaultContext))
reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
review, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{
Issue: issue,
Reviewer: reviewer,
Type: issues_model.ReviewTypeReject,
})
assert.NoError(t, err)
issue.IsClosed = true
pull.HasMerged = false
assert.NoError(t, issues_model.UpdateIssueCols(db.DefaultContext, issue, "is_closed"))
assert.NoError(t, pull.UpdateCols(db.DefaultContext, "has_merged"))
_, err = pull_service.DismissReview(db.DefaultContext, review.ID, issue.RepoID, "", &user_model.User{}, false, false)
assert.Error(t, err)
assert.True(t, pull_service.IsErrDismissRequestOnClosedPR(err))
pull.HasMerged = true
pull.Issue.IsClosed = false
assert.NoError(t, issues_model.UpdateIssueCols(db.DefaultContext, issue, "is_closed"))
assert.NoError(t, pull.UpdateCols(db.DefaultContext, "has_merged"))
_, err = pull_service.DismissReview(db.DefaultContext, review.ID, issue.RepoID, "", &user_model.User{}, false, false)
assert.Error(t, err)
assert.True(t, pull_service.IsErrDismissRequestOnClosedPR(err))
}

View File

@ -7,14 +7,14 @@
<dt>{{ctx.Locale.Tr "admin.config.disable_gravatar"}}</dt>
<dd>
<div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.disable_gravatar"}}">
<input type="checkbox" data-config-dyn-key="picture.disable_gravatar" {{if .SystemConfig.Picture.DisableGravatar.Value ctx}}checked{{end}}>
<input type="checkbox" data-config-dyn-key="picture.disable_gravatar" {{if .SystemConfig.Picture.DisableGravatar.Value ctx}}checked{{end}}><label></label>
</div>
</dd>
<div class="divider"></div>
<dt>{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}</dt>
<dd>
<div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}">
<input type="checkbox" data-config-dyn-key="picture.enable_federated_avatar" {{if .SystemConfig.Picture.EnableFederatedAvatar.Value ctx}}checked{{end}}>
<input type="checkbox" data-config-dyn-key="picture.enable_federated_avatar" {{if .SystemConfig.Picture.EnableFederatedAvatar.Value ctx}}checked{{end}}><label></label>
</div>
</dd>
</dl>

View File

@ -42,8 +42,8 @@
<div class="field color-field">
<label for="new_project_column_color_picker">{{ctx.Locale.Tr "repo.projects.column.color"}}</label>
<div class="color picker column">
<input class="color-picker" maxlength="7" placeholder="#c320f6" id="new_project_column_color_picker" name="color">
<div class="js-color-picker-input column">
<input maxlength="7" placeholder="#c320f6" id="new_project_column_color_picker" name="color">
{{template "repo/issue/label_precolors"}}
</div>
</div>
@ -114,8 +114,8 @@
<div class="field color-field">
<label for="new_project_column_color">{{ctx.Locale.Tr "repo.projects.column.color"}}</label>
<div class="color picker column">
<input class="color-picker" maxlength="7" placeholder="#c320f6" id="new_project_column_color" name="color" value="{{.Color}}">
<div class="js-color-picker-input column">
<input maxlength="7" placeholder="#c320f6" id="new_project_column_color" name="color" value="{{.Color}}">
{{template "repo/issue/label_precolors"}}
</div>
</div>

View File

@ -16,7 +16,7 @@
<td class="author tw-flex">
{{$userName := .Author.Name}}
{{if .User}}
{{if .User.FullName}}
{{if and .User.FullName DefaultShowFullName}}
{{$userName = .User.FullName}}
{{end}}
{{ctx.AvatarUtils.Avatar .User 28 "tw-mr-2"}}<a class="muted author-wrapper" href="{{.User.HomeLink}}">{{$userName}}</a>

View File

@ -61,7 +61,7 @@
<span class="author tw-flex tw-items-center tw-mr-2">
{{$userName := $commit.Commit.Author.Name}}
{{if $commit.User}}
{{if $commit.User.FullName}}
{{if and $commit.User.FullName DefaultShowFullName}}
{{$userName = $commit.User.FullName}}
{{end}}
<span class="tw-mr-1">{{ctx.AvatarUtils.Avatar $commit.User}}</span>

View File

@ -52,8 +52,8 @@
</div>
<div class="field color-field">
<label for="color">{{ctx.Locale.Tr "repo.issues.label_color"}}</label>
<div class="color picker column">
<input class="color-picker" name="color" value="#70c24a" required maxlength="7">
<div class="column js-color-picker-input">
<input name="color" value="#70c24a"placeholder="#c320f6" required maxlength="7">
{{template "repo/issue/label_precolors"}}
</div>
</div>

View File

@ -27,8 +27,8 @@
</div>
<div class="field color-field">
<label for="color">{{ctx.Locale.Tr "repo.issues.label_color"}}</label>
<div class="color picker column">
<input class="color-picker" name="color" value="#70c24a" required maxlength="7">
<div class="js-color-picker-input column">
<input name="color" value="#70c24a" placeholder="#c320f6" required maxlength="7">
{{template "repo/issue/label_precolors"}}
</div>
</div>

View File

@ -59,7 +59,7 @@
{{end}}
</div>
<div class="tw-flex tw-items-center tw-gap-2">
{{if (and $.Permission.IsAdmin (or (eq .Review.Type 1) (eq .Review.Type 3)) (not $.Issue.IsClosed))}}
{{if (and $.Permission.IsAdmin (or (eq .Review.Type 1) (eq .Review.Type 3)) (not $.Issue.IsClosed) (not $.Issue.PullRequest.HasMerged))}}
<a href="#" class="ui muted icon tw-flex tw-items-center show-modal" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dismiss_review"}}" data-modal="#dismiss-review-modal-{{.Review.ID}}">
{{svg "octicon-x" 20}}
</a>
@ -91,7 +91,7 @@
{{svg "octicon-hourglass" 16}}
</span>
{{end}}
{{if .CanChange}}
{{if and .CanChange (or .Checked (and (not $.Issue.IsClosed) (not $.Issue.PullRequest.HasMerged)))}}
<a href="#" class="ui muted icon re-request-review{{if .Checked}} checked{{end}}" data-tooltip-content="{{if .Checked}}{{ctx.Locale.Tr "repo.issues.remove_request_review"}}{{else}}{{ctx.Locale.Tr "repo.issues.re_request_review"}}{{end}}" data-issue-id="{{$.Issue.ID}}" data-id="{{.ItemID}}" data-update-url="{{$.RepoLink}}/issues/request_review">{{if .Checked}}{{svg "octicon-trash"}}{{else}}{{svg "octicon-sync"}}{{end}}</a>
{{end}}
{{svg (printf "octicon-%s" .Review.Type.Icon) 16 (printf "text %s" (.Review.HTMLTypeColorName))}}
@ -691,7 +691,7 @@
{{if and (not (eq .Issue.PullRequest.HeadRepo.FullName .Issue.PullRequest.BaseRepo.FullName)) .CanWriteToHeadRepo}}
<div class="divider"></div>
<div class="inline field">
<div class="ui checkbox" id="allow-edits-from-maintainers"
<div class="ui checkbox small-loading-icon" id="allow-edits-from-maintainers"
data-url="{{.Issue.Link}}"
data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers_desc"}}"
data-prompt-error="{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers_err"}}"

View File

@ -3,7 +3,7 @@
{{else}}
{{if .LatestCommitUser}}
{{ctx.AvatarUtils.Avatar .LatestCommitUser 24 "tw-mr-1"}}
{{if .LatestCommitUser.FullName}}
{{if and .LatestCommitUser.FullName DefaultShowFullName}}
<a class="muted author-wrapper" title="{{.LatestCommitUser.FullName}}" href="{{.LatestCommitUser.HomeLink}}"><strong>{{.LatestCommitUser.FullName}}</strong></a>
{{else}}
<a class="muted author-wrapper" title="{{if .LatestCommit.Author}}{{.LatestCommit.Author.Name}}{{else}}{{.LatestCommitUser.Name}}{{end}}" href="{{.LatestCommitUser.HomeLink}}"><strong>{{if .LatestCommit.Author}}{{.LatestCommit.Author.Name}}{{else}}{{.LatestCommitUser.Name}}{{end}}</strong></a>

View File

@ -1844,6 +1844,232 @@
}
}
},
"/orgs/{org}/actions/variables": {
"get": {
"produces": [
"application/json"
],
"tags": [
"organization"
],
"summary": "Get an org-level variables list",
"operationId": "getOrgVariablesList",
"parameters": [
{
"type": "string",
"description": "name of the organization",
"name": "org",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "page number of results to return (1-based)",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "page size of results",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"$ref": "#/responses/VariableList"
},
"400": {
"$ref": "#/responses/error"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/orgs/{org}/actions/variables/{variablename}": {
"get": {
"produces": [
"application/json"
],
"tags": [
"organization"
],
"summary": "Get an org-level variable",
"operationId": "getOrgVariable",
"parameters": [
{
"type": "string",
"description": "name of the organization",
"name": "org",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the variable",
"name": "variablename",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/ActionVariable"
},
"400": {
"$ref": "#/responses/error"
},
"404": {
"$ref": "#/responses/notFound"
}
}
},
"put": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"organization"
],
"summary": "Update an org-level variable",
"operationId": "updateOrgVariable",
"parameters": [
{
"type": "string",
"description": "name of the organization",
"name": "org",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the variable",
"name": "variablename",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"schema": {
"$ref": "#/definitions/UpdateVariableOption"
}
}
],
"responses": {
"201": {
"description": "response when updating an org-level variable"
},
"204": {
"description": "response when updating an org-level variable"
},
"400": {
"$ref": "#/responses/error"
},
"404": {
"$ref": "#/responses/notFound"
}
}
},
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"organization"
],
"summary": "Create an org-level variable",
"operationId": "createOrgVariable",
"parameters": [
{
"type": "string",
"description": "name of the organization",
"name": "org",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the variable",
"name": "variablename",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"schema": {
"$ref": "#/definitions/CreateVariableOption"
}
}
],
"responses": {
"201": {
"description": "response when creating an org-level variable"
},
"204": {
"description": "response when creating an org-level variable"
},
"400": {
"$ref": "#/responses/error"
},
"404": {
"$ref": "#/responses/notFound"
}
}
},
"delete": {
"produces": [
"application/json"
],
"tags": [
"organization"
],
"summary": "Delete an org-level variable",
"operationId": "deleteOrgVariable",
"parameters": [
{
"type": "string",
"description": "name of the organization",
"name": "org",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the variable",
"name": "variablename",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/ActionVariable"
},
"201": {
"description": "response when deleting a variable"
},
"204": {
"description": "response when deleting a variable"
},
"400": {
"$ref": "#/responses/error"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/orgs/{org}/activities/feeds": {
"get": {
"produces": [
@ -3723,6 +3949,261 @@
}
}
},
"/repos/{owner}/{repo}/actions/variables": {
"get": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Get repo-level variables list",
"operationId": "getRepoVariablesList",
"parameters": [
{
"type": "string",
"description": "name of the owner",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repository",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "page number of results to return (1-based)",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "page size of results",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"$ref": "#/responses/VariableList"
},
"400": {
"$ref": "#/responses/error"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/repos/{owner}/{repo}/actions/variables/{variablename}": {
"get": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Get a repo-level variable",
"operationId": "getRepoVariable",
"parameters": [
{
"type": "string",
"description": "name of the owner",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repository",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the variable",
"name": "variablename",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/ActionVariable"
},
"400": {
"$ref": "#/responses/error"
},
"404": {
"$ref": "#/responses/notFound"
}
}
},
"put": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Update a repo-level variable",
"operationId": "updateRepoVariable",
"parameters": [
{
"type": "string",
"description": "name of the owner",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repository",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the variable",
"name": "variablename",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"schema": {
"$ref": "#/definitions/UpdateVariableOption"
}
}
],
"responses": {
"201": {
"description": "response when updating a repo-level variable"
},
"204": {
"description": "response when updating a repo-level variable"
},
"400": {
"$ref": "#/responses/error"
},
"404": {
"$ref": "#/responses/notFound"
}
}
},
"post": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Create a repo-level variable",
"operationId": "createRepoVariable",
"parameters": [
{
"type": "string",
"description": "name of the owner",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repository",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the variable",
"name": "variablename",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"schema": {
"$ref": "#/definitions/CreateVariableOption"
}
}
],
"responses": {
"201": {
"description": "response when creating a repo-level variable"
},
"204": {
"description": "response when creating a repo-level variable"
},
"400": {
"$ref": "#/responses/error"
},
"404": {
"$ref": "#/responses/notFound"
}
}
},
"delete": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Delete a repo-level variable",
"operationId": "deleteRepoVariable",
"parameters": [
{
"type": "string",
"description": "name of the owner",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repository",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the variable",
"name": "variablename",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/ActionVariable"
},
"201": {
"description": "response when deleting a variable"
},
"204": {
"description": "response when deleting a variable"
},
"400": {
"$ref": "#/responses/error"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/repos/{owner}/{repo}/activities/feeds": {
"get": {
"produces": [
@ -11276,6 +11757,9 @@
"204": {
"$ref": "#/responses/empty"
},
"403": {
"$ref": "#/responses/forbidden"
},
"404": {
"$ref": "#/responses/notFound"
},
@ -15047,6 +15531,194 @@
}
}
},
"/user/actions/variables": {
"get": {
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Get the user-level list of variables which is created by current doer",
"operationId": "getUserVariablesList",
"parameters": [
{
"type": "integer",
"description": "page number of results to return (1-based)",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "page size of results",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"$ref": "#/responses/VariableList"
},
"400": {
"$ref": "#/responses/error"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/user/actions/variables/{variablename}": {
"get": {
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Get a user-level variable which is created by current doer",
"operationId": "getUserVariable",
"parameters": [
{
"type": "string",
"description": "name of the variable",
"name": "variablename",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/ActionVariable"
},
"400": {
"$ref": "#/responses/error"
},
"404": {
"$ref": "#/responses/notFound"
}
}
},
"put": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Update a user-level variable which is created by current doer",
"operationId": "updateUserVariable",
"parameters": [
{
"type": "string",
"description": "name of the variable",
"name": "variablename",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"schema": {
"$ref": "#/definitions/UpdateVariableOption"
}
}
],
"responses": {
"201": {
"description": "response when updating a variable"
},
"204": {
"description": "response when updating a variable"
},
"400": {
"$ref": "#/responses/error"
},
"404": {
"$ref": "#/responses/notFound"
}
}
},
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Create a user-level variable",
"operationId": "createUserVariable",
"parameters": [
{
"type": "string",
"description": "name of the variable",
"name": "variablename",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"schema": {
"$ref": "#/definitions/CreateVariableOption"
}
}
],
"responses": {
"201": {
"description": "response when creating a variable"
},
"204": {
"description": "response when creating a variable"
},
"400": {
"$ref": "#/responses/error"
},
"404": {
"$ref": "#/responses/notFound"
}
}
},
"delete": {
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Delete a user-level variable which is created by current doer",
"operationId": "deleteUserVariable",
"parameters": [
{
"type": "string",
"description": "name of the variable",
"name": "variablename",
"in": "path",
"required": true
}
],
"responses": {
"201": {
"description": "response when deleting a variable"
},
"204": {
"description": "response when deleting a variable"
},
"400": {
"$ref": "#/responses/error"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/user/applications/oauth2": {
"get": {
"produces": [
@ -17190,6 +17862,35 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"ActionVariable": {
"description": "ActionVariable return value of the query API",
"type": "object",
"properties": {
"data": {
"description": "the value of the variable",
"type": "string",
"x-go-name": "Data"
},
"name": {
"description": "the name of the variable",
"type": "string",
"x-go-name": "Name"
},
"owner_id": {
"description": "the owner to which the variable belongs",
"type": "integer",
"format": "int64",
"x-go-name": "OwnerID"
},
"repo_id": {
"description": "the repository to which the variable belongs",
"type": "integer",
"format": "int64",
"x-go-name": "RepoID"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"Activity": {
"type": "object",
"properties": {
@ -19076,6 +19777,21 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"CreateVariableOption": {
"description": "CreateVariableOption the option when creating variable",
"type": "object",
"required": [
"value"
],
"properties": {
"value": {
"description": "Value of the variable to create",
"type": "string",
"x-go-name": "Value"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"CreateWikiPageOptions": {
"description": "CreateWikiPageOptions form for creating wiki",
"type": "object",
@ -23377,6 +24093,26 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"UpdateVariableOption": {
"description": "UpdateVariableOption the option when updating variable",
"type": "object",
"required": [
"value"
],
"properties": {
"name": {
"description": "New name for the variable. If the field is empty, the variable name won't be updated.",
"type": "string",
"x-go-name": "Name"
},
"value": {
"description": "Value of the variable to update",
"type": "string",
"x-go-name": "Value"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"User": {
"description": "User represents a user",
"type": "object",
@ -23758,6 +24494,12 @@
}
}
},
"ActionVariable": {
"description": "ActionVariable",
"schema": {
"$ref": "#/definitions/ActionVariable"
}
},
"ActivityFeedsList": {
"description": "ActivityFeedsList",
"schema": {
@ -24641,6 +25383,15 @@
}
}
},
"VariableList": {
"description": "VariableList",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/ActionVariable"
}
}
},
"WatchInfo": {
"description": "WatchInfo",
"schema": {
@ -24716,7 +25467,7 @@
"parameterBodies": {
"description": "parameterBodies",
"schema": {
"$ref": "#/definitions/UserBadgeOption"
"$ref": "#/definitions/UpdateVariableOption"
}
},
"redirect": {

View File

@ -26,14 +26,34 @@ func TestAPITopicSearch(t *testing.T) {
TopicNames []*api.TopicResponse `json:"topics"`
}
// search all topics
res := MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
DecodeJSON(t, res, &topics)
assert.Len(t, topics.TopicNames, 6)
assert.EqualValues(t, "6", res.Header().Get("x-total-count"))
// pagination search topics first page
topics.TopicNames = nil
query := url.Values{"page": []string{"1"}, "limit": []string{"4"}}
searchURL.RawQuery = query.Encode()
res := MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
DecodeJSON(t, res, &topics)
assert.Len(t, topics.TopicNames, 4)
assert.EqualValues(t, "6", res.Header().Get("x-total-count"))
// pagination search topics second page
topics.TopicNames = nil
query = url.Values{"page": []string{"2"}, "limit": []string{"4"}}
searchURL.RawQuery = query.Encode()
res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
DecodeJSON(t, res, &topics)
assert.Len(t, topics.TopicNames, 2)
assert.EqualValues(t, "6", res.Header().Get("x-total-count"))
// add keyword search
query = url.Values{"page": []string{"1"}, "limit": []string{"4"}}
query.Add("q", "topic")
searchURL.RawQuery = query.Encode()
res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)

View File

@ -0,0 +1,149 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/http"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
)
func TestAPIRepoVariables(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
t.Run("CreateRepoVariable", func(t *testing.T) {
cases := []struct {
Name string
ExpectedStatus int
}{
{
Name: "-",
ExpectedStatus: http.StatusBadRequest,
},
{
Name: "_",
ExpectedStatus: http.StatusNoContent,
},
{
Name: "TEST_VAR",
ExpectedStatus: http.StatusNoContent,
},
{
Name: "test_var",
ExpectedStatus: http.StatusConflict,
},
{
Name: "ci",
ExpectedStatus: http.StatusBadRequest,
},
{
Name: "123var",
ExpectedStatus: http.StatusBadRequest,
},
{
Name: "var@test",
ExpectedStatus: http.StatusBadRequest,
},
{
Name: "github_var",
ExpectedStatus: http.StatusBadRequest,
},
{
Name: "gitea_var",
ExpectedStatus: http.StatusBadRequest,
},
}
for _, c := range cases {
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/variables/%s", repo.FullName(), c.Name), api.CreateVariableOption{
Value: "value",
}).AddTokenAuth(token)
MakeRequest(t, req, c.ExpectedStatus)
}
})
t.Run("UpdateRepoVariable", func(t *testing.T) {
variableName := "test_update_var"
url := fmt.Sprintf("/api/v1/repos/%s/actions/variables/%s", repo.FullName(), variableName)
req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{
Value: "initial_val",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
cases := []struct {
Name string
UpdateName string
ExpectedStatus int
}{
{
Name: "not_found_var",
ExpectedStatus: http.StatusNotFound,
},
{
Name: variableName,
UpdateName: "1invalid",
ExpectedStatus: http.StatusBadRequest,
},
{
Name: variableName,
UpdateName: "invalid@name",
ExpectedStatus: http.StatusBadRequest,
},
{
Name: variableName,
UpdateName: "ci",
ExpectedStatus: http.StatusBadRequest,
},
{
Name: variableName,
UpdateName: "updated_var_name",
ExpectedStatus: http.StatusNoContent,
},
{
Name: variableName,
ExpectedStatus: http.StatusNotFound,
},
{
Name: "updated_var_name",
ExpectedStatus: http.StatusNoContent,
},
}
for _, c := range cases {
req := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/actions/variables/%s", repo.FullName(), c.Name), api.UpdateVariableOption{
Name: c.UpdateName,
Value: "updated_val",
}).AddTokenAuth(token)
MakeRequest(t, req, c.ExpectedStatus)
}
})
t.Run("DeleteRepoVariable", func(t *testing.T) {
variableName := "test_delete_var"
url := fmt.Sprintf("/api/v1/repos/%s/actions/variables/%s", repo.FullName(), variableName)
req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{
Value: "initial_val",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
req = NewRequest(t, "DELETE", url).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
req = NewRequest(t, "DELETE", url).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
})
}

View File

@ -0,0 +1,144 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/http"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
)
func TestAPIUserVariables(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user1")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser)
t.Run("CreateRepoVariable", func(t *testing.T) {
cases := []struct {
Name string
ExpectedStatus int
}{
{
Name: "-",
ExpectedStatus: http.StatusBadRequest,
},
{
Name: "_",
ExpectedStatus: http.StatusNoContent,
},
{
Name: "TEST_VAR",
ExpectedStatus: http.StatusNoContent,
},
{
Name: "test_var",
ExpectedStatus: http.StatusConflict,
},
{
Name: "ci",
ExpectedStatus: http.StatusBadRequest,
},
{
Name: "123var",
ExpectedStatus: http.StatusBadRequest,
},
{
Name: "var@test",
ExpectedStatus: http.StatusBadRequest,
},
{
Name: "github_var",
ExpectedStatus: http.StatusBadRequest,
},
{
Name: "gitea_var",
ExpectedStatus: http.StatusBadRequest,
},
}
for _, c := range cases {
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/user/actions/variables/%s", c.Name), api.CreateVariableOption{
Value: "value",
}).AddTokenAuth(token)
MakeRequest(t, req, c.ExpectedStatus)
}
})
t.Run("UpdateRepoVariable", func(t *testing.T) {
variableName := "test_update_var"
url := fmt.Sprintf("/api/v1/user/actions/variables/%s", variableName)
req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{
Value: "initial_val",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
cases := []struct {
Name string
UpdateName string
ExpectedStatus int
}{
{
Name: "not_found_var",
ExpectedStatus: http.StatusNotFound,
},
{
Name: variableName,
UpdateName: "1invalid",
ExpectedStatus: http.StatusBadRequest,
},
{
Name: variableName,
UpdateName: "invalid@name",
ExpectedStatus: http.StatusBadRequest,
},
{
Name: variableName,
UpdateName: "ci",
ExpectedStatus: http.StatusBadRequest,
},
{
Name: variableName,
UpdateName: "updated_var_name",
ExpectedStatus: http.StatusNoContent,
},
{
Name: variableName,
ExpectedStatus: http.StatusNotFound,
},
{
Name: "updated_var_name",
ExpectedStatus: http.StatusNoContent,
},
}
for _, c := range cases {
req := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/user/actions/variables/%s", c.Name), api.UpdateVariableOption{
Name: c.UpdateName,
Value: "updated_val",
}).AddTokenAuth(token)
MakeRequest(t, req, c.ExpectedStatus)
}
})
t.Run("DeleteRepoVariable", func(t *testing.T) {
variableName := "test_delete_var"
url := fmt.Sprintf("/api/v1/user/actions/variables/%s", variableName)
req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{
Value: "initial_val",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
req = NewRequest(t, "DELETE", url).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
req = NewRequest(t, "DELETE", url).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
})
}

View File

@ -21,20 +21,42 @@ func TestTopicSearch(t *testing.T) {
TopicNames []*api.TopicResponse `json:"topics"`
}
// search all topics
res := MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
DecodeJSON(t, res, &topics)
assert.Len(t, topics.TopicNames, 6)
assert.EqualValues(t, "6", res.Header().Get("x-total-count"))
// pagination search topics
topics.TopicNames = nil
query := url.Values{"page": []string{"1"}, "limit": []string{"4"}}
searchURL.RawQuery = query.Encode()
res := MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
DecodeJSON(t, res, &topics)
assert.Len(t, topics.TopicNames, 4)
assert.EqualValues(t, "6", res.Header().Get("x-total-count"))
// second page
topics.TopicNames = nil
query = url.Values{"page": []string{"2"}, "limit": []string{"4"}}
searchURL.RawQuery = query.Encode()
res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
DecodeJSON(t, res, &topics)
assert.Len(t, topics.TopicNames, 2)
assert.EqualValues(t, "6", res.Header().Get("x-total-count"))
// add keyword search
topics.TopicNames = nil
query = url.Values{"page": []string{"1"}, "limit": []string{"4"}}
query.Add("q", "topic")
searchURL.RawQuery = query.Encode()
res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
DecodeJSON(t, res, &topics)
assert.Len(t, topics.TopicNames, 2)
topics.TopicNames = nil
query.Set("q", "database")
searchURL.RawQuery = query.Encode()
res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)

View File

@ -23,6 +23,7 @@
--height-loading: 16rem;
--min-height-textarea: 132px; /* padding + 6 lines + border = calc(1.57142em + 6lh + 2px), but lh is not fully supported */
--tab-size: 4;
--checkbox-size: 16px; /* height and width of checkbox and radio inputs */
}
:root * {
@ -300,8 +301,8 @@ a.label,
.inline-code-block {
padding: 2px 4px;
border-radius: var(--border-radius-medium);
background-color: var(--color-markup-code-block);
border-radius: .24em;
background-color: var(--color-label-bg);
}
/* fix Fomantic's line-height cutting off "g" on Windows Chrome with Segoe UI */
@ -1436,11 +1437,6 @@ table th[data-sortt-desc] .svg {
vertical-align: -0.15em;
}
/* for the jquery.minicolors plugin */
.minicolors-panel {
background: var(--color-secondary-dark-1) !important;
}
.ui.tabular.menu {
border-color: var(--color-secondary);
}

View File

@ -0,0 +1,164 @@
/* This is a stripped-down version of coloris's CSS tailored to our needs. It does only include
opaqua colors, and if more features like opacity are needed, the CSS needs to be extended
based on upstream: https://github.com/mdbassit/Coloris/blob/main/src/coloris.css. */
.js-color-picker-input {
display: flex;
flex-wrap: wrap;
}
.js-color-picker-input input {
padding-top: 8px !important;
padding-bottom: 8px !important;
padding-left: 32px !important;
}
.clr-picker {
display: none;
flex-wrap: wrap;
position: absolute;
width: 200px;
z-index: 1002; /* above .ui.modal which has 1001 */
border-radius: var(--border-radius);
background-color: var(--color-menu);
justify-content: flex-end;
direction: ltr;
box-shadow: 0 5px 20px var(--color-shadow);
user-select: none;
}
.clr-picker.clr-open {
display: flex;
}
.clr-gradient {
position: relative;
width: 100%;
height: 100px;
border-radius: 3px 3px 0 0;
background: linear-gradient(rgba(0,0,0,0), #000), linear-gradient(90deg, #fff, currentcolor); /* stylelint-disable-line scale-unlimited/declaration-strict-value */
cursor: pointer;
}
.clr-marker {
position: absolute;
width: 12px;
height: 12px;
margin: -6px 0 0 -6px;
border: 1px solid var(--color-white);
border-radius: 50%;
background-color: currentcolor;
cursor: pointer;
}
.clr-picker input[type="range"]::-webkit-slider-runnable-track {
width: 100%;
height: 16px;
}
.clr-picker input[type="range"]::-webkit-slider-thumb {
width: 16px;
height: 16px;
-webkit-appearance: none;
}
.clr-picker input[type="range"]::-moz-range-track {
width: 100%;
height: 16px;
border: 0;
}
.clr-picker input[type="range"]::-moz-range-thumb {
width: 16px;
height: 16px;
border: 0;
}
.clr-hue {
background: linear-gradient(to right, #f00 0%, #ff0 16.66%, #0f0 33.33%, #0ff 50%, #00f 66.66%, #f0f 83.33%, #f00 100%); /* stylelint-disable-line scale-unlimited/declaration-strict-value */
position: relative;
width: calc(100% - 40px);
height: 10px;
margin: 10px 20px;
border-radius: 4px;
}
.clr-hue input[type="range"] {
position: absolute;
width: calc(100% + 32px);
margin: 0;
background-color: transparent;
opacity: 0;
cursor: pointer;
appearance: none;
}
.clr-hue div {
position: absolute;
width: 16px;
height: 16px;
left: 0;
top: 50%;
transform: translate(-50%, -50%);
border: 2px solid var(--color-white);
border-radius: 50%;
background-color: currentcolor;
box-shadow: 0 0 1px var(--color-shadow);
pointer-events: none;
}
.clr-field {
flex: 1;
position: relative;
color: transparent;
}
.clr-field button {
position: absolute;
aspect-ratio: 1;
height: 16px;
left: 10px;
top: 50%;
transform: translateY(-50%);
margin: 0;
padding: 0;
border: 0;
color: inherit;
pointer-events: none;
border-radius: 2px;
background: repeating-linear-gradient(45deg, #aaa 25%, transparent 25%, transparent 75%, #aaa 75%, #aaa), repeating-linear-gradient(45deg, #aaa 25%, #fff 25%, #fff 75%, #aaa 75%, #aaa); /* stylelint-disable-line scale-unlimited/declaration-strict-value */
background-position: 0 0, 4px 4px;
background-size: 8px 8px;
}
.clr-field button::after {
content: "";
display: block;
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
border-radius: inherit;
background-color: currentcolor;
}
.clr-marker:focus {
outline: none;
}
.clr-keyboard-nav .clr-marker:focus,
.clr-keyboard-nav .clr-hue input:focus + div,
.clr-keyboard-nav .clr-alpha input:focus + div {
outline: none;
box-shadow: 0 0 2px 2px var(--color-white);
}
.clr-picker .clr-preview,
.clr-picker .clr-clear,
.clr-picker .clr-swatches,
.clr-picker .clr-format,
.clr-picker .clr-alpha,
.clr-picker .clr-color {
display: none;
}

View File

@ -102,26 +102,3 @@
.card-ghost * {
opacity: 0;
}
.color-field .minicolors.minicolors-theme-default {
display: block;
}
.color-field .minicolors.minicolors-theme-default .minicolors-input {
height: 38px;
padding-left: 2rem;
}
.color-field .minicolors.minicolors-theme-default .minicolors-swatch {
top: 10px;
}
.edit-project-column-modal .color.picker.column,
.new-project-column-modal .color.picker.column {
display: flex;
}
.edit-project-column-modal .color.picker.column .minicolors,
.new-project-column-modal .color.picker.column .minicolors {
flex: 1;
}

View File

@ -32,10 +32,7 @@ textarea,
.ui.form input[type="text"],
.ui.form input[type="time"],
.ui.form input[type="url"],
.ui.selection.dropdown,
.ui.checkbox label::before,
.ui.checkbox input:checked ~ label::before,
.ui.checkbox input:not([type="radio"]):indeterminate ~ label::before {
.ui.selection.dropdown {
background: var(--color-input-background);
border-color: var(--color-input-border);
color: var(--color-input-text);
@ -63,12 +60,7 @@ textarea:hover,
.ui.form input[type="text"]:hover,
.ui.form input[type="time"]:hover,
.ui.form input[type="url"]:hover,
.ui.selection.dropdown:hover,
.ui.checkbox label:hover::before,
.ui.checkbox label:active::before,
.ui.radio.checkbox label::after,
.ui.radio.checkbox input:focus ~ label::before,
.ui.radio.checkbox input:checked ~ label::before {
.ui.selection.dropdown:hover {
background: var(--color-input-background);
border-color: var(--color-input-border-hover);
color: var(--color-input-text);
@ -91,11 +83,7 @@ textarea:focus,
.ui.form input[type="text"]:focus,
.ui.form input[type="time"]:focus,
.ui.form input[type="url"]:focus,
.ui.selection.dropdown:focus,
.ui.checkbox input:focus ~ label::before,
.ui.checkbox input:not([type="radio"]):indeterminate:focus ~ label::before,
.ui.checkbox input:checked:focus ~ label::before,
.ui.radio.checkbox input:focus:checked ~ label::before {
.ui.selection.dropdown:focus {
background: var(--color-input-background);
border-color: var(--color-primary);
color: var(--color-input-text);
@ -106,58 +94,21 @@ textarea:focus,
.ui.form .inline.fields .field > label,
.ui.form .inline.fields .field > p,
.ui.form .inline.field > label,
.ui.form .inline.field > p,
.ui.checkbox label,
.ui.checkbox + label,
.ui.checkbox label:hover,
.ui.checkbox + label:hover,
.ui.checkbox input:focus ~ label,
.ui.checkbox input:active ~ label {
.ui.form .inline.field > p {
color: var(--color-text);
}
.ui.form .required.fields:not(.grouped) > .field > label::after,
.ui.form .required.fields.grouped > label::after,
.ui.form .required.field > label::after,
.ui.form .required.fields:not(.grouped) > .field > .checkbox::after,
.ui.form .required.field > .checkbox::after,
.ui.form label.required::after {
color: var(--color-red);
}
.ui.input,
.ui.checkbox input:focus ~ label::after,
.ui.checkbox input:checked ~ label::after,
.ui.checkbox label:active::after,
.ui.checkbox input:not([type="radio"]):indeterminate ~ label::after,
.ui.checkbox input:not([type="radio"]):indeterminate:focus ~ label::after,
.ui.checkbox input:checked:focus ~ label::after,
.ui.disabled.checkbox label,
.ui.checkbox input[disabled] ~ label {
.ui.input {
color: var(--color-input-text);
}
.ui.radio.checkbox input:focus ~ label::after,
.ui.radio.checkbox input:checked ~ label::after,
.ui.radio.checkbox input:focus:checked ~ label::after {
background: var(--color-input-text);
}
.ui.toggle.checkbox label::before {
background: var(--color-input-toggle-background);
}
.ui.toggle.checkbox label,
.ui.toggle.checkbox input:checked ~ label,
.ui.toggle.checkbox input:focus:checked ~ label {
color: var(--color-text) !important;
}
.ui.toggle.checkbox input:checked ~ label::before,
.ui.toggle.checkbox input:focus:checked ~ label::before {
background: var(--color-primary) !important;
}
/* match <select> padding to <input> */
.ui.form select {
padding: 0.67857143em 1em;

View File

@ -12,6 +12,7 @@
@import "./modules/message.css";
@import "./modules/table.css";
@import "./modules/card.css";
@import "./modules/checkbox.css";
@import "./modules/modal.css";
@import "./modules/select.css";

View File

@ -6,7 +6,6 @@
.is-loading {
pointer-events: none !important;
position: relative !important;
overflow: hidden !important;
}
.is-loading > * {

View File

@ -0,0 +1,120 @@
/* based on Fomantic UI checkbox module, with just the parts extracted that we use. If you find any
unused rules here after refactoring, please remove them. */
input[type="checkbox"],
input[type="radio"] {
width: var(--checkbox-size);
height: var(--checkbox-size);
}
.ui.checkbox {
position: relative;
display: inline-block;
vertical-align: baseline;
min-height: var(--checkbox-size);
line-height: var(--checkbox-size);
min-width: var(--checkbox-size);
padding: 1px;
}
.ui.checkbox input[type="checkbox"],
.ui.checkbox input[type="radio"] {
position: absolute;
top: 0;
left: 0;
width: var(--checkbox-size);
height: var(--checkbox-size);
}
.ui.checkbox input[type="checkbox"]:enabled,
.ui.checkbox input[type="radio"]:enabled,
.ui.checkbox label:enabled {
cursor: pointer;
}
.ui.checkbox label {
cursor: auto;
position: relative;
display: block;
user-select: none;
}
.ui.checkbox label,
.ui.radio.checkbox label {
padding-left: 1.85714em;
}
.ui.checkbox + label {
vertical-align: middle;
}
.ui.disabled.checkbox label,
.ui.checkbox input[disabled] ~ label {
cursor: default !important;
opacity: 0.5;
pointer-events: none;
}
.ui.radio.checkbox {
min-height: var(--checkbox-size);
}
/* "switch" styled checkbox */
.ui.toggle.checkbox {
min-height: 1.5rem;
}
.ui.toggle.checkbox input {
width: 3.5rem;
height: 1.5rem;
opacity: 0;
z-index: 3;
}
.ui.toggle.checkbox label {
min-height: 1.5rem;
padding-left: 4.5rem;
padding-top: 0.15em;
}
.ui.toggle.checkbox label::before {
display: block;
position: absolute;
content: "";
z-index: 1;
top: 0;
width: 3.5rem;
height: 1.5rem;
border-radius: 500rem;
left: 0;
}
.ui.toggle.checkbox label::after {
background: var(--color-white);
position: absolute;
content: "";
opacity: 1;
z-index: 2;
width: 1.5rem;
height: 1.5rem;
top: 0;
left: 0;
border-radius: 500rem;
transition: background 0.3s ease, left 0.3s ease;
}
.ui.toggle.checkbox input ~ label::after {
left: -0.05rem;
}
.ui.toggle.checkbox input:checked ~ label::after {
left: 2.15rem;
}
.ui.toggle.checkbox input:focus ~ label::before,
.ui.toggle.checkbox label::before {
background: var(--color-input-toggle-background);
}
.ui.toggle.checkbox label,
.ui.toggle.checkbox input:checked ~ label,
.ui.toggle.checkbox input:focus:checked ~ label {
color: var(--color-text) !important;
}
.ui.toggle.checkbox input:checked ~ label::before,
.ui.toggle.checkbox input:focus:checked ~ label::before {
background: var(--color-primary) !important;
}

View File

@ -89,10 +89,6 @@
text-align: center;
}
.organization.options input {
min-width: 300px;
}
.page-content.organization .org-avatar {
margin-right: 15px;
}

View File

@ -2260,24 +2260,6 @@
padding-top: 15px;
}
.edit-label.modal .form .color.picker.column,
.new-label.modal .form .color.picker.column {
display: flex;
}
.edit-label.modal .form .color.picker.column .minicolors,
.new-label.modal .form .color.picker.column .minicolors {
flex: 1;
}
.edit-label.modal .form .minicolors-swatch.minicolors-sprite,
.new-label.modal .form .minicolors-swatch.minicolors-sprite {
top: 10px;
left: 10px;
width: 15px;
height: 15px;
}
.tab-size-1 {
tab-size: 1 !important;
-moz-tab-size: 1 !important;

View File

@ -9,6 +9,7 @@
.issue-list-toolbar-left {
display: flex;
align-items: center;
}
.issue-list-toolbar-right .filter.menu {

View File

@ -2323,715 +2323,6 @@
Theme Overrides
*******************************/
/*******************************
Site Overrides
*******************************/
/*!
* # Fomantic-UI - Checkbox
* http://github.com/fomantic/Fomantic-UI/
*
*
* Released under the MIT license
* http://opensource.org/licenses/MIT
*
*/
/*******************************
Checkbox
*******************************/
/*--------------
Content
---------------*/
.ui.checkbox {
position: relative;
display: inline-block;
backface-visibility: hidden;
outline: none;
vertical-align: baseline;
font-style: normal;
min-height: 17px;
font-size: 1em;
line-height: 17px;
min-width: 17px;
}
/* HTML Checkbox */
.ui.checkbox input[type="checkbox"],
.ui.checkbox input[type="radio"] {
cursor: pointer;
position: absolute;
top: 0;
left: 0;
opacity: 0 !important;
outline: none;
z-index: 3;
width: 17px;
height: 17px;
}
.ui.checkbox label {
cursor: auto;
position: relative;
display: block;
padding-left: 1.85714em;
outline: none;
font-size: 1em;
}
.ui.checkbox label:before {
position: absolute;
top: 0;
left: 0;
width: 17px;
height: 17px;
content: '';
background: #FFFFFF;
border-radius: 0.21428571rem;
transition: border 0.1s ease, opacity 0.1s ease, transform 0.1s ease, box-shadow 0.1s ease;
border: 1px solid #D4D4D5;
}
/*--------------
Checkmark
---------------*/
.ui.checkbox label:after {
position: absolute;
font-size: 14px;
top: 0;
left: 0;
width: 17px;
height: 17px;
text-align: center;
opacity: 0;
color: rgba(0, 0, 0, 0.87);
transition: border 0.1s ease, opacity 0.1s ease, transform 0.1s ease, box-shadow 0.1s ease;
}
/*--------------
Label
---------------*/
/* Inside */
.ui.checkbox label,
.ui.checkbox + label {
color: rgba(0, 0, 0, 0.87);
transition: color 0.1s ease;
}
/* Outside */
.ui.checkbox + label {
vertical-align: middle;
}
/*******************************
States
*******************************/
/*--------------
Hover
---------------*/
.ui.checkbox label:hover::before {
background: #FFFFFF;
border-color: rgba(34, 36, 38, 0.35);
}
.ui.checkbox label:hover,
.ui.checkbox + label:hover {
color: rgba(0, 0, 0, 0.8);
}
/*--------------
Down
---------------*/
.ui.checkbox label:active::before {
background: #F9FAFB;
border-color: rgba(34, 36, 38, 0.35);
}
.ui.checkbox label:active::after {
color: rgba(0, 0, 0, 0.95);
}
.ui.checkbox input:active ~ label {
color: rgba(0, 0, 0, 0.95);
}
/*--------------
Focus
---------------*/
.ui.checkbox input:focus ~ label:before {
background: #FFFFFF;
border-color: #96C8DA;
}
.ui.checkbox input:focus ~ label:after {
color: rgba(0, 0, 0, 0.95);
}
.ui.checkbox input:focus ~ label {
color: rgba(0, 0, 0, 0.95);
}
/*--------------
Active
---------------*/
.ui.checkbox input:checked ~ label:before {
background: #FFFFFF;
border-color: rgba(34, 36, 38, 0.35);
}
.ui.checkbox input:checked ~ label:after {
opacity: 1;
color: rgba(0, 0, 0, 0.95);
}
/*--------------
Indeterminate
---------------*/
.ui.checkbox input:not([type=radio]):indeterminate ~ label:before {
background: #FFFFFF;
border-color: rgba(34, 36, 38, 0.35);
}
.ui.checkbox input:not([type=radio]):indeterminate ~ label:after {
opacity: 1;
color: rgba(0, 0, 0, 0.95);
}
.ui.indeterminate.toggle.checkbox input:not([type=radio]):indeterminate ~ label:before {
background: rgba(0, 0, 0, 0.15);
}
.ui.indeterminate.toggle.checkbox input:not([type=radio]) ~ label:after {
left: 1.075rem;
}
/*--------------
Active Focus
---------------*/
.ui.checkbox input:not([type=radio]):indeterminate:focus ~ label:before,
.ui.checkbox input:checked:focus ~ label:before {
background: #FFFFFF;
border-color: #96C8DA;
}
.ui.checkbox input:not([type=radio]):indeterminate:focus ~ label:after,
.ui.checkbox input:checked:focus ~ label:after {
color: rgba(0, 0, 0, 0.95);
}
/*--------------
Read-Only
---------------*/
.ui.read-only.checkbox,
.ui.read-only.checkbox label {
cursor: default;
}
/*--------------
Disabled
---------------*/
.ui.disabled.checkbox label,
.ui.checkbox input[disabled] ~ label {
cursor: default !important;
opacity: 0.5;
color: #000000;
pointer-events: none;
}
/*--------------
Hidden
---------------*/
/* Initialized checkbox moves input below element
to prevent manually triggering */
.ui.checkbox input.hidden {
z-index: -1;
}
/* Selectable Label */
.ui.checkbox input.hidden + label {
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
/*******************************
Types
*******************************/
/*--------------
Radio
---------------*/
.ui.radio.checkbox {
min-height: 15px;
}
.ui.radio.checkbox label {
padding-left: 1.85714em;
}
/* Box */
.ui.radio.checkbox label:before {
content: '';
transform: none;
width: 15px;
height: 15px;
border-radius: 500rem;
top: 1px;
left: 0;
}
/* Bullet */
.ui.radio.checkbox label:after {
border: none;
content: '' !important;
line-height: 15px;
top: 1px;
left: 0;
width: 15px;
height: 15px;
border-radius: 500rem;
transform: scale(0.46666667);
background-color: rgba(0, 0, 0, 0.87);
}
/* Focus */
.ui.radio.checkbox input:focus ~ label:before {
background-color: #FFFFFF;
}
.ui.radio.checkbox input:focus ~ label:after {
background-color: rgba(0, 0, 0, 0.95);
}
/* Indeterminate */
.ui.radio.checkbox input:indeterminate ~ label:after {
opacity: 0;
}
/* Active */
.ui.radio.checkbox input:checked ~ label:before {
background-color: #FFFFFF;
}
.ui.radio.checkbox input:checked ~ label:after {
background-color: rgba(0, 0, 0, 0.95);
}
/* Active Focus */
.ui.radio.checkbox input:focus:checked ~ label:before {
background-color: #FFFFFF;
}
.ui.radio.checkbox input:focus:checked ~ label:after {
background-color: rgba(0, 0, 0, 0.95);
}
/*--------------
Slider
---------------*/
.ui.slider.checkbox {
min-height: 1.25rem;
}
/* Input */
.ui.slider.checkbox input {
width: 3.5rem;
height: 1.25rem;
}
/* Label */
.ui.slider.checkbox label {
padding-left: 4.5rem;
line-height: 1rem;
color: rgba(0, 0, 0, 0.4);
}
/* Line */
.ui.slider.checkbox label:before {
display: block;
position: absolute;
content: '';
transform: none;
border: none !important;
left: 0;
z-index: 1;
top: 0.4rem;
background-color: rgba(0, 0, 0, 0.05);
width: 3.5rem;
height: 0.21428571rem;
border-radius: 500rem;
transition: background 0.3s ease;
}
/* Handle */
.ui.slider.checkbox label:after {
background: #FFFFFF linear-gradient(transparent, rgba(0, 0, 0, 0.05));
position: absolute;
content: '' !important;
opacity: 1;
z-index: 2;
border: none;
box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15), 0 0 0 1px rgba(34, 36, 38, 0.15) inset;
width: 1.5rem;
height: 1.5rem;
top: -0.25rem;
left: 0;
transform: none;
border-radius: 500rem;
transition: left 0.3s ease;
}
/* Focus */
.ui.slider.checkbox input:focus ~ label:before {
background-color: rgba(0, 0, 0, 0.15);
border: none;
}
/* Hover */
.ui.slider.checkbox label:hover {
color: rgba(0, 0, 0, 0.8);
}
.ui.slider.checkbox label:hover::before {
background: rgba(0, 0, 0, 0.15);
}
/* Active */
.ui.slider.checkbox input:checked ~ label {
color: rgba(0, 0, 0, 0.95) !important;
}
.ui.slider.checkbox input:checked ~ label:before {
background-color: #545454 !important;
}
.ui.slider.checkbox input:checked ~ label:after {
left: 2rem;
}
/* Active Focus */
.ui.slider.checkbox input:focus:checked ~ label {
color: rgba(0, 0, 0, 0.95) !important;
}
.ui.slider.checkbox input:focus:checked ~ label:before {
background-color: #000000 !important;
}
/*--------------
Toggle
---------------*/
.ui.toggle.checkbox {
min-height: 1.5rem;
}
/* Input */
.ui.toggle.checkbox input {
width: 3.5rem;
height: 1.5rem;
}
/* Label */
.ui.toggle.checkbox label {
min-height: 1.5rem;
padding-left: 4.5rem;
color: rgba(0, 0, 0, 0.87);
}
.ui.toggle.checkbox label {
padding-top: 0.15em;
}
/* Switch */
.ui.toggle.checkbox label:before {
display: block;
position: absolute;
content: '';
z-index: 1;
transform: none;
border: none;
top: 0;
background: rgba(0, 0, 0, 0.05);
box-shadow: none;
width: 3.5rem;
height: 1.5rem;
border-radius: 500rem;
}
/* Handle */
.ui.toggle.checkbox label:after {
background: #FFFFFF linear-gradient(transparent, rgba(0, 0, 0, 0.05));
position: absolute;
content: '' !important;
opacity: 1;
z-index: 2;
border: none;
box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15), 0 0 0 1px rgba(34, 36, 38, 0.15) inset;
width: 1.5rem;
height: 1.5rem;
top: 0;
left: 0;
border-radius: 500rem;
transition: background 0.3s ease, left 0.3s ease;
}
.ui.toggle.checkbox input ~ label:after {
left: -0.05rem;
box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15), 0 0 0 1px rgba(34, 36, 38, 0.15) inset;
}
/* Focus */
.ui.toggle.checkbox input:focus ~ label:before {
background-color: rgba(0, 0, 0, 0.15);
border: none;
}
/* Hover */
.ui.toggle.checkbox label:hover::before {
background-color: rgba(0, 0, 0, 0.15);
border: none;
}
/* Active */
.ui.toggle.checkbox input:checked ~ label {
color: rgba(0, 0, 0, 0.95) !important;
}
.ui.toggle.checkbox input:checked ~ label:before {
background-color: #2185D0 !important;
}
.ui.toggle.checkbox input:checked ~ label:after {
left: 2.15rem;
box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15), 0 0 0 1px rgba(34, 36, 38, 0.15) inset;
}
/* Active Focus */
.ui.toggle.checkbox input:focus:checked ~ label {
color: rgba(0, 0, 0, 0.95) !important;
}
.ui.toggle.checkbox input:focus:checked ~ label:before {
background-color: #0d71bb !important;
}
/*******************************
Variations
*******************************/
/*--------------
Fitted
---------------*/
.ui.fitted.checkbox label {
padding-left: 0 !important;
}
.ui.fitted.toggle.checkbox {
width: 3.5rem;
}
.ui.fitted.slider.checkbox {
width: 3.5rem;
}
/*--------------------
Size
---------------------*/
.ui.mini.checkbox {
font-size: 0.78571429em;
}
.ui.tiny.checkbox {
font-size: 0.85714286em;
}
.ui.small.checkbox {
font-size: 0.92857143em;
}
.ui.large.checkbox {
font-size: 1.14285714em;
}
.ui.large.form .checkbox:not(.slider):not(.toggle):not(.radio) label:after,
.ui.large.checkbox:not(.slider):not(.toggle):not(.radio) label:after,
.ui.large.form .checkbox:not(.slider):not(.toggle):not(.radio) label:before,
.ui.large.checkbox:not(.slider):not(.toggle):not(.radio) label:before {
transform: scale(1.14285714);
transform-origin: left;
}
.ui.large.form .checkbox.radio label:before,
.ui.large.checkbox.radio label:before {
transform: scale(1.14285714);
transform-origin: left;
}
.ui.large.form .checkbox.radio label:after,
.ui.large.checkbox.radio label:after {
transform: scale(0.57142857);
transform-origin: left;
left: 0.33571429em;
}
.ui.big.checkbox {
font-size: 1.28571429em;
}
.ui.big.form .checkbox:not(.slider):not(.toggle):not(.radio) label:after,
.ui.big.checkbox:not(.slider):not(.toggle):not(.radio) label:after,
.ui.big.form .checkbox:not(.slider):not(.toggle):not(.radio) label:before,
.ui.big.checkbox:not(.slider):not(.toggle):not(.radio) label:before {
transform: scale(1.28571429);
transform-origin: left;
}
.ui.big.form .checkbox.radio label:before,
.ui.big.checkbox.radio label:before {
transform: scale(1.28571429);
transform-origin: left;
}
.ui.big.form .checkbox.radio label:after,
.ui.big.checkbox.radio label:after {
transform: scale(0.64285714);
transform-origin: left;
left: 0.37142857em;
}
.ui.huge.checkbox {
font-size: 1.42857143em;
}
.ui.huge.form .checkbox:not(.slider):not(.toggle):not(.radio) label:after,
.ui.huge.checkbox:not(.slider):not(.toggle):not(.radio) label:after,
.ui.huge.form .checkbox:not(.slider):not(.toggle):not(.radio) label:before,
.ui.huge.checkbox:not(.slider):not(.toggle):not(.radio) label:before {
transform: scale(1.42857143);
transform-origin: left;
}
.ui.huge.form .checkbox.radio label:before,
.ui.huge.checkbox.radio label:before {
transform: scale(1.42857143);
transform-origin: left;
}
.ui.huge.form .checkbox.radio label:after,
.ui.huge.checkbox.radio label:after {
transform: scale(0.71428571);
transform-origin: left;
left: 0.40714286em;
}
.ui.massive.checkbox {
font-size: 1.71428571em;
}
.ui.massive.form .checkbox:not(.slider):not(.toggle):not(.radio) label:after,
.ui.massive.checkbox:not(.slider):not(.toggle):not(.radio) label:after,
.ui.massive.form .checkbox:not(.slider):not(.toggle):not(.radio) label:before,
.ui.massive.checkbox:not(.slider):not(.toggle):not(.radio) label:before {
transform: scale(1.71428571);
transform-origin: left;
}
.ui.massive.form .checkbox.radio label:before,
.ui.massive.checkbox.radio label:before {
transform: scale(1.71428571);
transform-origin: left;
}
.ui.massive.form .checkbox.radio label:after,
.ui.massive.checkbox.radio label:after {
transform: scale(0.85714286);
transform-origin: left;
left: 0.47857143em;
}
/*******************************
Theme Overrides
*******************************/
@font-face {
font-family: 'Checkbox';
src: url(data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMg8SBD8AAAC8AAAAYGNtYXAYVtCJAAABHAAAAFRnYXNwAAAAEAAAAXAAAAAIZ2x5Zn4huwUAAAF4AAABYGhlYWQGPe1ZAAAC2AAAADZoaGVhB30DyAAAAxAAAAAkaG10eBBKAEUAAAM0AAAAHGxvY2EAmgESAAADUAAAABBtYXhwAAkALwAAA2AAAAAgbmFtZSC8IugAAAOAAAABknBvc3QAAwAAAAAFFAAAACAAAwMTAZAABQAAApkCzAAAAI8CmQLMAAAB6wAzAQkAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADoAgPA/8AAQAPAAEAAAAABAAAAAAAAAAAAAAAgAAAAAAADAAAAAwAAABwAAQADAAAAHAADAAEAAAAcAAQAOAAAAAoACAACAAIAAQAg6AL//f//AAAAAAAg6AD//f//AAH/4xgEAAMAAQAAAAAAAAAAAAAAAQAB//8ADwABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAEUAUQO7AvgAGgAAARQHAQYjIicBJjU0PwE2MzIfAQE2MzIfARYVA7sQ/hQQFhcQ/uMQEE4QFxcQqAF2EBcXEE4QAnMWEP4UEBABHRAXFhBOEBCoAXcQEE4QFwAAAAABAAABbgMlAkkAFAAAARUUBwYjISInJj0BNDc2MyEyFxYVAyUQEBf9SRcQEBAQFwK3FxAQAhJtFxAQEBAXbRcQEBAQFwAAAAABAAAASQMlA24ALAAAARUUBwYrARUUBwYrASInJj0BIyInJj0BNDc2OwE1NDc2OwEyFxYdATMyFxYVAyUQEBfuEBAXbhYQEO4XEBAQEBfuEBAWbhcQEO4XEBACEm0XEBDuFxAQEBAX7hAQF20XEBDuFxAQEBAX7hAQFwAAAQAAAAIAAHRSzT9fDzz1AAsEAAAAAADRsdR3AAAAANGx1HcAAAAAA7sDbgAAAAgAAgAAAAAAAAABAAADwP/AAAAEAAAAAAADuwABAAAAAAAAAAAAAAAAAAAABwQAAAAAAAAAAAAAAAIAAAAEAABFAyUAAAMlAAAAAAAAAAoAFAAeAE4AcgCwAAEAAAAHAC0AAQAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAOAK4AAQAAAAAAAQAIAAAAAQAAAAAAAgAHAGkAAQAAAAAAAwAIADkAAQAAAAAABAAIAH4AAQAAAAAABQALABgAAQAAAAAABgAIAFEAAQAAAAAACgAaAJYAAwABBAkAAQAQAAgAAwABBAkAAgAOAHAAAwABBAkAAwAQAEEAAwABBAkABAAQAIYAAwABBAkABQAWACMAAwABBAkABgAQAFkAAwABBAkACgA0ALBDaGVja2JveABDAGgAZQBjAGsAYgBvAHhWZXJzaW9uIDIuMABWAGUAcgBzAGkAbwBuACAAMgAuADBDaGVja2JveABDAGgAZQBjAGsAYgBvAHhDaGVja2JveABDAGgAZQBjAGsAYgBvAHhSZWd1bGFyAFIAZQBnAHUAbABhAHJDaGVja2JveABDAGgAZQBjAGsAYgBvAHhGb250IGdlbmVyYXRlZCBieSBJY29Nb29uLgBGAG8AbgB0ACAAZwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABJAGMAbwBNAG8AbwBuAC4AAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) format('truetype');
}
/* Checkmark */
.ui.checkbox label:after,
.ui.checkbox .box:after {
font-family: 'Checkbox';
}
/* Checked */
.ui.checkbox input:checked ~ .box:after,
.ui.checkbox input:checked ~ label:after {
content: '\e800';
}
/* Indeterminate */
.ui.checkbox input:indeterminate ~ .box:after,
.ui.checkbox input:indeterminate ~ label:after {
font-size: 12px;
content: '\e801';
}
/* UTF Reference
.check:before { content: '\e800'; }
.dash:before { content: '\e801'; }
.plus:before { content: '\e802'; }
*/
/*******************************
Site Overrides
*******************************/

View File

@ -1184,883 +1184,6 @@ $.api.settings = {
})( jQuery, window, document );
/*!
* # Fomantic-UI - Checkbox
* http://github.com/fomantic/Fomantic-UI/
*
*
* Released under the MIT license
* http://opensource.org/licenses/MIT
*
*/
;(function ($, window, document, undefined) {
'use strict';
$.isFunction = $.isFunction || function(obj) {
return typeof obj === "function" && typeof obj.nodeType !== "number";
};
window = (typeof window != 'undefined' && window.Math == Math)
? window
: (typeof self != 'undefined' && self.Math == Math)
? self
: Function('return this')()
;
$.fn.checkbox = function(parameters) {
var
$allModules = $(this),
moduleSelector = $allModules.selector || '',
time = new Date().getTime(),
performance = [],
query = arguments[0],
methodInvoked = (typeof query == 'string'),
queryArguments = [].slice.call(arguments, 1),
returnedValue
;
$allModules
.each(function() {
var
settings = $.extend(true, {}, $.fn.checkbox.settings, parameters),
className = settings.className,
namespace = settings.namespace,
selector = settings.selector,
error = settings.error,
eventNamespace = '.' + namespace,
moduleNamespace = 'module-' + namespace,
$module = $(this),
$label = $(this).children(selector.label),
$input = $(this).children(selector.input),
input = $input[0],
initialLoad = false,
shortcutPressed = false,
instance = $module.data(moduleNamespace),
observer,
element = this,
module
;
module = {
initialize: function() {
module.verbose('Initializing checkbox', settings);
module.create.label();
module.bind.events();
module.set.tabbable();
module.hide.input();
module.observeChanges();
module.instantiate();
module.setup();
},
instantiate: function() {
module.verbose('Storing instance of module', module);
instance = module;
$module
.data(moduleNamespace, module)
;
},
destroy: function() {
module.verbose('Destroying module');
module.unbind.events();
module.show.input();
$module.removeData(moduleNamespace);
},
fix: {
reference: function() {
if( $module.is(selector.input) ) {
module.debug('Behavior called on <input> adjusting invoked element');
$module = $module.closest(selector.checkbox);
module.refresh();
}
}
},
setup: function() {
module.set.initialLoad();
if( module.is.indeterminate() ) {
module.debug('Initial value is indeterminate');
module.indeterminate();
}
else if( module.is.checked() ) {
module.debug('Initial value is checked');
module.check();
}
else {
module.debug('Initial value is unchecked');
module.uncheck();
}
module.remove.initialLoad();
},
refresh: function() {
$label = $module.children(selector.label);
$input = $module.children(selector.input);
input = $input[0];
},
hide: {
input: function() {
module.verbose('Modifying <input> z-index to be unselectable');
$input.addClass(className.hidden);
}
},
show: {
input: function() {
module.verbose('Modifying <input> z-index to be selectable');
$input.removeClass(className.hidden);
}
},
observeChanges: function() {
if('MutationObserver' in window) {
observer = new MutationObserver(function(mutations) {
module.debug('DOM tree modified, updating selector cache');
module.refresh();
});
observer.observe(element, {
childList : true,
subtree : true
});
module.debug('Setting up mutation observer', observer);
}
},
attachEvents: function(selector, event) {
var
$element = $(selector)
;
event = $.isFunction(module[event])
? module[event]
: module.toggle
;
if($element.length > 0) {
module.debug('Attaching checkbox events to element', selector, event);
$element
.on('click' + eventNamespace, event)
;
}
else {
module.error(error.notFound);
}
},
preventDefaultOnInputTarget: function() {
if(typeof event !== 'undefined' && event !== null && $(event.target).is(selector.input)) {
module.verbose('Preventing default check action after manual check action');
event.preventDefault();
}
},
event: {
change: function(event) {
if( !module.should.ignoreCallbacks() ) {
settings.onChange.call(input);
}
},
click: function(event) {
var
$target = $(event.target)
;
if( $target.is(selector.input) ) {
module.verbose('Using default check action on initialized checkbox');
return;
}
if( $target.is(selector.link) ) {
module.debug('Clicking link inside checkbox, skipping toggle');
return;
}
module.toggle();
$input.focus();
event.preventDefault();
},
keydown: function(event) {
var
key = event.which,
keyCode = {
enter : 13,
space : 32,
escape : 27,
left : 37,
up : 38,
right : 39,
down : 40
}
;
var r = module.get.radios(),
rIndex = r.index($module),
rLen = r.length,
checkIndex = false;
if(key == keyCode.left || key == keyCode.up) {
checkIndex = (rIndex === 0 ? rLen : rIndex) - 1;
} else if(key == keyCode.right || key == keyCode.down) {
checkIndex = rIndex === rLen-1 ? 0 : rIndex+1;
}
if (!module.should.ignoreCallbacks() && checkIndex !== false) {
if(settings.beforeUnchecked.apply(input)===false) {
module.verbose('Option not allowed to be unchecked, cancelling key navigation');
return false;
}
if (settings.beforeChecked.apply($(r[checkIndex]).children(selector.input)[0])===false) {
module.verbose('Next option should not allow check, cancelling key navigation');
return false;
}
}
if(key == keyCode.escape) {
module.verbose('Escape key pressed blurring field');
$input.blur();
shortcutPressed = true;
}
else if(!event.ctrlKey && ( key == keyCode.space || (key == keyCode.enter && settings.enableEnterKey)) ) {
module.verbose('Enter/space key pressed, toggling checkbox');
module.toggle();
shortcutPressed = true;
}
else {
shortcutPressed = false;
}
},
keyup: function(event) {
if(shortcutPressed) {
event.preventDefault();
}
}
},
check: function() {
if( !module.should.allowCheck() ) {
return;
}
module.debug('Checking checkbox', $input);
module.set.checked();
if( !module.should.ignoreCallbacks() ) {
settings.onChecked.call(input);
module.trigger.change();
}
module.preventDefaultOnInputTarget();
},
uncheck: function() {
if( !module.should.allowUncheck() ) {
return;
}
module.debug('Unchecking checkbox');
module.set.unchecked();
if( !module.should.ignoreCallbacks() ) {
settings.onUnchecked.call(input);
module.trigger.change();
}
module.preventDefaultOnInputTarget();
},
indeterminate: function() {
if( module.should.allowIndeterminate() ) {
module.debug('Checkbox is already indeterminate');
return;
}
module.debug('Making checkbox indeterminate');
module.set.indeterminate();
if( !module.should.ignoreCallbacks() ) {
settings.onIndeterminate.call(input);
module.trigger.change();
}
},
determinate: function() {
if( module.should.allowDeterminate() ) {
module.debug('Checkbox is already determinate');
return;
}
module.debug('Making checkbox determinate');
module.set.determinate();
if( !module.should.ignoreCallbacks() ) {
settings.onDeterminate.call(input);
module.trigger.change();
}
},
enable: function() {
if( module.is.enabled() ) {
module.debug('Checkbox is already enabled');
return;
}
module.debug('Enabling checkbox');
module.set.enabled();
if( !module.should.ignoreCallbacks() ) {
settings.onEnable.call(input);
// preserve legacy callbacks
settings.onEnabled.call(input);
module.trigger.change();
}
},
disable: function() {
if( module.is.disabled() ) {
module.debug('Checkbox is already disabled');
return;
}
module.debug('Disabling checkbox');
module.set.disabled();
if( !module.should.ignoreCallbacks() ) {
settings.onDisable.call(input);
// preserve legacy callbacks
settings.onDisabled.call(input);
module.trigger.change();
}
},
get: {
radios: function() {
var
name = module.get.name()
;
return $('input[name="' + name + '"]').closest(selector.checkbox);
},
otherRadios: function() {
return module.get.radios().not($module);
},
name: function() {
return $input.attr('name');
}
},
is: {
initialLoad: function() {
return initialLoad;
},
radio: function() {
return ($input.hasClass(className.radio) || $input.attr('type') == 'radio');
},
indeterminate: function() {
return $input.prop('indeterminate') !== undefined && $input.prop('indeterminate');
},
checked: function() {
return $input.prop('checked') !== undefined && $input.prop('checked');
},
disabled: function() {
return $input.prop('disabled') !== undefined && $input.prop('disabled');
},
enabled: function() {
return !module.is.disabled();
},
determinate: function() {
return !module.is.indeterminate();
},
unchecked: function() {
return !module.is.checked();
}
},
should: {
allowCheck: function() {
if(module.is.determinate() && module.is.checked() && !module.is.initialLoad() ) {
module.debug('Should not allow check, checkbox is already checked');
return false;
}
if(!module.should.ignoreCallbacks() && settings.beforeChecked.apply(input) === false) {
module.debug('Should not allow check, beforeChecked cancelled');
return false;
}
return true;
},
allowUncheck: function() {
if(module.is.determinate() && module.is.unchecked() && !module.is.initialLoad() ) {
module.debug('Should not allow uncheck, checkbox is already unchecked');
return false;
}
if(!module.should.ignoreCallbacks() && settings.beforeUnchecked.apply(input) === false) {
module.debug('Should not allow uncheck, beforeUnchecked cancelled');
return false;
}
return true;
},
allowIndeterminate: function() {
if(module.is.indeterminate() && !module.is.initialLoad() ) {
module.debug('Should not allow indeterminate, checkbox is already indeterminate');
return false;
}
if(!module.should.ignoreCallbacks() && settings.beforeIndeterminate.apply(input) === false) {
module.debug('Should not allow indeterminate, beforeIndeterminate cancelled');
return false;
}
return true;
},
allowDeterminate: function() {
if(module.is.determinate() && !module.is.initialLoad() ) {
module.debug('Should not allow determinate, checkbox is already determinate');
return false;
}
if(!module.should.ignoreCallbacks() && settings.beforeDeterminate.apply(input) === false) {
module.debug('Should not allow determinate, beforeDeterminate cancelled');
return false;
}
return true;
},
ignoreCallbacks: function() {
return (initialLoad && !settings.fireOnInit);
}
},
can: {
change: function() {
return !( $module.hasClass(className.disabled) || $module.hasClass(className.readOnly) || $input.prop('disabled') || $input.prop('readonly') );
},
uncheck: function() {
return (typeof settings.uncheckable === 'boolean')
? settings.uncheckable
: !module.is.radio()
;
}
},
set: {
initialLoad: function() {
initialLoad = true;
},
checked: function() {
module.verbose('Setting class to checked');
$module
.removeClass(className.indeterminate)
.addClass(className.checked)
;
if( module.is.radio() ) {
module.uncheckOthers();
}
if(!module.is.indeterminate() && module.is.checked()) {
module.debug('Input is already checked, skipping input property change');
return;
}
module.verbose('Setting state to checked', input);
$input
.prop('indeterminate', false)
.prop('checked', true)
;
},
unchecked: function() {
module.verbose('Removing checked class');
$module
.removeClass(className.indeterminate)
.removeClass(className.checked)
;
if(!module.is.indeterminate() && module.is.unchecked() ) {
module.debug('Input is already unchecked');
return;
}
module.debug('Setting state to unchecked');
$input
.prop('indeterminate', false)
.prop('checked', false)
;
},
indeterminate: function() {
module.verbose('Setting class to indeterminate');
$module
.addClass(className.indeterminate)
;
if( module.is.indeterminate() ) {
module.debug('Input is already indeterminate, skipping input property change');
return;
}
module.debug('Setting state to indeterminate');
$input
.prop('indeterminate', true)
;
},
determinate: function() {
module.verbose('Removing indeterminate class');
$module
.removeClass(className.indeterminate)
;
if( module.is.determinate() ) {
module.debug('Input is already determinate, skipping input property change');
return;
}
module.debug('Setting state to determinate');
$input
.prop('indeterminate', false)
;
},
disabled: function() {
module.verbose('Setting class to disabled');
$module
.addClass(className.disabled)
;
if( module.is.disabled() ) {
module.debug('Input is already disabled, skipping input property change');
return;
}
module.debug('Setting state to disabled');
$input
.prop('disabled', 'disabled')
;
},
enabled: function() {
module.verbose('Removing disabled class');
$module.removeClass(className.disabled);
if( module.is.enabled() ) {
module.debug('Input is already enabled, skipping input property change');
return;
}
module.debug('Setting state to enabled');
$input
.prop('disabled', false)
;
},
tabbable: function() {
module.verbose('Adding tabindex to checkbox');
if( $input.attr('tabindex') === undefined) {
$input.attr('tabindex', 0);
}
}
},
remove: {
initialLoad: function() {
initialLoad = false;
}
},
trigger: {
change: function() {
var
inputElement = $input[0]
;
if(inputElement) {
var events = document.createEvent('HTMLEvents');
module.verbose('Triggering native change event');
events.initEvent('change', true, false);
inputElement.dispatchEvent(events);
}
}
},
create: {
label: function() {
if($input.prevAll(selector.label).length > 0) {
$input.prev(selector.label).detach().insertAfter($input);
module.debug('Moving existing label', $label);
}
else if( !module.has.label() ) {
$label = $('<label>').insertAfter($input);
module.debug('Creating label', $label);
}
}
},
has: {
label: function() {
return ($label.length > 0);
}
},
bind: {
events: function() {
module.verbose('Attaching checkbox events');
$module
.on('click' + eventNamespace, module.event.click)
.on('change' + eventNamespace, module.event.change)
.on('keydown' + eventNamespace, selector.input, module.event.keydown)
.on('keyup' + eventNamespace, selector.input, module.event.keyup)
;
}
},
unbind: {
events: function() {
module.debug('Removing events');
$module
.off(eventNamespace)
;
}
},
uncheckOthers: function() {
var
$radios = module.get.otherRadios()
;
module.debug('Unchecking other radios', $radios);
$radios.removeClass(className.checked);
},
toggle: function() {
if( !module.can.change() ) {
if(!module.is.radio()) {
module.debug('Checkbox is read-only or disabled, ignoring toggle');
}
return;
}
if( module.is.indeterminate() || module.is.unchecked() ) {
module.debug('Currently unchecked');
module.check();
}
else if( module.is.checked() && module.can.uncheck() ) {
module.debug('Currently checked');
module.uncheck();
}
},
setting: function(name, value) {
module.debug('Changing setting', name, value);
if( $.isPlainObject(name) ) {
$.extend(true, settings, name);
}
else if(value !== undefined) {
if($.isPlainObject(settings[name])) {
$.extend(true, settings[name], value);
}
else {
settings[name] = value;
}
}
else {
return settings[name];
}
},
internal: function(name, value) {
if( $.isPlainObject(name) ) {
$.extend(true, module, name);
}
else if(value !== undefined) {
module[name] = value;
}
else {
return module[name];
}
},
debug: function() {
if(!settings.silent && settings.debug) {
if(settings.performance) {
module.performance.log(arguments);
}
else {
module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
module.debug.apply(console, arguments);
}
}
},
verbose: function() {
if(!settings.silent && settings.verbose && settings.debug) {
if(settings.performance) {
module.performance.log(arguments);
}
else {
module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
module.verbose.apply(console, arguments);
}
}
},
error: function() {
if(!settings.silent) {
module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
module.error.apply(console, arguments);
}
},
performance: {
log: function(message) {
var
currentTime,
executionTime,
previousTime
;
if(settings.performance) {
currentTime = new Date().getTime();
previousTime = time || currentTime;
executionTime = currentTime - previousTime;
time = currentTime;
performance.push({
'Name' : message[0],
'Arguments' : [].slice.call(message, 1) || '',
'Element' : element,
'Execution Time' : executionTime
});
}
clearTimeout(module.performance.timer);
module.performance.timer = setTimeout(module.performance.display, 500);
},
display: function() {
var
title = settings.name + ':',
totalTime = 0
;
time = false;
clearTimeout(module.performance.timer);
$.each(performance, function(index, data) {
totalTime += data['Execution Time'];
});
title += ' ' + totalTime + 'ms';
if(moduleSelector) {
title += ' \'' + moduleSelector + '\'';
}
if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
console.groupCollapsed(title);
if(console.table) {
console.table(performance);
}
else {
$.each(performance, function(index, data) {
console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
});
}
console.groupEnd();
}
performance = [];
}
},
invoke: function(query, passedArguments, context) {
var
object = instance,
maxDepth,
found,
response
;
passedArguments = passedArguments || queryArguments;
context = element || context;
if(typeof query == 'string' && object !== undefined) {
query = query.split(/[\. ]/);
maxDepth = query.length - 1;
$.each(query, function(depth, value) {
var camelCaseValue = (depth != maxDepth)
? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
: query
;
if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
object = object[camelCaseValue];
}
else if( object[camelCaseValue] !== undefined ) {
found = object[camelCaseValue];
return false;
}
else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
object = object[value];
}
else if( object[value] !== undefined ) {
found = object[value];
return false;
}
else {
module.error(error.method, query);
return false;
}
});
}
if ( $.isFunction( found ) ) {
response = found.apply(context, passedArguments);
}
else if(found !== undefined) {
response = found;
}
if(Array.isArray(returnedValue)) {
returnedValue.push(response);
}
else if(returnedValue !== undefined) {
returnedValue = [returnedValue, response];
}
else if(response !== undefined) {
returnedValue = response;
}
return found;
}
};
if(methodInvoked) {
if(instance === undefined) {
module.initialize();
}
module.invoke(query);
}
else {
if(instance !== undefined) {
instance.invoke('destroy');
}
module.initialize();
}
})
;
return (returnedValue !== undefined)
? returnedValue
: this
;
};
$.fn.checkbox.settings = {
name : 'Checkbox',
namespace : 'checkbox',
silent : false,
debug : false,
verbose : true,
performance : true,
// delegated event context
uncheckable : 'auto',
fireOnInit : false,
enableEnterKey : true,
onChange : function(){},
beforeChecked : function(){},
beforeUnchecked : function(){},
beforeDeterminate : function(){},
beforeIndeterminate : function(){},
onChecked : function(){},
onUnchecked : function(){},
onDeterminate : function() {},
onIndeterminate : function() {},
onEnable : function(){},
onDisable : function(){},
// preserve misspelled callbacks (will be removed in 3.0)
onEnabled : function(){},
onDisabled : function(){},
className : {
checked : 'checked',
indeterminate : 'indeterminate',
disabled : 'disabled',
hidden : 'hidden',
radio : 'radio',
readOnly : 'read-only'
},
error : {
method : 'The method you called is not defined'
},
selector : {
checkbox : '.ui.checkbox',
label : 'label, .box',
input : 'input[type="checkbox"], input[type="radio"]',
link : 'a[href]'
}
};
})( jQuery, window, document );
/*!

View File

@ -23,7 +23,6 @@
"components": [
"api",
"button",
"checkbox",
"dimmer",
"dropdown",
"form",

View File

@ -218,17 +218,24 @@ export function initAdminCommon() {
});
// Select actions
const $checkboxes = $('.select.table .ui.checkbox');
const checkboxes = document.querySelectorAll('.select.table .ui.checkbox input');
$('.select.action').on('click', function () {
switch ($(this).data('action')) {
case 'select-all':
$checkboxes.checkbox('check');
for (const checkbox of checkboxes) {
checkbox.checked = true;
}
break;
case 'deselect-all':
$checkboxes.checkbox('uncheck');
for (const checkbox of checkboxes) {
checkbox.checked = false;
}
break;
case 'inverse':
$checkboxes.checkbox('toggle');
for (const checkbox of checkboxes) {
checkbox.checked = !checkbox.checked;
}
break;
}
});
@ -236,11 +243,11 @@ export function initAdminCommon() {
e.preventDefault();
this.classList.add('is-loading', 'disabled');
const data = new FormData();
$checkboxes.each(function () {
if ($(this).checkbox('is checked')) {
data.append('ids[]', this.getAttribute('data-id'));
for (const checkbox of checkboxes) {
if (checkbox.checked) {
data.append('ids[]', checkbox.closest('.ui.checkbox').getAttribute('data-id'));
}
});
}
await POST(this.getAttribute('data-link'), {data});
window.location.href = this.getAttribute('data-redirect');
});

View File

@ -1,12 +1,31 @@
import $ from 'jquery';
export async function initColorPickers(selector = '.js-color-picker-input input', opts = {}) {
const inputEls = document.querySelectorAll(selector);
if (!inputEls.length) return;
export async function createColorPicker(els) {
if (!els.length) return;
await Promise.all([
import(/* webpackChunkName: "minicolors" */'@claviska/jquery-minicolors'),
import(/* webpackChunkName: "minicolors" */'@claviska/jquery-minicolors/jquery.minicolors.css'),
const [{coloris, init}] = await Promise.all([
import(/* webpackChunkName: "colorpicker" */'@melloware/coloris'),
import(/* webpackChunkName: "colorpicker" */'../../css/features/colorpicker.css'),
]);
return $(els).minicolors();
init();
coloris({
el: selector,
alpha: false,
focusInput: true,
selectInput: false,
...opts,
});
for (const inputEl of inputEls) {
const parent = inputEl.closest('.js-color-picker-input');
// prevent tabbing on the color preview `button` inside the input
parent.querySelector('button').tabIndex = -1;
// init precolors
for (const el of parent.querySelectorAll('.precolors .color')) {
el.addEventListener('click', (e) => {
inputEl.value = e.target.getAttribute('data-color-hex');
inputEl.dispatchEvent(new Event('input', {bubbles: true}));
});
}
}
}

View File

@ -2,7 +2,6 @@ import $ from 'jquery';
import '../vendor/jquery.are-you-sure.js';
import {clippie} from 'clippie';
import {createDropzone} from './dropzone.js';
import {initCompColorPicker} from './comp/ColorPicker.js';
import {showGlobalErrorMessage} from '../bootstrap.js';
import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.js';
import {svg} from '../svg.js';
@ -196,8 +195,6 @@ export function initGlobalCommon() {
$uiDropdowns.filter('.upward').dropdown('setting', 'direction', 'upward');
$uiDropdowns.filter('.downward').dropdown('setting', 'direction', 'downward');
$('.ui.checkbox').checkbox();
$('.tabular.menu .item').tab();
initSubmitEventPolyfill();
@ -379,10 +376,7 @@ function initGlobalShowModal() {
$attrTarget.text(attrib.value); // FIXME: it should be more strict here, only handle div/span/p
}
}
const $colorPickers = $modal.find('.color-picker');
if ($colorPickers.length > 0) {
initCompColorPicker(); // FIXME: this might cause duplicate init
}
$modal.modal('setting', {
onApprove: () => {
// "form-fetch-action" can handle network errors gracefully,

View File

@ -1,16 +0,0 @@
import $ from 'jquery';
import {createColorPicker} from '../colorpicker.js';
export function initCompColorPicker() {
(async () => {
await createColorPicker(document.querySelectorAll('.color-picker'));
for (const el of document.querySelectorAll('.precolors .color')) {
el.addEventListener('click', (e) => {
const color = e.target.getAttribute('data-color-hex');
const parent = e.target.closest('.color.picker');
$(parent.querySelector('.color-picker')).minicolors('value', color);
});
}
})();
}

View File

@ -1,5 +1,4 @@
import $ from 'jquery';
import {initCompColorPicker} from './ColorPicker.js';
function isExclusiveScopeName(name) {
return /.*[^/]\/[^/].*/.test(name);
@ -28,13 +27,17 @@ function updateExclusiveLabelEdit(form) {
export function initCompLabelEdit(selector) {
if (!$(selector).length) return;
initCompColorPicker();
// Create label
$('.new-label.button').on('click', () => {
updateExclusiveLabelEdit('.new-label');
$('.new-label.modal').modal({
onApprove() {
const form = document.querySelector('.new-label.form');
if (!form.checkValidity()) {
form.reportValidity();
return false;
}
$('.new-label.form').trigger('submit');
},
}).modal('show');
@ -60,10 +63,18 @@ export function initCompLabelEdit(selector) {
updateExclusiveLabelEdit('.edit-label');
$('.edit-label .label-desc-input').val(this.getAttribute('data-description'));
$('.edit-label .color-picker').minicolors('value', this.getAttribute('data-color'));
const colorInput = document.querySelector('.edit-label .js-color-picker-input input');
colorInput.value = this.getAttribute('data-color');
colorInput.dispatchEvent(new Event('input', {bubbles: true}));
$('.edit-label.modal').modal({
onApprove() {
const form = document.querySelector('.edit-label.form');
if (!form.checkValidity()) {
form.reportValidity();
return false;
}
$('.edit-label.form').trigger('submit');
},
}).modal('show');

View File

@ -282,32 +282,26 @@ export function initRepoPullRequestMergeInstruction() {
}
export function initRepoPullRequestAllowMaintainerEdit() {
const checkbox = document.getElementById('allow-edits-from-maintainers');
if (!checkbox) return;
const wrapper = document.getElementById('allow-edits-from-maintainers');
if (!wrapper) return;
const $checkbox = $(checkbox);
const promptError = checkbox.getAttribute('data-prompt-error');
$checkbox.checkbox({
'onChange': async () => {
const checked = $checkbox.checkbox('is checked');
let url = checkbox.getAttribute('data-url');
url += '/set_allow_maintainer_edit';
$checkbox.checkbox('set disabled');
try {
const response = await POST(url, {
data: {allow_maintainer_edit: checked},
});
if (!response.ok) {
throw new Error('Failed to update maintainer edit permission');
}
} catch (error) {
console.error(error);
showTemporaryTooltip(checkbox, promptError);
} finally {
$checkbox.checkbox('set enabled');
wrapper.querySelector('input[type="checkbox"]')?.addEventListener('change', async (e) => {
const checked = e.target.checked;
const url = `${wrapper.getAttribute('data-url')}/set_allow_maintainer_edit`;
wrapper.classList.add('is-loading');
e.target.disabled = true;
try {
const response = await POST(url, {data: {allow_maintainer_edit: checked}});
if (!response.ok) {
throw new Error('Failed to update maintainer edit permission');
}
},
} catch (error) {
console.error(error);
showTemporaryTooltip(wrapper, wrapper.getAttribute('data-prompt-error'));
} finally {
wrapper.classList.remove('is-loading');
e.target.disabled = false;
}
});
}

View File

@ -86,6 +86,7 @@ import {initRepoRecentCommits} from './features/recent-commits.js';
import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.js';
import {initDirAuto} from './modules/dirauto.js';
import {initRepositorySearch} from './features/repo-search.js';
import {initColorPickers} from './features/colorpicker.js';
// Init Gitea's Fomantic settings
initGiteaFomantic();
@ -188,4 +189,5 @@ onDomReady(() => {
initRepoDiffView();
initPdfViewer();
initScopedAccessTokenCategories();
initColorPickers();
});

View File

@ -11,8 +11,6 @@ export const fomanticMobileScreen = window.matchMedia('only screen and (max-widt
export function initGiteaFomantic() {
// Silence fomantic's error logging when tabs are used without a target content element
$.fn.tab.settings.silent = true;
// Disable the behavior of fomantic to toggle the checkbox when you press enter on a checkbox element.
$.fn.checkbox.settings.enableEnterKey = false;
// By default, use "exact match" for full text search
$.fn.dropdown.settings.fullTextSearch = 'exact';

View File

@ -41,24 +41,19 @@ The ideal checkboxes should be:
<label><input type="checkbox"> ... </label>
```
However, related CSS styles aren't supported (not implemented) yet, so at the moment,
almost all the checkboxes are still using Fomantic UI checkbox.
## Fomantic UI Checkbox
However, the templates still have the Fomantic-style HTML layout:
```html
<div class="ui checkbox">
<input type="checkbox"> <!-- class "hidden" will be added by $.checkbox() -->
<input type="checkbox">
<label>...</label>
</div>
```
Then the JS `$.checkbox()` should be called to make it work with keyboard and label-clicking,
then it works like the ideal checkboxes.
There is still a problem: Fomantic UI checkbox is not friendly to screen readers,
so we add IDs to all the Fomantic UI checkboxes automatically by JS.
If the `label` part is empty, then the checkbox needs to get the `aria-label` attribute manually.
We call `initAriaCheckboxPatch` to link the `input` and `label` which makes clicking the
label etc. work. There is still a problem: These checkboxes are not friendly to screen readers,
so we add IDs to all the Fomantic UI checkboxes automatically by JS. If the `label` part is empty,
then the checkbox needs to get the `aria-label` attribute manually.
# Fomantic Dropdown

View File

@ -1,38 +1,15 @@
import $ from 'jquery';
import {generateAriaId} from './base.js';
const ariaPatchKey = '_giteaAriaPatchCheckbox';
const fomanticCheckboxFn = $.fn.checkbox;
// use our own `$.fn.checkbox` to patch Fomantic's checkbox module
export function initAriaCheckboxPatch() {
if ($.fn.checkbox === ariaCheckboxFn) throw new Error('initAriaCheckboxPatch could only be called once');
$.fn.checkbox = ariaCheckboxFn;
ariaCheckboxFn.settings = fomanticCheckboxFn.settings;
}
// the patched `$.fn.checkbox` checkbox function
// * it does the one-time attaching on the first call
function ariaCheckboxFn(...args) {
const ret = fomanticCheckboxFn.apply(this, args);
for (const el of this) {
if (el[ariaPatchKey]) continue;
attachInit(el);
// link the label and the input element so it's clickable and accessible
for (const el of document.querySelectorAll('.ui.checkbox')) {
if (el.hasAttribute('data-checkbox-patched')) continue;
const label = el.querySelector('label');
const input = el.querySelector('input');
if (!label || !input || input.getAttribute('id') || label.getAttribute('for')) continue;
const id = generateAriaId();
input.setAttribute('id', id);
label.setAttribute('for', id);
el.setAttribute('data-checkbox-patched', 'true');
}
return ret;
}
function attachInit(el) {
// Fomantic UI checkbox needs to be something like: <div class="ui checkbox"><label /><input /></div>
// It doesn't work well with <label><input />...</label>
// To make it work with aria, the "id"/"for" attributes are necessary, so add them automatically if missing.
// In the future, refactor to use native checkbox directly, then this patch could be removed.
el[ariaPatchKey] = {}; // record that this element has been patched
const label = el.querySelector('label');
const input = el.querySelector('input');
if (!label || !input || input.getAttribute('id')) return;
const id = generateAriaId();
input.setAttribute('id', id);
label.setAttribute('for', id);
}

View File

@ -192,13 +192,6 @@ export default {
filename: 'fonts/[name].[contenthash:8][ext]',
},
},
{
test: /\.png$/i,
type: 'asset/resource',
generator: {
filename: 'img/webpack/[name].[contenthash:8][ext]',
},
},
],
},
plugins: [