mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-10 05:24:18 +09:00
enhance: refine repo watching (#38835)
Follow-up to https://github.com/go-gitea/gitea/pull/37571. "Participating and mentions" deleted the watch row, so choosing it dropped you out of the watcher count. It is a watch like the others, so it now keeps a row and simply subscribes to no events. The dashboard feed ignored the per-event options, so a "Custom: issues" watcher still got pull request activity there. It now gates on the same options as mail and notifications. That also closes a gap where pull request reviews bypassed the permission check. Also, address https://github.com/go-gitea/gitea/pull/37571#discussion_r3740487363 and reword a UI text for clarity. --------- Signed-off-by: silverwind <me@silverwind.io> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -54,7 +54,7 @@ func TestCreateOrUpdateIssueNotificationsIgnored(t *testing.T) {
|
|||||||
// user 4 watches repo 1 and would be notified about issue 1
|
// user 4 watches repo 1 and would be notified about issue 1
|
||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||||
assert.NoError(t, repo_model.IgnoreRepo(t.Context(), user, repo))
|
assert.NoError(t, repo_model.WatchIgnoreRepo(t.Context(), user, repo))
|
||||||
|
|
||||||
notified, err := activities_model.CreateOrUpdateIssueNotifications(t.Context(), 1, 0, 2, 0)
|
notified, err := activities_model.CreateOrUpdateIssueNotifications(t.Context(), 1, 0, 2, 0)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|||||||
+37
-3
@@ -74,6 +74,29 @@ func (w *Watch) IsIgnoring() bool {
|
|||||||
return w.Mode == WatchModeDont
|
return w.Mode == WatchModeDont
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsWatching reports whether the watch counts the user as a watcher of the repository
|
||||||
|
func (w *Watch) IsWatching() bool {
|
||||||
|
return IsWatchMode(w.Mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsWatchingAll reports whether every event is enabled, which is the "all activity" mode
|
||||||
|
func (w *Watch) IsWatchingAll() bool {
|
||||||
|
return w.PullRequests && w.Issues && w.Releases
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectedMode returns the mode the user picked in the watch menu
|
||||||
|
func (w *Watch) SelectedMode() string {
|
||||||
|
switch {
|
||||||
|
case w.IsIgnoring():
|
||||||
|
return "ignore"
|
||||||
|
case !IsWatchMode(w.Mode), !(w.PullRequests || w.Issues || w.Releases):
|
||||||
|
return "participate" // also the default while there is no watch row
|
||||||
|
case w.IsWatchingAll():
|
||||||
|
return "all"
|
||||||
|
}
|
||||||
|
return "custom"
|
||||||
|
}
|
||||||
|
|
||||||
// IsWatchMode Decodes watchability of WatchMode
|
// IsWatchMode Decodes watchability of WatchMode
|
||||||
func IsWatchMode(mode WatchMode) bool {
|
func IsWatchMode(mode WatchMode) bool {
|
||||||
return mode != WatchModeNone && mode != WatchModeDont
|
return mode != WatchModeNone && mode != WatchModeDont
|
||||||
@@ -145,8 +168,8 @@ func WatchRepo(ctx context.Context, doer *user_model.User, repo *Repository, doW
|
|||||||
return watchRepoMode(ctx, watch, WatchModeNormal)
|
return watchRepoMode(ctx, watch, WatchModeNormal)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IgnoreRepo mutes the repository, so nothing about it reaches the user.
|
// WatchIgnoreRepo mutes the repository (unwatch), so nothing about it reaches the user.
|
||||||
func IgnoreRepo(ctx context.Context, doer *user_model.User, repo *Repository) error {
|
func WatchIgnoreRepo(ctx context.Context, doer *user_model.User, repo *Repository) error {
|
||||||
watch, err := GetWatch(ctx, doer.ID, repo.ID)
|
watch, err := GetWatch(ctx, doer.ID, repo.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -160,6 +183,16 @@ type WatchOptions struct {
|
|||||||
Releases bool
|
Releases bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WatchRepoWithOptions starts watching the repository and subscribes to the given events
|
||||||
|
func WatchRepoWithOptions(ctx context.Context, doer *user_model.User, repo *Repository, opts WatchOptions) error {
|
||||||
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
|
if err := WatchRepo(ctx, doer, repo, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return SetWatchOptions(ctx, doer.ID, repo.ID, opts)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// SetWatchOptions updates the per-event options of a watch, callers must run WatchRepo first
|
// SetWatchOptions updates the per-event options of a watch, callers must run WatchRepo first
|
||||||
func SetWatchOptions(ctx context.Context, userID, repoID int64, opts WatchOptions) error {
|
func SetWatchOptions(ctx context.Context, userID, repoID int64, opts WatchOptions) error {
|
||||||
_, err := db.GetEngine(ctx).Where("user_id=? AND repo_id=?", userID, repoID).
|
_, err := db.GetEngine(ctx).Where("user_id=? AND repo_id=?", userID, repoID).
|
||||||
@@ -187,11 +220,12 @@ func GetUserWatches(ctx context.Context, userID int64, repoIDs []int64) (map[int
|
|||||||
return watchesByRepo, nil
|
return watchesByRepo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWatchers returns all watchers of given repository.
|
// GetWatchers returns all watchers of given repository, skipping those subscribed to no event.
|
||||||
func GetWatchers(ctx context.Context, repoID int64) ([]*Watch, error) {
|
func GetWatchers(ctx context.Context, repoID int64) ([]*Watch, error) {
|
||||||
watches := make([]*Watch, 0, 10)
|
watches := make([]*Watch, 0, 10)
|
||||||
return watches, db.GetEngine(ctx).Where("`watch`.repo_id=?", repoID).
|
return watches, db.GetEngine(ctx).Where("`watch`.repo_id=?", repoID).
|
||||||
And("`watch`.mode<>?", WatchModeDont).
|
And("`watch`.mode<>?", WatchModeDont).
|
||||||
|
And(builder.Or(builder.Eq{"`watch`.pull_requests": true}, builder.Eq{"`watch`.issues": true}, builder.Eq{"`watch`.releases": true})).
|
||||||
And("`user`.is_active=?", true).
|
And("`user`.is_active=?", true).
|
||||||
And("`user`.prohibit_login=?", false).
|
And("`user`.prohibit_login=?", false).
|
||||||
Join("INNER", "`user`", "`user`.id = `watch`.user_id").
|
Join("INNER", "`user`", "`user`.id = `watch`.user_id").
|
||||||
|
|||||||
@@ -167,5 +167,14 @@ func TestWatchOptions(t *testing.T) {
|
|||||||
assert.NoError(t, repo_model.WatchRepo(t.Context(), user, repo, true))
|
assert.NoError(t, repo_model.WatchRepo(t.Context(), user, repo, true))
|
||||||
watch, err := repo_model.GetWatch(t.Context(), user.ID, repo.ID)
|
watch, err := repo_model.GetWatch(t.Context(), user.ID, repo.ID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.True(t, watch.PullRequests && watch.Issues && watch.Releases)
|
assert.True(t, watch.IsWatchingAll())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWatchSelectedMode(t *testing.T) {
|
||||||
|
// a user without a watch row gets the dummy record, whose flags are the column defaults
|
||||||
|
assert.Equal(t, "participate", (&repo_model.Watch{Mode: repo_model.WatchModeNone, PullRequests: true, Issues: true, Releases: true}).SelectedMode())
|
||||||
|
assert.Equal(t, "participate", (&repo_model.Watch{Mode: repo_model.WatchModeNormal}).SelectedMode())
|
||||||
|
assert.Equal(t, "ignore", (&repo_model.Watch{Mode: repo_model.WatchModeDont}).SelectedMode())
|
||||||
|
assert.Equal(t, "custom", (&repo_model.Watch{Mode: repo_model.WatchModeNormal, Issues: true}).SelectedMode())
|
||||||
|
assert.Equal(t, "all", (&repo_model.Watch{Mode: repo_model.WatchModeAuto, PullRequests: true, Issues: true, Releases: true}).SelectedMode())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1157,7 +1157,6 @@
|
|||||||
"repo.watch": "Watch",
|
"repo.watch": "Watch",
|
||||||
"repo.watching": "Watching",
|
"repo.watching": "Watching",
|
||||||
"repo.ignoring": "Ignoring",
|
"repo.ignoring": "Ignoring",
|
||||||
"repo.watch.options.required": "Select at least one event type.",
|
|
||||||
"repo.watch.options.issues": "Issues",
|
"repo.watch.options.issues": "Issues",
|
||||||
"repo.watch.options.pull_requests": "Pull Requests",
|
"repo.watch.options.pull_requests": "Pull Requests",
|
||||||
"repo.watch.options.releases": "Releases",
|
"repo.watch.options.releases": "Releases",
|
||||||
@@ -1168,7 +1167,7 @@
|
|||||||
"repo.watch.mode.ignore": "Ignore",
|
"repo.watch.mode.ignore": "Ignore",
|
||||||
"repo.watch.mode.ignore.desc": "Never receive notifications from this repository.",
|
"repo.watch.mode.ignore.desc": "Never receive notifications from this repository.",
|
||||||
"repo.watch.mode.custom": "Custom",
|
"repo.watch.mode.custom": "Custom",
|
||||||
"repo.watch.mode.custom.desc": "Choose the events you want to receive notifications for.",
|
"repo.watch.mode.custom.desc": "Select events you want to be notified of, in addition to participating and mentions.",
|
||||||
"repo.unstar": "Unstar",
|
"repo.unstar": "Unstar",
|
||||||
"repo.star": "Star",
|
"repo.star": "Star",
|
||||||
"repo.fork": "Fork",
|
"repo.fork": "Fork",
|
||||||
|
|||||||
@@ -17,29 +17,22 @@ func ActionWatch(ctx *context.Context) {
|
|||||||
action := ctx.PathParam("action")
|
action := ctx.PathParam("action")
|
||||||
var err error
|
var err error
|
||||||
if action == "ignore" {
|
if action == "ignore" {
|
||||||
err = repo_model.IgnoreRepo(ctx, ctx.Doer, ctx.Repo.Repository)
|
err = repo_model.WatchIgnoreRepo(ctx, ctx.Doer, ctx.Repo.Repository)
|
||||||
} else {
|
} else {
|
||||||
err = repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, action == "watch")
|
all := action == "watch" // "participate" is a watch that subscribes to no event on its own
|
||||||
|
err = repo_model.WatchRepoWithOptions(ctx, ctx.Doer, ctx.Repo.Repository, repo_model.WatchOptions{PullRequests: all, Issues: all, Releases: all})
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleActionError(ctx, err)
|
handleActionError(ctx, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if action == "watch" { // watching again always restores every event, so "all activity" can undo a custom selection
|
|
||||||
opts := repo_model.WatchOptions{PullRequests: true, Issues: true, Releases: true}
|
|
||||||
if err := repo_model.SetWatchOptions(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, opts); err != nil {
|
|
||||||
ctx.ServerError("SetWatchOptions", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch, err := repo_model.GetWatch(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
|
watch, err := repo_model.GetWatch(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetWatch", err)
|
ctx.ServerError("GetWatch", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Data["Watch"] = watch
|
ctx.Data["RepoWatch"] = 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)
|
ctx.Data["Repository"], err = repo_model.GetRepositoryByName(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -51,22 +44,14 @@ func ActionWatch(ctx *context.Context) {
|
|||||||
|
|
||||||
// ActionWatchOptions watches the repository with a custom selection of events
|
// ActionWatchOptions watches the repository with a custom selection of events
|
||||||
func ActionWatchOptions(ctx *context.Context) {
|
func ActionWatchOptions(ctx *context.Context) {
|
||||||
opts := repo_model.WatchOptions{
|
opts := repo_model.WatchOptions{ // clearing every event is allowed, it leaves the participating state
|
||||||
PullRequests: ctx.FormBool(string(repo_model.WatchPullRequests)),
|
PullRequests: ctx.FormBool(string(repo_model.WatchPullRequests)),
|
||||||
Issues: ctx.FormBool(string(repo_model.WatchIssues)),
|
Issues: ctx.FormBool(string(repo_model.WatchIssues)),
|
||||||
Releases: ctx.FormBool(string(repo_model.WatchReleases)),
|
Releases: ctx.FormBool(string(repo_model.WatchReleases)),
|
||||||
}
|
}
|
||||||
if !opts.PullRequests && !opts.Issues && !opts.Releases {
|
if err := repo_model.WatchRepoWithOptions(ctx, ctx.Doer, ctx.Repo.Repository, opts); err != nil {
|
||||||
ctx.JSONError(ctx.Tr("repo.watch.options.required"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true); err != nil {
|
|
||||||
handleActionError(ctx, err)
|
handleActionError(ctx, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := repo_model.SetWatchOptions(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, opts); err != nil {
|
|
||||||
ctx.ServerError("SetWatchOptions", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.JSONRedirect("")
|
ctx.JSONRedirect("")
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1739,7 +1739,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
|||||||
m.Get("/watchers", repo.Watchers)
|
m.Get("/watchers", repo.Watchers)
|
||||||
m.Get("/search", reqUnitCodeReader, repo.Search)
|
m.Get("/search", reqUnitCodeReader, repo.Search)
|
||||||
m.Post("/action/{action:star|unstar}", reqSignIn, starsEnabled, repo.ActionStar)
|
m.Post("/action/{action:star|unstar}", reqSignIn, starsEnabled, repo.ActionStar)
|
||||||
m.Post("/action/{action:watch|unwatch|ignore}", reqSignIn, repo.ActionWatch)
|
m.Post("/action/{action:watch|participate|ignore}", reqSignIn, repo.ActionWatch)
|
||||||
m.Post("/action/watch/options", reqSignIn, repo.ActionWatchOptions)
|
m.Post("/action/watch/options", reqSignIn, repo.ActionWatchOptions)
|
||||||
m.Post("/action/{action:accept_transfer|reject_transfer}", reqSignIn, repo.ActionTransfer)
|
m.Post("/action/{action:accept_transfer|reject_transfer}", reqSignIn, repo.ActionTransfer)
|
||||||
}, optSignIn, context.RepoAssignment)
|
}, optSignIn, context.RepoAssignment)
|
||||||
|
|||||||
@@ -644,8 +644,7 @@ func repoAssignmentPrepareTemplateData(ctx *Context, data *repoAssignmentPrepare
|
|||||||
ctx.ServerError("GetWatch", err)
|
ctx.ServerError("GetWatch", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Data["Watch"] = watch
|
ctx.Data["RepoWatch"] = watch
|
||||||
ctx.Data["IsWatchingRepo"] = repo_model.IsWatchMode(watch.Mode)
|
|
||||||
ctx.Data["IsStaringRepo"] = repo_model.IsStaring(ctx, ctx.Doer.ID, repo.ID)
|
ctx.Data["IsStaringRepo"] = repo_model.IsStaring(ctx, ctx.Doer.ID, repo.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+14
-12
@@ -69,20 +69,23 @@ func notifyWatchers(ctx context.Context, act *activities_model.Action, watchers
|
|||||||
act.UserID = watcher.UserID
|
act.UserID = watcher.UserID
|
||||||
act.Repo.Units = nil
|
act.Repo.Units = nil
|
||||||
|
|
||||||
|
var allowed bool
|
||||||
switch act.OpType {
|
switch act.OpType {
|
||||||
case activities_model.ActionCommitRepo, activities_model.ActionPushTag, activities_model.ActionDeleteTag, activities_model.ActionPublishRelease, activities_model.ActionDeleteBranch:
|
case activities_model.ActionCommitRepo, activities_model.ActionPushTag, activities_model.ActionDeleteTag, activities_model.ActionDeleteBranch:
|
||||||
if !permCode[i] {
|
allowed = permCode[i] && watcher.IsWatchingAll()
|
||||||
continue
|
case activities_model.ActionPublishRelease:
|
||||||
}
|
allowed = permCode[i] && watcher.Releases
|
||||||
case activities_model.ActionCreateIssue, activities_model.ActionCommentIssue, activities_model.ActionCloseIssue, activities_model.ActionReopenIssue:
|
case activities_model.ActionCreateIssue, activities_model.ActionCommentIssue, activities_model.ActionCloseIssue, activities_model.ActionReopenIssue:
|
||||||
if !permIssue[i] {
|
allowed = permIssue[i] && watcher.Issues
|
||||||
continue
|
case activities_model.ActionCreatePullRequest, activities_model.ActionCommentPull, activities_model.ActionMergePullRequest, activities_model.ActionClosePullRequest,
|
||||||
}
|
activities_model.ActionReopenPullRequest, activities_model.ActionAutoMergePullRequest, activities_model.ActionApprovePullRequest,
|
||||||
case activities_model.ActionCreatePullRequest, activities_model.ActionCommentPull, activities_model.ActionMergePullRequest, activities_model.ActionClosePullRequest, activities_model.ActionReopenPullRequest, activities_model.ActionAutoMergePullRequest:
|
activities_model.ActionRejectPullRequest, activities_model.ActionPullReviewDismissed, activities_model.ActionPullRequestReadyForReview:
|
||||||
if !permPR[i] {
|
allowed = permPR[i] && watcher.PullRequests
|
||||||
continue
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
|
allowed = watcher.IsWatchingAll() // repository events have no watch option of their own
|
||||||
|
}
|
||||||
|
if !allowed {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := db.Insert(ctx, act); err != nil {
|
if err := db.Insert(ctx, act); err != nil {
|
||||||
@@ -120,7 +123,6 @@ func NotifyWatchers(ctx context.Context, acts ...*activities_model.Action) error
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get watchers: %w", err)
|
return fmt.Errorf("get watchers: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
permCode := make([]bool, len(watchers))
|
permCode := make([]bool, len(watchers))
|
||||||
permIssue := make([]bool, len(watchers))
|
permIssue := make([]bool, len(watchers))
|
||||||
permPR := make([]bool, len(watchers))
|
permPR := make([]bool, len(watchers))
|
||||||
|
|||||||
@@ -200,3 +200,19 @@ func TestNotifyWatchers(t *testing.T) {
|
|||||||
OpType: action.OpType,
|
OpType: action.OpType,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNotifyWatchersRespectsWatchOptions(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
// user 1 watches repo 1 for issues only, user 4 keeps every event
|
||||||
|
assert.NoError(t, repo_model.SetWatchOptions(t.Context(), 1, 1, repo_model.WatchOptions{Issues: true}))
|
||||||
|
|
||||||
|
assert.NoError(t, NotifyWatchers(t.Context(),
|
||||||
|
&activities_model.Action{ActUserID: 8, RepoID: 1, OpType: activities_model.ActionCreateIssue},
|
||||||
|
&activities_model.Action{ActUserID: 8, RepoID: 1, OpType: activities_model.ActionApprovePullRequest},
|
||||||
|
))
|
||||||
|
|
||||||
|
unittest.AssertExistsAndLoadBean(t, &activities_model.Action{UserID: 1, RepoID: 1, OpType: activities_model.ActionCreateIssue})
|
||||||
|
unittest.AssertNotExistsBean(t, &activities_model.Action{UserID: 1, RepoID: 1, OpType: activities_model.ActionApprovePullRequest})
|
||||||
|
unittest.AssertExistsAndLoadBean(t, &activities_model.Action{UserID: 4, RepoID: 1, OpType: activities_model.ActionApprovePullRequest})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
{{$isIgnoring := and $.IsSigned $.Watch.IsIgnoring}}
|
{{$isIgnoring := and $.IsSigned $.RepoWatch.IsIgnoring}}
|
||||||
{{$buttonText := ctx.Locale.Tr (Iif $isIgnoring "repo.ignoring" (Iif $.IsWatchingRepo "repo.watching" "repo.watch"))}}
|
{{$buttonText := ctx.Locale.Tr (Iif $isIgnoring "repo.ignoring" (Iif $.RepoWatch.IsWatching "repo.watching" "repo.watch"))}}
|
||||||
{{$count := CountFmt .Repository.NumWatches}}
|
{{$count := CountFmt .Repository.NumWatches}}
|
||||||
<div id="repo-header-watch" class="flex-text-block">
|
<div id="repo-header-watch" class="flex-text-block">
|
||||||
{{if $.IsSigned}}
|
{{if $.IsSigned}}
|
||||||
{{$isAll := and $.IsWatchingRepo $.Watch.PullRequests $.Watch.Issues $.Watch.Releases}}
|
{{$mode := $.RepoWatch.SelectedMode}}
|
||||||
{{$isCustom := and $.IsWatchingRepo (not $isAll)}}
|
{{$isAll := eq $mode "all"}}
|
||||||
{{$isParticipating := not (or $.IsWatchingRepo $isIgnoring)}}
|
{{$isCustom := eq $mode "custom"}}
|
||||||
|
{{$isParticipating := eq $mode "participate"}}
|
||||||
<button class="ui dropdown custom compact small basic button" data-global-init="initRepoWatchMenu" aria-label="{{$buttonText}}">
|
<button class="ui dropdown custom compact small basic button" data-global-init="initRepoWatchMenu" aria-label="{{$buttonText}}">
|
||||||
{{svg (Iif $isIgnoring "octicon-eye-closed" "octicon-eye")}}
|
{{svg (Iif $isIgnoring "octicon-eye-closed" "octicon-eye")}}
|
||||||
<span class="not-mobile" aria-hidden="true">{{$buttonText}}</span>
|
<span class="not-mobile" aria-hidden="true">{{$buttonText}}</span>
|
||||||
@@ -13,34 +14,37 @@
|
|||||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||||
</button>
|
</button>
|
||||||
<div class="tippy-target">
|
<div class="tippy-target">
|
||||||
{{$participating := ctx.Locale.Tr "repo.watch.mode.participating"}}
|
{{$testParticipating := ctx.Locale.Tr "repo.watch.mode.participating"}}
|
||||||
<a class="item{{if $isParticipating}} active{{end}}" role="menuitem" aria-label="{{$participating}}"
|
<a class="item{{if $isParticipating}} active{{end}}" role="menuitem" aria-label="{{$testParticipating}}"
|
||||||
data-fetch-method="post" data-fetch-url="{{$.RepoLink}}/action/unwatch" data-fetch-sync="$body #repo-header-watch"
|
data-fetch-method="post" data-fetch-url="{{$.RepoLink}}/action/participate" data-fetch-sync="$body #repo-header-watch"
|
||||||
>
|
>
|
||||||
{{svg "octicon-check" 16 (Iif $isParticipating "" "tw-invisible")}}
|
{{svg "octicon-check" 16 (Iif $isParticipating "" "tw-invisible")}}
|
||||||
<div>{{$participating}}<div class="tw-text-12 tw-text-text-light-2">{{ctx.Locale.Tr "repo.watch.mode.participating.desc"}}</div></div>
|
<div>{{$testParticipating}}<div class="tw-text-12 tw-text-text-light-2">{{ctx.Locale.Tr "repo.watch.mode.participating.desc"}}</div></div>
|
||||||
</a>
|
</a>
|
||||||
{{$all := ctx.Locale.Tr "repo.watch.mode.all"}}
|
{{$textAll := ctx.Locale.Tr "repo.watch.mode.all"}}
|
||||||
<a class="item{{if $isAll}} active{{end}}" role="menuitem" aria-label="{{$all}}"
|
<a class="item{{if $isAll}} active{{end}}" role="menuitem" aria-label="{{$textAll}}"
|
||||||
data-fetch-method="post" data-fetch-url="{{$.RepoLink}}/action/watch" data-fetch-sync="$body #repo-header-watch"
|
data-fetch-method="post" data-fetch-url="{{$.RepoLink}}/action/watch" data-fetch-sync="$body #repo-header-watch"
|
||||||
>
|
>
|
||||||
{{svg "octicon-check" 16 (Iif $isAll "" "tw-invisible")}}
|
{{svg "octicon-check" 16 (Iif $isAll "" "tw-invisible")}}
|
||||||
<div>{{$all}}<div class="tw-text-12 tw-text-text-light-2">{{ctx.Locale.Tr "repo.watch.mode.all.desc"}}</div></div>
|
<div>{{$textAll}}<div class="tw-text-12 tw-text-text-light-2">{{ctx.Locale.Tr "repo.watch.mode.all.desc"}}</div></div>
|
||||||
</a>
|
</a>
|
||||||
{{$ignore := ctx.Locale.Tr "repo.watch.mode.ignore"}}
|
{{$textIgnore := ctx.Locale.Tr "repo.watch.mode.ignore"}}
|
||||||
<a class="item{{if $.Watch.IsIgnoring}} active{{end}}" role="menuitem" aria-label="{{$ignore}}"
|
<a class="item{{if $isIgnoring}} active{{end}}" role="menuitem" aria-label="{{$textIgnore}}"
|
||||||
data-fetch-method="post" data-fetch-url="{{$.RepoLink}}/action/ignore" data-fetch-sync="$body #repo-header-watch"
|
data-fetch-method="post" data-fetch-url="{{$.RepoLink}}/action/ignore" data-fetch-sync="$body #repo-header-watch"
|
||||||
>
|
>
|
||||||
{{svg "octicon-check" 16 (Iif $.Watch.IsIgnoring "" "tw-invisible")}}
|
{{svg "octicon-check" 16 (Iif $isIgnoring "" "tw-invisible")}}
|
||||||
<div>{{$ignore}}<div class="tw-text-12 tw-text-text-light-2">{{ctx.Locale.Tr "repo.watch.mode.ignore.desc"}}</div></div>
|
<div>{{$textIgnore}}<div class="tw-text-12 tw-text-text-light-2">{{ctx.Locale.Tr "repo.watch.mode.ignore.desc"}}</div></div>
|
||||||
</a>
|
</a>
|
||||||
{{$custom := ctx.Locale.Tr "repo.watch.mode.custom"}}
|
{{$textCustom := ctx.Locale.Tr "repo.watch.mode.custom"}}
|
||||||
<a class="item{{if $isCustom}} active{{end}}" role="menuitem" aria-label="{{$custom}}"
|
<a class="item {{if $isCustom}}active{{end}} show-modal" role="menuitem" aria-label="{{$textCustom}}"
|
||||||
data-global-click="onRepoWatchOptionsClick" data-url="{{$.RepoLink}}/action/watch/options"
|
data-modal="#repo-watch-options-modal"
|
||||||
data-issues="{{$.Watch.Issues}}" data-pull-requests="{{$.Watch.PullRequests}}" data-releases="{{$.Watch.Releases}}"
|
data-modal-form.url="{{$.RepoLink}}/action/watch/options"
|
||||||
|
data-modal-issues="{{$.RepoWatch.Issues}}"
|
||||||
|
data-modal-pull_requests="{{$.RepoWatch.PullRequests}}"
|
||||||
|
data-modal-releases="{{$.RepoWatch.Releases}}"
|
||||||
>
|
>
|
||||||
{{svg "octicon-check" 16 (Iif $isCustom "" "tw-invisible")}}
|
{{svg "octicon-check" 16 (Iif $isCustom "" "tw-invisible")}}
|
||||||
<div>{{$custom}}<div class="tw-text-12 tw-text-text-light-2">{{ctx.Locale.Tr "repo.watch.mode.custom.desc"}}</div></div>
|
<div>{{$textCustom}}<div class="tw-text-12 tw-text-text-light-2">{{ctx.Locale.Tr "repo.watch.mode.custom.desc"}}</div></div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|||||||
@@ -2,13 +2,20 @@
|
|||||||
<div class="header">{{ctx.Locale.Tr "notifications"}}</div>
|
<div class="header">{{ctx.Locale.Tr "notifications"}}</div>
|
||||||
<form class="ui form form-fetch-action" method="post">
|
<form class="ui form form-fetch-action" method="post">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{{range $name := StringUtils.Split "issues,pull_requests,releases" ","}}
|
<div class="flex-relaxed-list tw-gap-4">
|
||||||
<div class="field">
|
<div class="ui checkbox">
|
||||||
<div class="ui checkbox">
|
<input name="issues" type="checkbox">
|
||||||
<input name="{{$name}}" type="checkbox"><label>{{ctx.Locale.Tr (printf "repo.watch.options.%s" $name)}}</label>
|
<label>{{ctx.Locale.Tr "repo.watch.options.issues"}}</label>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
<div class="ui checkbox">
|
||||||
|
<input name="pull_requests" type="checkbox">
|
||||||
|
<label>{{ctx.Locale.Tr "repo.watch.options.pull_requests"}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input name="releases" type="checkbox">
|
||||||
|
<label>{{ctx.Locale.Tr "repo.watch.options.releases"}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{template "base/modal_actions_confirm" dict "ModalButtonTypes" "confirm"}}
|
{{template "base/modal_actions_confirm" dict "ModalButtonTypes" "confirm"}}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -57,9 +57,12 @@
|
|||||||
</a>
|
</a>
|
||||||
{{$watch := and $.Watches (index $.Watches .ID)}}
|
{{$watch := and $.Watches (index $.Watches .ID)}}
|
||||||
{{if $watch}}
|
{{if $watch}}
|
||||||
<button class="btn flex-text-inline" data-global-click="onRepoWatchOptionsClick"
|
<button class="btn flex-text-inline show-modal"
|
||||||
data-url="{{.Link}}/action/watch/options"
|
data-modal="#repo-watch-options-modal"
|
||||||
data-issues="{{$watch.Issues}}" data-pull-requests="{{$watch.PullRequests}}" data-releases="{{$watch.Releases}}"
|
data-modal-form.url="{{.Link}}/action/watch/options"
|
||||||
|
data-modal-issues="{{$watch.Issues}}"
|
||||||
|
data-modal-pull_requests="{{$watch.PullRequests}}"
|
||||||
|
data-modal-releases="{{$watch.Releases}}"
|
||||||
data-tooltip-content="{{ctx.Locale.Tr "notifications"}}"
|
data-tooltip-content="{{ctx.Locale.Tr "notifications"}}"
|
||||||
>{{svg "octicon-gear" 16}}</button>
|
>{{svg "octicon-gear" 16}}</button>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -111,8 +111,10 @@ function onShowModalClick(el: HTMLElement, e: MouseEvent) {
|
|||||||
|
|
||||||
if (attrTargetProp) {
|
if (attrTargetProp) {
|
||||||
assignElementProperty(attrTarget, attrTargetProp, attrib.value);
|
assignElementProperty(attrTarget, attrTargetProp, attrib.value);
|
||||||
|
} else if (attrTarget.matches('input[type=checkbox], input[type=radio]')) {
|
||||||
|
(attrTarget as HTMLInputElement).checked = attrib.value === 'true';
|
||||||
} else if (attrTarget.matches('input, textarea')) {
|
} else if (attrTarget.matches('input, textarea')) {
|
||||||
(attrTarget as HTMLInputElement | HTMLTextAreaElement).value = attrib.value; // FIXME: add more supports like checkbox
|
(attrTarget as HTMLInputElement | HTMLTextAreaElement).value = attrib.value;
|
||||||
} else {
|
} else {
|
||||||
attrTarget.textContent = attrib.value; // FIXME: it should be more strict here, only handle div/span/p
|
attrTarget.textContent = attrib.value; // FIXME: it should be more strict here, only handle div/span/p
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
import {createTippy} from '../modules/tippy.ts';
|
import {createTippy} from '../modules/tippy.ts';
|
||||||
import {showFomanticModal} from '../modules/fomantic/modal.ts';
|
import {registerGlobalInitFunc} from '../modules/observer.ts';
|
||||||
import {registerGlobalEventFunc, registerGlobalInitFunc} from '../modules/observer.ts';
|
|
||||||
import type {Instance} from 'tippy.js';
|
|
||||||
|
|
||||||
let watchMenuTippy: Instance | null = null;
|
|
||||||
|
|
||||||
export function initRepoWatch() {
|
export function initRepoWatch() {
|
||||||
registerGlobalInitFunc('initRepoWatchMenu', (btn: HTMLElement) => {
|
registerGlobalInitFunc('initRepoWatchMenu', (btn: HTMLElement) => {
|
||||||
watchMenuTippy?.destroy(); // a watch action replaces the button, orphaning the old menu
|
|
||||||
const menu = btn.nextElementSibling!;
|
const menu = btn.nextElementSibling!;
|
||||||
watchMenuTippy = createTippy(btn, {
|
const watchMenuTippy = createTippy(btn, {
|
||||||
content: menu,
|
content: menu,
|
||||||
theme: 'menu',
|
theme: 'menu',
|
||||||
maxWidth: 350,
|
maxWidth: 350,
|
||||||
@@ -18,16 +13,6 @@ export function initRepoWatch() {
|
|||||||
interactive: true,
|
interactive: true,
|
||||||
hideOnClick: true,
|
hideOnClick: true,
|
||||||
});
|
});
|
||||||
menu.addEventListener('click', () => watchMenuTippy!.hide());
|
menu.addEventListener('click', () => watchMenuTippy.hide());
|
||||||
});
|
|
||||||
|
|
||||||
registerGlobalEventFunc('click', 'onRepoWatchOptionsClick', (btn: HTMLElement) => {
|
|
||||||
const elModal = document.querySelector<HTMLElement>('#repo-watch-options-modal')!;
|
|
||||||
const form = elModal.querySelector<HTMLFormElement>('form')!;
|
|
||||||
form.action = btn.getAttribute('data-url')!;
|
|
||||||
for (const el of form.querySelectorAll<HTMLInputElement>('input[type=checkbox]')) {
|
|
||||||
el.checked = btn.getAttribute(`data-${el.name.replaceAll('_', '-')}`) === 'true';
|
|
||||||
}
|
|
||||||
showFomanticModal(elModal);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user