mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-14 04:20:42 +02:00
Merge 4308598c98550ee5ff717aaef4fb3bb425796dd6 into a5d81d9ce230aaa6e1021b6236ca01cb6d2b56c3
This commit is contained in:
commit
53ba749b8c
@ -115,7 +115,7 @@ func init() {
|
||||
db.RegisterModel(new(Notification))
|
||||
}
|
||||
|
||||
// CreateRepoTransferNotification creates notification for the user a repository was transferred to
|
||||
// CreateRepoTransferNotification creates notification for the user a repository was transferred to
|
||||
func CreateRepoTransferNotification(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
var notify []*Notification
|
||||
|
||||
@ -101,7 +101,12 @@ func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, n
|
||||
}
|
||||
toNotify.AddMultiple(issueWatches...)
|
||||
if !(issue.IsPull && issues_model.HasWorkInProgressPrefix(issue.Title)) {
|
||||
repoWatches, err := repo_model.GetRepoWatchersIDs(ctx, issue.RepoID)
|
||||
repoWatchType := repo_model.WatchIssues
|
||||
if issue.IsPull {
|
||||
repoWatchType = repo_model.WatchPullRequests
|
||||
}
|
||||
|
||||
repoWatches, err := repo_model.GetRepoWatchersIDs(ctx, issue.RepoID, repoWatchType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -112,6 +117,18 @@ func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, n
|
||||
return err
|
||||
}
|
||||
toNotify.AddMultiple(issueParticipants...)
|
||||
issueAssignees, err := issues_model.GetAssigneeIDsByIssue(ctx, issueID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
toNotify.AddMultiple(issueAssignees...)
|
||||
if issue.IsPull {
|
||||
issueReviewers, err := issues_model.GetPullRequestRequestedReviewerIDs(ctx, issueID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
toNotify.AddMultiple(issueReviewers...)
|
||||
}
|
||||
|
||||
// don't notify user who cause notification
|
||||
delete(toNotify, notificationAuthorID)
|
||||
|
||||
@ -7,9 +7,11 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/activities"
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
@ -138,3 +140,69 @@ func TestSetIssueReadBy(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, activities_model.NotificationStatusRead, nt.Status)
|
||||
}
|
||||
|
||||
func TestIssueNotificationWithWatchOptions(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
watcher := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
iss := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
|
||||
|
||||
assert.NoError(t, issues_model.RemoveIssueWatchersByRepoID(t.Context(), watcher.ID, repo.ID))
|
||||
assert.NoError(t, repo_model.WatchRepo(t.Context(), watcher, repo, true))
|
||||
assert.NoError(t, repo_model.WatchRepoOptions(t.Context(), watcher, repo, repo_model.WatchOptions{
|
||||
PullRequests: true,
|
||||
Issues: false,
|
||||
Releases: true,
|
||||
}))
|
||||
|
||||
assert.NoError(t, activities_model.CreateOrUpdateIssueNotifications(t.Context(), iss.ID, 0, doer.ID, 0))
|
||||
notification, err := activities.GetIssueNotification(t.Context(), watcher.ID, iss.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(0), notification.IssueID) // No notification found
|
||||
|
||||
assert.NoError(t, repo_model.WatchRepoOptions(t.Context(), watcher, repo, repo_model.WatchOptions{
|
||||
PullRequests: true,
|
||||
Issues: true,
|
||||
Releases: true,
|
||||
}))
|
||||
|
||||
assert.NoError(t, activities_model.CreateOrUpdateIssueNotifications(t.Context(), iss.ID, 0, doer.ID, 0))
|
||||
notification, err = activities.GetIssueNotification(t.Context(), watcher.ID, iss.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, activities.NotificationStatusUnread, notification.Status)
|
||||
}
|
||||
|
||||
func TestPullRequestNotificationWithWatchOptions(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
watcher := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 8})
|
||||
|
||||
assert.NoError(t, repo_model.WatchRepo(t.Context(), watcher, repo2, true))
|
||||
assert.NoError(t, repo_model.WatchRepoOptions(t.Context(), watcher, repo2, repo_model.WatchOptions{
|
||||
PullRequests: false,
|
||||
Issues: true,
|
||||
Releases: true,
|
||||
}))
|
||||
|
||||
assert.NoError(t, activities_model.CreateOrUpdateIssueNotifications(t.Context(), pr.ID, 0, doer.ID, 0))
|
||||
notification, err := activities.GetIssueNotification(t.Context(), watcher.ID, pr.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(0), notification.IssueID) // No notification found
|
||||
|
||||
assert.NoError(t, repo_model.WatchRepoOptions(t.Context(), watcher, repo2, repo_model.WatchOptions{
|
||||
PullRequests: true,
|
||||
Issues: true,
|
||||
Releases: true,
|
||||
}))
|
||||
|
||||
assert.NoError(t, activities_model.CreateOrUpdateIssueNotifications(t.Context(), pr.ID, 0, doer.ID, 0))
|
||||
notification, err = activities.GetIssueNotification(t.Context(), watcher.ID, pr.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, activities.NotificationStatusUnread, notification.Status)
|
||||
}
|
||||
|
||||
@ -1,43 +1,56 @@
|
||||
-
|
||||
id: 1
|
||||
- id: 1
|
||||
user_id: 1
|
||||
repo_id: 1
|
||||
mode: 1 # normal
|
||||
pull_requests: true
|
||||
issues: true
|
||||
releases: true
|
||||
|
||||
-
|
||||
id: 2
|
||||
- id: 2
|
||||
user_id: 4
|
||||
repo_id: 1
|
||||
mode: 1 # normal
|
||||
pull_requests: true
|
||||
issues: true
|
||||
releases: true
|
||||
|
||||
-
|
||||
id: 3
|
||||
- id: 3
|
||||
user_id: 9
|
||||
repo_id: 1
|
||||
mode: 1 # normal
|
||||
pull_requests: true
|
||||
issues: true
|
||||
releases: true
|
||||
|
||||
-
|
||||
id: 4
|
||||
- id: 4
|
||||
user_id: 8
|
||||
repo_id: 1
|
||||
mode: 2 # don't watch
|
||||
pull_requests: true
|
||||
issues: true
|
||||
releases: true
|
||||
|
||||
-
|
||||
id: 5
|
||||
- id: 5
|
||||
user_id: 11
|
||||
repo_id: 1
|
||||
mode: 3 # auto
|
||||
pull_requests: true
|
||||
issues: true
|
||||
releases: true
|
||||
|
||||
-
|
||||
id: 6
|
||||
- id: 6
|
||||
user_id: 10
|
||||
repo_id: 21
|
||||
mode: 1 # normal
|
||||
pull_requests: true
|
||||
issues: true
|
||||
releases: true
|
||||
|
||||
-
|
||||
id: 7
|
||||
- id: 7
|
||||
user_id: 10
|
||||
repo_id: 32
|
||||
mode: 1 # normal
|
||||
|
||||
pull_requests: true
|
||||
issues: true
|
||||
releases: true
|
||||
# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly
|
||||
|
||||
@ -81,7 +81,14 @@ func CheckIssueWatch(ctx context.Context, user *user_model.User, issue *Issue) (
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return repo_model.IsWatchMode(w.Mode) || IsUserParticipantsOfIssue(ctx, user, issue), nil
|
||||
if repo_model.IsWatchMode(w.Mode) {
|
||||
if issue.IsPull {
|
||||
return w.PullRequests, nil
|
||||
} else {
|
||||
return w.Issues, nil
|
||||
}
|
||||
}
|
||||
return IsUserParticipantsOfIssue(ctx, user, issue), nil
|
||||
}
|
||||
|
||||
// GetIssueWatchersIDs returns IDs of subscribers or explicit unsubscribers to a given issue id
|
||||
|
||||
@ -1008,3 +1008,14 @@ func GetPullRequestByMergedCommit(ctx context.Context, repoID int64, sha string)
|
||||
|
||||
return pr, nil
|
||||
}
|
||||
|
||||
// GetPullRequestRequestedReviewerIDs returns IDs of reviewers requested for the given pull request
|
||||
func GetPullRequestRequestedReviewerIDs(ctx context.Context, issueID int64) ([]int64, error) {
|
||||
userIDs := make([]int64, 0, 5)
|
||||
return userIDs, db.GetEngine(ctx).
|
||||
Table("review").
|
||||
Cols("reviewer_id").
|
||||
Where("issue_id=?", issueID).
|
||||
Distinct("reviewer_id").
|
||||
Find(&userIDs)
|
||||
}
|
||||
|
||||
@ -409,6 +409,7 @@ func prepareMigrationTasks() []*migration {
|
||||
// Gitea 1.26.0 ends at migration ID number 330 (database version 331)
|
||||
|
||||
newMigration(331, "Add ActionRunAttempt model and related action fields", v1_27.AddActionRunAttemptModel),
|
||||
newMigration(332, "Add watch options", v1_27.AddWatchOptions),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
21
models/migrations/v1_27/v332.go
Normal file
21
models/migrations/v1_27/v332.go
Normal file
@ -0,0 +1,21 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_27
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func AddWatchOptions(x *xorm.Engine) error {
|
||||
type Watch struct { //revive:disable-line:exported
|
||||
PullRequests bool `xorm:"NOT NULL DEFAULT true"`
|
||||
Issues bool `xorm:"NOT NULL DEFAULT true"`
|
||||
Releases bool `xorm:"NOT NULL DEFAULT true"`
|
||||
}
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreConstrains: true,
|
||||
IgnoreIndices: true,
|
||||
}, new(Watch))
|
||||
return err
|
||||
}
|
||||
@ -26,14 +26,25 @@ const (
|
||||
WatchModeAuto // 3
|
||||
)
|
||||
|
||||
type WatchType int8
|
||||
|
||||
const (
|
||||
WatchPullRequests WatchType = iota // 0
|
||||
WatchIssues // 1
|
||||
WatchReleases // 2
|
||||
)
|
||||
|
||||
// Watch is connection request for receiving repository notification.
|
||||
type Watch struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
UserID int64 `xorm:"UNIQUE(watch)"`
|
||||
RepoID int64 `xorm:"UNIQUE(watch)"`
|
||||
Mode WatchMode `xorm:"SMALLINT NOT NULL DEFAULT 1"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
UserID int64 `xorm:"UNIQUE(watch)"`
|
||||
RepoID int64 `xorm:"UNIQUE(watch)"`
|
||||
Mode WatchMode `xorm:"SMALLINT NOT NULL DEFAULT 1"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
PullRequests bool `xorm:"NOT NULL DEFAULT true"`
|
||||
Issues bool `xorm:"NOT NULL DEFAULT true"`
|
||||
Releases bool `xorm:"NOT NULL DEFAULT true"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
@ -120,11 +131,37 @@ func WatchRepo(ctx context.Context, doer *user_model.User, repo *Repository, doW
|
||||
return user_model.ErrBlockedUser
|
||||
}
|
||||
|
||||
watch.PullRequests = true
|
||||
watch.Issues = true
|
||||
watch.Releases = true
|
||||
return watchRepoMode(ctx, watch, WatchModeNormal)
|
||||
}
|
||||
|
||||
// GetWatchers returns all watchers of given repository.
|
||||
func GetWatchers(ctx context.Context, repoID int64) ([]*Watch, error) {
|
||||
type WatchOptions struct {
|
||||
PullRequests bool
|
||||
Issues bool
|
||||
Releases bool
|
||||
}
|
||||
|
||||
func WatchRepoOptions(ctx context.Context, doer *user_model.User, repo *Repository, opts WatchOptions) error {
|
||||
watch := Watch{UserID: doer.ID, RepoID: repo.ID}
|
||||
if _, err := db.GetEngine(ctx).Get(&watch); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
watch.PullRequests = opts.PullRequests
|
||||
watch.Issues = opts.Issues
|
||||
watch.Releases = opts.Releases
|
||||
|
||||
if _, err := db.GetEngine(ctx).ID(watch.ID).Cols("pull_requests", "issues", "releases").Update(&watch); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRepoWatches returns all watches of given repository.
|
||||
func GetRepoWatches(ctx context.Context, repoID int64) ([]*Watch, error) {
|
||||
watches := make([]*Watch, 0, 10)
|
||||
return watches, db.GetEngine(ctx).Where("`watch`.repo_id=?", repoID).
|
||||
And("`watch`.mode<>?", WatchModeDont).
|
||||
@ -137,13 +174,45 @@ func GetWatchers(ctx context.Context, repoID int64) ([]*Watch, error) {
|
||||
// GetRepoWatchersIDs returns IDs of watchers for a given repo ID
|
||||
// but avoids joining with `user` for performance reasons
|
||||
// User permissions must be verified elsewhere if required
|
||||
func GetRepoWatchersIDs(ctx context.Context, repoID int64) ([]int64, error) {
|
||||
func GetRepoWatchersIDs(ctx context.Context, repoID int64, watchType WatchType) ([]int64, error) {
|
||||
ids := make([]int64, 0, 64)
|
||||
return ids, db.GetEngine(ctx).Table("watch").
|
||||
sess := db.GetEngine(ctx).Table("watch").
|
||||
Where("watch.repo_id=?", repoID).
|
||||
And("watch.mode<>?", WatchModeDont)
|
||||
|
||||
switch watchType {
|
||||
case WatchPullRequests:
|
||||
sess = sess.And("watch.pull_requests=?", true)
|
||||
case WatchIssues:
|
||||
sess = sess.And("watch.issues=?", true)
|
||||
case WatchReleases:
|
||||
sess = sess.And("watch.releases=?", true)
|
||||
}
|
||||
|
||||
return ids, sess.Select("user_id").Find(&ids)
|
||||
}
|
||||
|
||||
func GetWatches(ctx context.Context, repos []*Repository) (map[int64]*Watch, error) {
|
||||
repoIDs := make([]int64, 0, len(repos))
|
||||
for i := range repos {
|
||||
repoIDs = append(repoIDs, repos[i].ID)
|
||||
}
|
||||
|
||||
var watches []*Watch
|
||||
err := db.GetEngine(ctx).Table("watch").
|
||||
In("watch.repo_id", repoIDs).
|
||||
And("watch.mode<>?", WatchModeDont).
|
||||
Select("user_id").
|
||||
Find(&ids)
|
||||
Find(&watches)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
watchesByRepo := make(map[int64]*Watch, len(watches))
|
||||
for _, w := range watches {
|
||||
watchesByRepo[w.RepoID] = w
|
||||
}
|
||||
|
||||
return watchesByRepo, nil
|
||||
}
|
||||
|
||||
// GetRepoWatchers returns range of users watching given repository.
|
||||
|
||||
@ -32,7 +32,7 @@ func TestGetWatchers(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
watches, err := repo_model.GetWatchers(t.Context(), repo.ID)
|
||||
watches, err := repo_model.GetRepoWatches(t.Context(), repo.ID)
|
||||
assert.NoError(t, err)
|
||||
// One watchers are inactive, thus minus 1
|
||||
assert.Len(t, watches, repo.NumWatches-1)
|
||||
@ -40,7 +40,7 @@ func TestGetWatchers(t *testing.T) {
|
||||
assert.Equal(t, repo.ID, watch.RepoID)
|
||||
}
|
||||
|
||||
watches, err = repo_model.GetWatchers(t.Context(), unittest.NonexistentID)
|
||||
watches, err = repo_model.GetRepoWatches(t.Context(), unittest.NonexistentID)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, watches)
|
||||
}
|
||||
@ -125,16 +125,41 @@ func TestClearRepoWatches(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
const repoID int64 = 1
|
||||
watchers, err := repo_model.GetRepoWatchersIDs(t.Context(), repoID)
|
||||
watchers, err := repo_model.GetRepoWatchers(t.Context(), repoID, db.ListOptions{Page: 1})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, watchers)
|
||||
|
||||
assert.NoError(t, repo_model.ClearRepoWatches(t.Context(), repoID))
|
||||
|
||||
watchers, err = repo_model.GetRepoWatchersIDs(t.Context(), repoID)
|
||||
watchers, err = repo_model.GetRepoWatchers(t.Context(), repoID, db.ListOptions{Page: 1})
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, watchers)
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
|
||||
assert.Zero(t, repo.NumWatches)
|
||||
}
|
||||
|
||||
func TestWatchOptions(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 5})
|
||||
user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
|
||||
assert.NoError(t, repo_model.WatchRepo(t.Context(), user5, repo, true))
|
||||
watch, err := repo_model.GetWatch(t.Context(), user5.ID, repo.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, watch.PullRequests)
|
||||
assert.True(t, watch.Issues)
|
||||
assert.True(t, watch.Releases)
|
||||
|
||||
assert.NoError(t, repo_model.WatchRepoOptions(t.Context(), user5, repo, repo_model.WatchOptions{
|
||||
PullRequests: true,
|
||||
Issues: false,
|
||||
Releases: true,
|
||||
}))
|
||||
watch, err = repo_model.GetWatch(t.Context(), user5.ID, repo.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, watch.PullRequests)
|
||||
assert.False(t, watch.Issues)
|
||||
assert.True(t, watch.Releases)
|
||||
}
|
||||
|
||||
@ -1154,6 +1154,10 @@
|
||||
"repo.star_guest_user": "Sign in to star this repository.",
|
||||
"repo.unwatch": "Unwatch",
|
||||
"repo.watch": "Watch",
|
||||
"repo.watch.options.desc": "Select notifications you want to receive in addition to mentions and subscriptions.",
|
||||
"repo.watch.options.pull_requests": "Pull Requests",
|
||||
"repo.watch.options.issues": "Issues",
|
||||
"repo.watch.options.releases": "Releases",
|
||||
"repo.unstar": "Unstar",
|
||||
"repo.star": "Star",
|
||||
"repo.fork": "Fork",
|
||||
|
||||
@ -20,11 +20,33 @@ func ActionWatch(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["IsWatchingRepo"] = repo_model.IsWatching(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
|
||||
watch, err := repo_model.GetWatch(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetWatch", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Watch"] = watch
|
||||
ctx.Data["IsWatchingRepo"] = repo_model.IsWatchMode(watch.Mode)
|
||||
|
||||
ctx.Data["Repository"], err = repo_model.GetRepositoryByName(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.Name)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRepositoryByName", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplWatchUnwatch)
|
||||
}
|
||||
|
||||
func ActionWatchOptions(ctx *context.Context) {
|
||||
err := repo_model.WatchRepoOptions(ctx, ctx.Doer, ctx.Repo.Repository, repo_model.WatchOptions{
|
||||
PullRequests: ctx.FormBool("pull_requests"),
|
||||
Issues: ctx.FormBool("issues"),
|
||||
Releases: ctx.FormBool("releases"),
|
||||
})
|
||||
if err != nil {
|
||||
handleActionError(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
@ -373,6 +373,13 @@ func NotificationWatching(ctx *context.Context) {
|
||||
ctx.Data["Total"] = count
|
||||
ctx.Data["Repos"] = repos
|
||||
|
||||
watches, err := repo_model.GetWatches(ctx, repos)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetWatches", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Watches"] = watches
|
||||
|
||||
// redirect to last page if request page is more than total pages
|
||||
pager := context.NewPagination(count, setting.UI.User.RepoPagingNum, page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
|
||||
@ -1728,6 +1728,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Get("/search", reqUnitCodeReader, repo.Search)
|
||||
m.Post("/action/{action:star|unstar}", reqSignIn, starsEnabled, repo.ActionStar)
|
||||
m.Post("/action/{action:watch|unwatch}", reqSignIn, repo.ActionWatch)
|
||||
m.Post("/action/watch/options", reqSignIn, repo.ActionWatchOptions)
|
||||
m.Post("/action/{action:accept_transfer|reject_transfer}", reqSignIn, repo.ActionTransfer)
|
||||
}, optSignIn, context.RepoAssignment)
|
||||
|
||||
|
||||
@ -651,7 +651,13 @@ func repoAssignmentPrepareTemplateData(ctx *Context, data *repoAssignmentPrepare
|
||||
}
|
||||
|
||||
if ctx.IsSigned {
|
||||
ctx.Data["IsWatchingRepo"] = repo_model.IsWatching(ctx, ctx.Doer.ID, repo.ID)
|
||||
watch, err := repo_model.GetWatch(ctx, ctx.Doer.ID, repo.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetWatch", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Watch"] = watch
|
||||
ctx.Data["IsWatchingRepo"] = repo_model.IsWatchMode(watch.Mode)
|
||||
ctx.Data["IsStaringRepo"] = repo_model.IsStaring(ctx, ctx.Doer.ID, repo.ID)
|
||||
}
|
||||
|
||||
|
||||
@ -115,16 +115,16 @@ func NotifyWatchers(ctx context.Context, acts ...*activities_model.Action) error
|
||||
|
||||
actUserID := acts[0].ActUserID
|
||||
|
||||
// Add feeds for user self and all watchers.
|
||||
watchers, err := repo_model.GetWatchers(ctx, repoID)
|
||||
// Add feeds for user self and all watches.
|
||||
watches, err := repo_model.GetRepoWatches(ctx, repoID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get watchers: %w", err)
|
||||
}
|
||||
|
||||
permCode := make([]bool, len(watchers))
|
||||
permIssue := make([]bool, len(watchers))
|
||||
permPR := make([]bool, len(watchers))
|
||||
for i, watcher := range watchers {
|
||||
permCode := make([]bool, len(watches))
|
||||
permIssue := make([]bool, len(watches))
|
||||
permPR := make([]bool, len(watches))
|
||||
for i, watcher := range watches {
|
||||
user, err := user_model.GetUserByID(ctx, watcher.UserID)
|
||||
if err != nil {
|
||||
permCode[i] = false
|
||||
@ -153,7 +153,7 @@ func NotifyWatchers(ctx context.Context, acts ...*activities_model.Action) error
|
||||
}
|
||||
|
||||
act.Repo = repo
|
||||
if err := notifyWatchers(ctx, act, watchers, permCode, permIssue, permPR); err != nil {
|
||||
if err := notifyWatchers(ctx, act, watches, permCode, permIssue, permPR); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,7 +66,12 @@ func mailIssueCommentToParticipants(ctx context.Context, comment *mailComment, m
|
||||
// =========== Repo watchers ===========
|
||||
// Make repo watchers last, since it's likely the list with the most users
|
||||
if !(comment.Issue.IsPull && comment.Issue.PullRequest.IsWorkInProgress(ctx) && comment.ActionType != activities_model.ActionCreatePullRequest) {
|
||||
ids, err = repo_model.GetRepoWatchersIDs(ctx, comment.Issue.RepoID)
|
||||
watchType := repo_model.WatchIssues
|
||||
if comment.Issue.IsPull {
|
||||
watchType = repo_model.WatchPullRequests
|
||||
}
|
||||
|
||||
ids, err = repo_model.GetRepoWatchersIDs(ctx, comment.Issue.RepoID, watchType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetRepoWatchersIDs(%d): %w", comment.Issue.RepoID, err)
|
||||
}
|
||||
|
||||
86
services/mailer/mail_issue_test.go
Normal file
86
services/mailer/mail_issue_test.go
Normal file
@ -0,0 +1,86 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package mailer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/activities"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
sender_service "code.gitea.io/gitea/services/mailer/sender"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMailNewIssueAndPullRequest(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
defer test.MockVariableValue(&setting.MailService)()
|
||||
defer test.MockVariableValue(&setting.Domain)()
|
||||
defer test.MockVariableValue(&setting.AppName)()
|
||||
defer test.MockVariableValue(&setting.AppURL)()
|
||||
|
||||
setting.MailService = &setting.Mailer{
|
||||
From: "Gitea",
|
||||
FromEmail: "noreply@example.com",
|
||||
}
|
||||
setting.Domain = "example.com"
|
||||
setting.AppName = "Gitea"
|
||||
setting.AppURL = "https://example.com/"
|
||||
defer mockMailTemplates(string("repo/issue/new"), "{{.Subject}}", "<p>Issue</p>")()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
watcher := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
didSend := false
|
||||
origSend := SendAsync
|
||||
SendAsync = func(msgs ...*sender_service.Message) {
|
||||
for _, msg := range msgs {
|
||||
if msg.To == watcher.Email {
|
||||
didSend = true
|
||||
}
|
||||
}
|
||||
}
|
||||
defer func() {
|
||||
SendAsync = origSend
|
||||
}()
|
||||
|
||||
iss := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
|
||||
|
||||
assert.NoError(t, issues_model.RemoveIssueWatchersByRepoID(t.Context(), watcher.ID, repo.ID))
|
||||
assert.NoError(t, repo_model.WatchRepo(t.Context(), watcher, repo, true))
|
||||
assert.NoError(t, repo_model.WatchRepoOptions(t.Context(), watcher, repo, repo_model.WatchOptions{
|
||||
PullRequests: false,
|
||||
Issues: false,
|
||||
Releases: true,
|
||||
}))
|
||||
|
||||
var mentions []*user_model.User
|
||||
assert.NoError(t, MailParticipants(t.Context(), iss, doer, activities.ActionCreateIssue, mentions))
|
||||
assert.False(t, didSend)
|
||||
|
||||
didSend = false
|
||||
assert.NoError(t, MailParticipants(t.Context(), pr, doer, activities.ActionCreatePullRequest, mentions))
|
||||
assert.False(t, didSend)
|
||||
|
||||
assert.NoError(t, repo_model.WatchRepoOptions(t.Context(), watcher, repo, repo_model.WatchOptions{
|
||||
PullRequests: true,
|
||||
Issues: true,
|
||||
Releases: true,
|
||||
}))
|
||||
didSend = false
|
||||
assert.NoError(t, MailParticipants(t.Context(), pr, doer, activities.ActionCreatePullRequest, mentions))
|
||||
assert.True(t, didSend)
|
||||
|
||||
didSend = false
|
||||
assert.NoError(t, MailParticipants(t.Context(), iss, doer, activities.ActionCreateIssue, mentions))
|
||||
assert.True(t, didSend)
|
||||
}
|
||||
@ -35,7 +35,7 @@ func MailNewRelease(ctx context.Context, rel *repo_model.Release) {
|
||||
return
|
||||
}
|
||||
|
||||
watcherIDList, err := repo_model.GetRepoWatchersIDs(ctx, rel.RepoID)
|
||||
watcherIDList, err := repo_model.GetRepoWatchersIDs(ctx, rel.RepoID, repo_model.WatchReleases)
|
||||
if err != nil {
|
||||
log.Error("GetRepoWatchersIDs(%d): %v", rel.RepoID, err)
|
||||
return
|
||||
|
||||
@ -62,3 +62,55 @@ func TestMailNewReleaseFiltersUnauthorizedWatchers(t *testing.T) {
|
||||
assert.Equal(t, admin.EmailTo(), sent[0].To)
|
||||
assert.NotEqual(t, unauthorized.EmailTo(), sent[0].To)
|
||||
}
|
||||
|
||||
func TestMailNewReleaseWithWatchOptions(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
defer test.MockVariableValue(&setting.MailService)()
|
||||
defer test.MockVariableValue(&setting.Domain)()
|
||||
defer test.MockVariableValue(&setting.AppName)()
|
||||
defer test.MockVariableValue(&setting.AppURL)()
|
||||
|
||||
setting.MailService = &setting.Mailer{
|
||||
From: "Gitea",
|
||||
FromEmail: "noreply@example.com",
|
||||
}
|
||||
setting.Domain = "example.com"
|
||||
setting.AppName = "Gitea"
|
||||
setting.AppURL = "https://example.com/"
|
||||
defer mockMailTemplates(string(tplNewReleaseMail), "{{.Subject}}", "<p>{{.Release.TagName}}</p>")()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
|
||||
assert.NoError(t, repo_model.WatchRepo(t.Context(), user, repo, true))
|
||||
assert.NoError(t, repo_model.WatchRepoOptions(t.Context(), user, repo, repo_model.WatchOptions{
|
||||
PullRequests: true,
|
||||
Issues: true,
|
||||
Releases: false,
|
||||
}))
|
||||
|
||||
didSend := false
|
||||
origSend := SendAsync
|
||||
SendAsync = func(msgs ...*sender_service.Message) {
|
||||
didSend = true
|
||||
}
|
||||
defer func() {
|
||||
SendAsync = origSend
|
||||
}()
|
||||
|
||||
rel := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: 11})
|
||||
rel.Publisher = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: rel.PublisherID})
|
||||
MailNewRelease(t.Context(), rel)
|
||||
assert.False(t, didSend)
|
||||
|
||||
assert.NoError(t, repo_model.WatchRepoOptions(t.Context(), user, repo, repo_model.WatchOptions{
|
||||
PullRequests: true,
|
||||
Issues: true,
|
||||
Releases: true,
|
||||
}))
|
||||
|
||||
didSend = false
|
||||
MailNewRelease(t.Context(), rel)
|
||||
assert.True(t, didSend)
|
||||
}
|
||||
|
||||
@ -76,13 +76,13 @@ func TestMakeRepoPrivateClearsWatches(t *testing.T) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
assert.False(t, repo.IsPrivate)
|
||||
|
||||
watchers, err := repo_model.GetRepoWatchersIDs(t.Context(), repo.ID)
|
||||
watchers, err := repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, watchers)
|
||||
|
||||
assert.NoError(t, MakeRepoPrivate(t.Context(), repo, true))
|
||||
|
||||
watchers, err = repo_model.GetRepoWatchersIDs(t.Context(), repo.ID)
|
||||
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, watchers)
|
||||
|
||||
|
||||
@ -139,22 +139,18 @@ func (ns *notificationService) NewPullRequest(ctx context.Context, pr *issues_mo
|
||||
return
|
||||
}
|
||||
toNotify := make(container.Set[int64], 32)
|
||||
repoWatchers, err := repo_model.GetRepoWatchersIDs(ctx, pr.Issue.RepoID)
|
||||
repoWatchers, err := repo_model.GetRepoWatchersIDs(ctx, pr.Issue.RepoID, repo_model.WatchPullRequests)
|
||||
if err != nil {
|
||||
log.Error("GetRepoWatchersIDs: %v", err)
|
||||
return
|
||||
}
|
||||
for _, id := range repoWatchers {
|
||||
toNotify.Add(id)
|
||||
}
|
||||
toNotify.AddMultiple(repoWatchers...)
|
||||
issueParticipants, err := issues_model.GetParticipantsIDsByIssueID(ctx, pr.IssueID)
|
||||
if err != nil {
|
||||
log.Error("GetParticipantsIDsByIssueID: %v", err)
|
||||
return
|
||||
}
|
||||
for _, id := range issueParticipants {
|
||||
toNotify.Add(id)
|
||||
}
|
||||
toNotify.AddMultiple(issueParticipants...)
|
||||
delete(toNotify, pr.Issue.PosterID)
|
||||
for _, mention := range mentions {
|
||||
toNotify.Add(mention.ID)
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
<div class="ui labeled button" {{if not $.IsSigned}}data-tooltip-content="{{ctx.Locale.Tr "repo.watch_guest_user"}}"{{end}}>
|
||||
<div class="ui buttons" {{if not $.IsSigned}}data-tooltip-content="{{ctx.Locale.Tr "repo.watch_guest_user"}}"{{end}}>
|
||||
{{$buttonText := ctx.Locale.Tr "repo.watch"}}
|
||||
{{if $.IsWatchingRepo}}{{$buttonText = ctx.Locale.Tr "repo.unwatch"}}{{end}}
|
||||
<a role="button" class="ui compact small basic button" aria-label="{{$buttonText}}"
|
||||
{{if $.IsSigned}}
|
||||
data-fetch-method="post"
|
||||
data-fetch-url="{{$.RepoLink}}/action/{{if $.IsWatchingRepo}}unwatch{{else}}watch{{end}}"
|
||||
data-fetch-sync="$closest(.ui.labeled.button)"
|
||||
data-fetch-sync="$closest(.ui.buttons)"
|
||||
{{else}}
|
||||
href="{{AppSubUrl}}/user/login"
|
||||
{{end}}
|
||||
@ -13,7 +13,19 @@
|
||||
{{svg "octicon-eye"}}
|
||||
<span class="not-mobile" aria-hidden="true">{{$buttonText}}</span>
|
||||
</a>
|
||||
<a class="ui basic label" href="{{$.RepoLink}}/watchers">
|
||||
<a class="ui basic compact small button watch-label" href="{{$.RepoLink}}/watchers">
|
||||
{{CountFmt .Repository.NumWatches}}
|
||||
</a>
|
||||
{{if $.IsWatchingRepo}}
|
||||
<a class="ui basic compact small button watch-icon"
|
||||
data-global-init="initWatchOptions"
|
||||
data-repo-link="{{$.RepoLink}}"
|
||||
data-watch-pull-requests="{{$.Watch.PullRequests}}"
|
||||
data-watch-issues="{{$.Watch.Issues}}"
|
||||
data-watch-releases="{{$.Watch.Releases}}"
|
||||
>
|
||||
{{svg "octicon-gear"}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{template "repo/watch_options_modal" $}}
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
</h2>
|
||||
{{template "base/alert" .}}
|
||||
|
||||
<form class="ui form" action="{{.Link}}" method="post" data-global-init="initReleaseEditForm"
|
||||
<form class="ui form" action="{{.Link}}" id="new-release" method="post" data-global-init="initReleaseEditForm"
|
||||
data-existing-tags="{{JsonUtils.EncodeToString .Tags}}"
|
||||
data-tag-helper="{{ctx.Locale.Tr "repo.release.tag_helper"}}"
|
||||
data-tag-helper-new="{{ctx.Locale.Tr "repo.release.tag_helper_new"}}"
|
||||
|
||||
33
templates/repo/watch_options_modal.tmpl
Normal file
33
templates/repo/watch_options_modal.tmpl
Normal file
@ -0,0 +1,33 @@
|
||||
<div class="ui small modal" id="repo-watch-options-modal">
|
||||
<div class="header">{{ctx.Locale.Tr "notifications"}}</div>
|
||||
<div class="content">
|
||||
<p>{{ctx.Locale.Tr "repo.watch.options.desc"}}</p>
|
||||
<form class="ui form" method="post">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="pull_requests" type="checkbox"><label>{{ctx.Locale.Tr "repo.watch.options.pull_requests"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="issues" type="checkbox"><label>{{ctx.Locale.Tr "repo.watch.options.issues"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="releases" type="checkbox"><label>{{ctx.Locale.Tr "repo.watch.options.releases"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="ui small basic cancel button">
|
||||
{{svg "octicon-x"}}
|
||||
{{ctx.Locale.Tr "cancel"}}
|
||||
</button>
|
||||
<button class="ui primary small approve button">
|
||||
{{svg "fontawesome-save"}}
|
||||
{{ctx.Locale.Tr "save"}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,3 +1,6 @@
|
||||
{{/* Template Attributes:
|
||||
* showWatchOptions: whether to show the watch options button
|
||||
*/}}
|
||||
<div class="flex-divided-list items-with-main">
|
||||
{{range .Repos}}
|
||||
<div class="item">
|
||||
@ -37,7 +40,7 @@
|
||||
<div class="item-trailing muted-links">
|
||||
{{if .PrimaryLanguage}}
|
||||
<a class="flex-text-inline" href="?q={{$.Keyword}}&sort={{$.SortType}}&language={{.PrimaryLanguage.Language}}{{if $.TabName}}&tab={{$.TabName}}{{end}}">
|
||||
<i class="color-icon tw-mr-2" style="background-color: {{.PrimaryLanguage.Color}}"></i>
|
||||
<i class="color-icon tw-mr-1" style="background-color: {{.PrimaryLanguage.Color}}"></i>
|
||||
{{.PrimaryLanguage.Language}}
|
||||
</a>
|
||||
{{end}}
|
||||
@ -51,6 +54,18 @@
|
||||
<span class="tw-contents" aria-label="{{ctx.Locale.Tr "repo.forks"}}">{{svg "octicon-git-branch" 16}}</span>
|
||||
<span {{if ge .NumForks 1000}}data-tooltip-content="{{.NumForks}}"{{end}}>{{CountFmt .NumForks}}</span>
|
||||
</a>
|
||||
{{if $.showWatchOptions}}
|
||||
{{$watch := index $.Watches .ID}}
|
||||
<a class="flex-text-inline tw-ml-1"
|
||||
data-global-init="initWatchOptions"
|
||||
data-repo-link="{{.Link}}"
|
||||
data-watch-pull-requests="{{$watch.PullRequests}}"
|
||||
data-watch-issues="{{$watch.Issues}}"
|
||||
data-watch-releases="{{$watch.Releases}}"
|
||||
>
|
||||
{{svg "octicon-gear"}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{$description := .DescriptionHTML ctx}}
|
||||
@ -72,4 +87,5 @@
|
||||
{{ctx.Locale.Tr "search.no_results"}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{template "repo/watch_options_modal"}}
|
||||
</div>
|
||||
|
||||
@ -68,7 +68,7 @@
|
||||
{{end}}
|
||||
{{else}}
|
||||
{{template "shared/repo/search" .}}
|
||||
{{template "shared/repo/list" .}}
|
||||
{{template "shared/repo/list" dict "." . "showWatchOptions" true}}
|
||||
{{template "base/paginate" .}}
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
@ -127,7 +127,7 @@ func testNewIssue(t *testing.T, session *TestSession, user, repo, title, content
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
link, exists := htmlDoc.doc.Find("form.ui.form").Attr("action")
|
||||
link, exists := htmlDoc.doc.Find("form#new-issue").Attr("action")
|
||||
assert.True(t, exists, "The template has changed")
|
||||
req = NewRequestWithValues(t, "POST", link, map[string]string{
|
||||
"title": title,
|
||||
|
||||
@ -55,7 +55,7 @@ func testPullCreate(t *testing.T, session *TestSession, user, repo string, toSel
|
||||
|
||||
// Submit the form for creating the pull
|
||||
htmlDoc = NewHTMLParser(t, resp.Body)
|
||||
link, exists = htmlDoc.doc.Find("form.ui.form").Attr("action")
|
||||
link, exists = htmlDoc.doc.Find("form#new-issue").Attr("action")
|
||||
assert.True(t, exists, "The template has changed")
|
||||
req = NewRequestWithValues(t, "POST", link, map[string]string{
|
||||
"title": title,
|
||||
@ -98,7 +98,7 @@ func testPullCreateDirectly(t *testing.T, session *TestSession, opts createPullR
|
||||
|
||||
// Submit the form for creating the pull
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
link, exists := htmlDoc.doc.Find("form.ui.form").Attr("action")
|
||||
link, exists := htmlDoc.doc.Find("form#new-issue").Attr("action")
|
||||
assert.True(t, exists, "The template has changed")
|
||||
params := map[string]string{
|
||||
"title": opts.Title,
|
||||
@ -125,7 +125,7 @@ func testPullCreateFailure(t *testing.T, session *TestSession, baseRepoOwner, ba
|
||||
|
||||
// Submit the form for creating the pull
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
link, exists := htmlDoc.doc.Find("form.ui.form").Attr("action")
|
||||
link, exists := htmlDoc.doc.Find("form#new-issue").Attr("action")
|
||||
assert.True(t, exists, "The template has changed")
|
||||
req = NewRequestWithValues(t, "POST", link, map[string]string{
|
||||
"title": title,
|
||||
|
||||
@ -24,7 +24,7 @@ func createNewRelease(t *testing.T, session *TestSession, repoURL, tag, title st
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
|
||||
link, exists := htmlDoc.doc.Find("form.ui.form").Attr("action")
|
||||
link, exists := htmlDoc.doc.Find("form#new-release").Attr("action")
|
||||
assert.True(t, exists, "The template has changed")
|
||||
|
||||
postData := map[string]string{
|
||||
|
||||
@ -73,6 +73,7 @@
|
||||
@import "./repo/clone.css";
|
||||
@import "./repo/commit-sign.css";
|
||||
@import "./repo/packages.css";
|
||||
@import "./repo/watch.css";
|
||||
|
||||
@import "./editor/combomarkdowneditor.css";
|
||||
|
||||
|
||||
@ -400,11 +400,21 @@ It needs some tricks to tweak the left/right borders with active state */
|
||||
border-left: 1px solid var(--color-light-border);
|
||||
}
|
||||
|
||||
.ui.buttons .button:first-child:hover,
|
||||
.ui.buttons .button.tw-hidden:first-child + .button:hover {
|
||||
border-left-color: var(--color-secondary-dark-2);
|
||||
}
|
||||
|
||||
.ui.buttons .button:last-child,
|
||||
.ui.buttons .button:nth-last-child(2):has(+ .button.tw-hidden) {
|
||||
border-right: 1px solid var(--color-light-border);
|
||||
}
|
||||
|
||||
.ui.buttons .button:last-child:hover,
|
||||
.ui.buttons .button:nth-last-child(2):has(+ .button.tw-hidden):hover {
|
||||
border-right-color: var(--color-secondary-dark-2);
|
||||
}
|
||||
|
||||
.ui.buttons .button.active {
|
||||
border-left: 1px solid var(--color-light-border);
|
||||
border-right: 1px solid var(--color-light-border);
|
||||
|
||||
9
web_src/css/repo/watch.css
Normal file
9
web_src/css/repo/watch.css
Normal file
@ -0,0 +1,9 @@
|
||||
.ui.buttons .ui.watch-label {
|
||||
font-size: 1rem !important;
|
||||
font-weight: var(--font-weight-medium);
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.ui.buttons .ui.watch-icon {
|
||||
padding: 0.42em;
|
||||
}
|
||||
35
web_src/js/features/repo-watch-options.ts
Normal file
35
web_src/js/features/repo-watch-options.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
import {registerGlobalInitFunc} from '../modules/observer.ts';
|
||||
import {submitFormFetchAction} from './common-fetch-action.ts';
|
||||
|
||||
export function initRepoWatchOptions() {
|
||||
const elModal = document.querySelector<HTMLElement>('#repo-watch-options-modal');
|
||||
if (!elModal) return;
|
||||
|
||||
const elPullRequests = elModal.querySelector<HTMLInputElement>('[name="pull_requests"]')!;
|
||||
const elIssues = elModal.querySelector<HTMLInputElement>('[name="issues"]')!;
|
||||
const elReleases = elModal.querySelector<HTMLInputElement>('[name="releases"]')!;
|
||||
|
||||
const showModal = (btn: HTMLElement) => {
|
||||
const form = elModal.querySelector<HTMLFormElement>('form')!;
|
||||
elPullRequests.checked = btn.getAttribute('data-watch-pull-requests') === 'true';
|
||||
elIssues.checked = btn.getAttribute('data-watch-issues') === 'true';
|
||||
elReleases.checked = btn.getAttribute('data-watch-releases') === 'true';
|
||||
form.action = `${btn.getAttribute('data-repo-link')!}/action/watch/options`;
|
||||
|
||||
fomanticQuery(elModal).modal({
|
||||
autofocus: false,
|
||||
onApprove() {
|
||||
submitFormFetchAction(form);
|
||||
return false;
|
||||
},
|
||||
}).modal('show');
|
||||
};
|
||||
|
||||
registerGlobalInitFunc('initWatchOptions', (el: HTMLElement) => {
|
||||
el.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
showModal(el);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -66,6 +66,7 @@ import {initActionsPermissionsForm} from './features/common-actions-permissions.
|
||||
import {initRefIssueContextPopup} from './features/ref-issue.ts';
|
||||
import {initGlobalShortcut} from './modules/shortcut.ts';
|
||||
import {initDevtest} from './modules/devtest.ts';
|
||||
import {initRepoWatchOptions} from './features/repo-watch-options.ts';
|
||||
|
||||
const initStartTime = performance.now();
|
||||
const initPerformanceTracer = callInitFunctions([
|
||||
@ -143,6 +144,7 @@ const initPerformanceTracer = callInitFunctions([
|
||||
initRepoContributors,
|
||||
initRepoCodeFrequency,
|
||||
initRepoRecentCommits,
|
||||
initRepoWatchOptions,
|
||||
|
||||
initCommitStatuses,
|
||||
initCaptcha,
|
||||
|
||||
@ -34,7 +34,7 @@ export function registerGlobalSelectorFunc<T extends Element>(selector: string,
|
||||
}
|
||||
}
|
||||
|
||||
// It handles the global init functions for all `<div data-global-int="initSomeElem"></div>` elements.
|
||||
// It handles the global init functions for all `<div data-global-init="initSomeElem"></div>` elements.
|
||||
export function registerGlobalInitFunc<T extends HTMLElement>(name: string, handler: GlobalInitFunc<T>) {
|
||||
globalInitFuncs[name] = handler as GlobalInitFunc<Element>;
|
||||
// The "global init" functions are managed internally and called by callGlobalInitFunc
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user