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
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
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)
|
||||
assert.NoError(t, err)
|
||||
|
||||
+37
-3
@@ -74,6 +74,29 @@ func (w *Watch) IsIgnoring() bool {
|
||||
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
|
||||
func IsWatchMode(mode WatchMode) bool {
|
||||
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)
|
||||
}
|
||||
|
||||
// IgnoreRepo mutes the repository, so nothing about it reaches the user.
|
||||
func IgnoreRepo(ctx context.Context, doer *user_model.User, repo *Repository) error {
|
||||
// WatchIgnoreRepo mutes the repository (unwatch), so nothing about it reaches the user.
|
||||
func WatchIgnoreRepo(ctx context.Context, doer *user_model.User, repo *Repository) error {
|
||||
watch, err := GetWatch(ctx, doer.ID, repo.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -160,6 +183,16 @@ type WatchOptions struct {
|
||||
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
|
||||
func SetWatchOptions(ctx context.Context, userID, repoID int64, opts WatchOptions) error {
|
||||
_, 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
|
||||
}
|
||||
|
||||
// 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) {
|
||||
watches := make([]*Watch, 0, 10)
|
||||
return watches, db.GetEngine(ctx).Where("`watch`.repo_id=?", repoID).
|
||||
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`.prohibit_login=?", false).
|
||||
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))
|
||||
watch, err := repo_model.GetWatch(t.Context(), user.ID, repo.ID)
|
||||
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.watching": "Watching",
|
||||
"repo.ignoring": "Ignoring",
|
||||
"repo.watch.options.required": "Select at least one event type.",
|
||||
"repo.watch.options.issues": "Issues",
|
||||
"repo.watch.options.pull_requests": "Pull Requests",
|
||||
"repo.watch.options.releases": "Releases",
|
||||
@@ -1168,7 +1167,7 @@
|
||||
"repo.watch.mode.ignore": "Ignore",
|
||||
"repo.watch.mode.ignore.desc": "Never receive notifications from this repository.",
|
||||
"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.star": "Star",
|
||||
"repo.fork": "Fork",
|
||||
|
||||
@@ -17,29 +17,22 @@ func ActionWatch(ctx *context.Context) {
|
||||
action := ctx.PathParam("action")
|
||||
var err error
|
||||
if action == "ignore" {
|
||||
err = repo_model.IgnoreRepo(ctx, ctx.Doer, ctx.Repo.Repository)
|
||||
err = repo_model.WatchIgnoreRepo(ctx, ctx.Doer, ctx.Repo.Repository)
|
||||
} 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 {
|
||||
handleActionError(ctx, err)
|
||||
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)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetWatch", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Watch"] = watch
|
||||
ctx.Data["IsWatchingRepo"] = repo_model.IsWatchMode(watch.Mode)
|
||||
ctx.Data["RepoWatch"] = watch
|
||||
|
||||
ctx.Data["Repository"], err = repo_model.GetRepositoryByName(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.Name)
|
||||
if err != nil {
|
||||
@@ -51,22 +44,14 @@ func ActionWatch(ctx *context.Context) {
|
||||
|
||||
// ActionWatchOptions watches the repository with a custom selection of events
|
||||
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)),
|
||||
Issues: ctx.FormBool(string(repo_model.WatchIssues)),
|
||||
Releases: ctx.FormBool(string(repo_model.WatchReleases)),
|
||||
}
|
||||
if !opts.PullRequests && !opts.Issues && !opts.Releases {
|
||||
ctx.JSONError(ctx.Tr("repo.watch.options.required"))
|
||||
return
|
||||
}
|
||||
if err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true); err != nil {
|
||||
if err := repo_model.WatchRepoWithOptions(ctx, ctx.Doer, ctx.Repo.Repository, opts); err != nil {
|
||||
handleActionError(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := repo_model.SetWatchOptions(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, opts); err != nil {
|
||||
ctx.ServerError("SetWatchOptions", err)
|
||||
return
|
||||
}
|
||||
ctx.JSONRedirect("")
|
||||
}
|
||||
|
||||
+1
-1
@@ -1739,7 +1739,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Get("/watchers", repo.Watchers)
|
||||
m.Get("/search", reqUnitCodeReader, repo.Search)
|
||||
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/{action:accept_transfer|reject_transfer}", reqSignIn, repo.ActionTransfer)
|
||||
}, optSignIn, context.RepoAssignment)
|
||||
|
||||
@@ -644,8 +644,7 @@ func repoAssignmentPrepareTemplateData(ctx *Context, data *repoAssignmentPrepare
|
||||
ctx.ServerError("GetWatch", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Watch"] = watch
|
||||
ctx.Data["IsWatchingRepo"] = repo_model.IsWatchMode(watch.Mode)
|
||||
ctx.Data["RepoWatch"] = watch
|
||||
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.Repo.Units = nil
|
||||
|
||||
var allowed bool
|
||||
switch act.OpType {
|
||||
case activities_model.ActionCommitRepo, activities_model.ActionPushTag, activities_model.ActionDeleteTag, activities_model.ActionPublishRelease, activities_model.ActionDeleteBranch:
|
||||
if !permCode[i] {
|
||||
continue
|
||||
}
|
||||
case activities_model.ActionCommitRepo, activities_model.ActionPushTag, activities_model.ActionDeleteTag, activities_model.ActionDeleteBranch:
|
||||
allowed = permCode[i] && watcher.IsWatchingAll()
|
||||
case activities_model.ActionPublishRelease:
|
||||
allowed = permCode[i] && watcher.Releases
|
||||
case activities_model.ActionCreateIssue, activities_model.ActionCommentIssue, activities_model.ActionCloseIssue, activities_model.ActionReopenIssue:
|
||||
if !permIssue[i] {
|
||||
continue
|
||||
}
|
||||
case activities_model.ActionCreatePullRequest, activities_model.ActionCommentPull, activities_model.ActionMergePullRequest, activities_model.ActionClosePullRequest, activities_model.ActionReopenPullRequest, activities_model.ActionAutoMergePullRequest:
|
||||
if !permPR[i] {
|
||||
continue
|
||||
}
|
||||
allowed = permIssue[i] && watcher.Issues
|
||||
case activities_model.ActionCreatePullRequest, activities_model.ActionCommentPull, activities_model.ActionMergePullRequest, activities_model.ActionClosePullRequest,
|
||||
activities_model.ActionReopenPullRequest, activities_model.ActionAutoMergePullRequest, activities_model.ActionApprovePullRequest,
|
||||
activities_model.ActionRejectPullRequest, activities_model.ActionPullReviewDismissed, activities_model.ActionPullRequestReadyForReview:
|
||||
allowed = permPR[i] && watcher.PullRequests
|
||||
default:
|
||||
allowed = watcher.IsWatchingAll() // repository events have no watch option of their own
|
||||
}
|
||||
if !allowed {
|
||||
continue
|
||||
}
|
||||
|
||||
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 {
|
||||
return fmt.Errorf("get watchers: %w", err)
|
||||
}
|
||||
|
||||
permCode := make([]bool, len(watchers))
|
||||
permIssue := make([]bool, len(watchers))
|
||||
permPR := make([]bool, len(watchers))
|
||||
|
||||
@@ -200,3 +200,19 @@ func TestNotifyWatchers(t *testing.T) {
|
||||
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}}
|
||||
{{$buttonText := ctx.Locale.Tr (Iif $isIgnoring "repo.ignoring" (Iif $.IsWatchingRepo "repo.watching" "repo.watch"))}}
|
||||
{{$isIgnoring := and $.IsSigned $.RepoWatch.IsIgnoring}}
|
||||
{{$buttonText := ctx.Locale.Tr (Iif $isIgnoring "repo.ignoring" (Iif $.RepoWatch.IsWatching "repo.watching" "repo.watch"))}}
|
||||
{{$count := CountFmt .Repository.NumWatches}}
|
||||
<div id="repo-header-watch" class="flex-text-block">
|
||||
{{if $.IsSigned}}
|
||||
{{$isAll := and $.IsWatchingRepo $.Watch.PullRequests $.Watch.Issues $.Watch.Releases}}
|
||||
{{$isCustom := and $.IsWatchingRepo (not $isAll)}}
|
||||
{{$isParticipating := not (or $.IsWatchingRepo $isIgnoring)}}
|
||||
{{$mode := $.RepoWatch.SelectedMode}}
|
||||
{{$isAll := eq $mode "all"}}
|
||||
{{$isCustom := eq $mode "custom"}}
|
||||
{{$isParticipating := eq $mode "participate"}}
|
||||
<button class="ui dropdown custom compact small basic button" data-global-init="initRepoWatchMenu" aria-label="{{$buttonText}}">
|
||||
{{svg (Iif $isIgnoring "octicon-eye-closed" "octicon-eye")}}
|
||||
<span class="not-mobile" aria-hidden="true">{{$buttonText}}</span>
|
||||
@@ -13,34 +14,37 @@
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
</button>
|
||||
<div class="tippy-target">
|
||||
{{$participating := ctx.Locale.Tr "repo.watch.mode.participating"}}
|
||||
<a class="item{{if $isParticipating}} active{{end}}" role="menuitem" aria-label="{{$participating}}"
|
||||
data-fetch-method="post" data-fetch-url="{{$.RepoLink}}/action/unwatch" data-fetch-sync="$body #repo-header-watch"
|
||||
{{$testParticipating := ctx.Locale.Tr "repo.watch.mode.participating"}}
|
||||
<a class="item{{if $isParticipating}} active{{end}}" role="menuitem" aria-label="{{$testParticipating}}"
|
||||
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")}}
|
||||
<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>
|
||||
{{$all := ctx.Locale.Tr "repo.watch.mode.all"}}
|
||||
<a class="item{{if $isAll}} active{{end}}" role="menuitem" aria-label="{{$all}}"
|
||||
{{$textAll := ctx.Locale.Tr "repo.watch.mode.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"
|
||||
>
|
||||
{{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>
|
||||
{{$ignore := ctx.Locale.Tr "repo.watch.mode.ignore"}}
|
||||
<a class="item{{if $.Watch.IsIgnoring}} active{{end}}" role="menuitem" aria-label="{{$ignore}}"
|
||||
{{$textIgnore := ctx.Locale.Tr "repo.watch.mode.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"
|
||||
>
|
||||
{{svg "octicon-check" 16 (Iif $.Watch.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>
|
||||
{{svg "octicon-check" 16 (Iif $isIgnoring "" "tw-invisible")}}
|
||||
<div>{{$textIgnore}}<div class="tw-text-12 tw-text-text-light-2">{{ctx.Locale.Tr "repo.watch.mode.ignore.desc"}}</div></div>
|
||||
</a>
|
||||
{{$custom := ctx.Locale.Tr "repo.watch.mode.custom"}}
|
||||
<a class="item{{if $isCustom}} active{{end}}" role="menuitem" aria-label="{{$custom}}"
|
||||
data-global-click="onRepoWatchOptionsClick" data-url="{{$.RepoLink}}/action/watch/options"
|
||||
data-issues="{{$.Watch.Issues}}" data-pull-requests="{{$.Watch.PullRequests}}" data-releases="{{$.Watch.Releases}}"
|
||||
{{$textCustom := ctx.Locale.Tr "repo.watch.mode.custom"}}
|
||||
<a class="item {{if $isCustom}}active{{end}} show-modal" role="menuitem" aria-label="{{$textCustom}}"
|
||||
data-modal="#repo-watch-options-modal"
|
||||
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")}}
|
||||
<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>
|
||||
</div>
|
||||
{{else}}
|
||||
|
||||
@@ -2,13 +2,20 @@
|
||||
<div class="header">{{ctx.Locale.Tr "notifications"}}</div>
|
||||
<form class="ui form form-fetch-action" method="post">
|
||||
<div class="content">
|
||||
{{range $name := StringUtils.Split "issues,pull_requests,releases" ","}}
|
||||
<div class="field">
|
||||
<div class="flex-relaxed-list tw-gap-4">
|
||||
<div class="ui checkbox">
|
||||
<input name="{{$name}}" type="checkbox"><label>{{ctx.Locale.Tr (printf "repo.watch.options.%s" $name)}}</label>
|
||||
<input name="issues" type="checkbox">
|
||||
<label>{{ctx.Locale.Tr "repo.watch.options.issues"}}</label>
|
||||
</div>
|
||||
<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>
|
||||
{{end}}
|
||||
</div>
|
||||
{{template "base/modal_actions_confirm" dict "ModalButtonTypes" "confirm"}}
|
||||
</form>
|
||||
|
||||
@@ -57,9 +57,12 @@
|
||||
</a>
|
||||
{{$watch := and $.Watches (index $.Watches .ID)}}
|
||||
{{if $watch}}
|
||||
<button class="btn flex-text-inline" data-global-click="onRepoWatchOptionsClick"
|
||||
data-url="{{.Link}}/action/watch/options"
|
||||
data-issues="{{$watch.Issues}}" data-pull-requests="{{$watch.PullRequests}}" data-releases="{{$watch.Releases}}"
|
||||
<button class="btn flex-text-inline show-modal"
|
||||
data-modal="#repo-watch-options-modal"
|
||||
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"}}"
|
||||
>{{svg "octicon-gear" 16}}</button>
|
||||
{{end}}
|
||||
|
||||
@@ -111,8 +111,10 @@ function onShowModalClick(el: HTMLElement, e: MouseEvent) {
|
||||
|
||||
if (attrTargetProp) {
|
||||
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')) {
|
||||
(attrTarget as HTMLInputElement | HTMLTextAreaElement).value = attrib.value; // FIXME: add more supports like checkbox
|
||||
(attrTarget as HTMLInputElement | HTMLTextAreaElement).value = attrib.value;
|
||||
} else {
|
||||
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 {showFomanticModal} from '../modules/fomantic/modal.ts';
|
||||
import {registerGlobalEventFunc, registerGlobalInitFunc} from '../modules/observer.ts';
|
||||
import type {Instance} from 'tippy.js';
|
||||
|
||||
let watchMenuTippy: Instance | null = null;
|
||||
import {registerGlobalInitFunc} from '../modules/observer.ts';
|
||||
|
||||
export function initRepoWatch() {
|
||||
registerGlobalInitFunc('initRepoWatchMenu', (btn: HTMLElement) => {
|
||||
watchMenuTippy?.destroy(); // a watch action replaces the button, orphaning the old menu
|
||||
const menu = btn.nextElementSibling!;
|
||||
watchMenuTippy = createTippy(btn, {
|
||||
const watchMenuTippy = createTippy(btn, {
|
||||
content: menu,
|
||||
theme: 'menu',
|
||||
maxWidth: 350,
|
||||
@@ -18,16 +13,6 @@ export function initRepoWatch() {
|
||||
interactive: true,
|
||||
hideOnClick: true,
|
||||
});
|
||||
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);
|
||||
menu.addEventListener('click', () => watchMenuTippy.hide());
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user