mirror of https://github.com/go-gitea/gitea.git
Merge branch 'release/v1.22' into backport/30660-to-1.22
This commit is contained in:
commit
94c1adb610
2
Makefile
2
Makefile
|
@ -778,7 +778,7 @@ generate-backend: $(TAGS_PREREQ) generate-go
|
|||
.PHONY: generate-go
|
||||
generate-go: $(TAGS_PREREQ)
|
||||
@echo "Running go generate..."
|
||||
@CC= GOOS= GOARCH= $(GO) generate -tags '$(TAGS)' ./...
|
||||
@CC= GOOS= GOARCH= CGO_ENABLED=0 $(GO) generate -tags '$(TAGS)' ./...
|
||||
|
||||
.PHONY: security-check
|
||||
security-check:
|
||||
|
|
|
@ -130,7 +130,10 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us
|
|||
// and just waste 1 unit is cheaper than re-allocate memory once.
|
||||
users := make([]*user_model.User, 0, len(uniqueUserIDs)+1)
|
||||
if len(userIDs) > 0 {
|
||||
if err = e.In("id", uniqueUserIDs.Values()).OrderBy(user_model.GetOrderByName()).Find(&users); err != nil {
|
||||
if err = e.In("id", uniqueUserIDs.Values()).
|
||||
Where(builder.Eq{"`user`.is_active": true}).
|
||||
OrderBy(user_model.GetOrderByName()).
|
||||
Find(&users); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
@ -152,7 +155,8 @@ func GetReviewers(ctx context.Context, repo *Repository, doerID, posterID int64)
|
|||
return nil, err
|
||||
}
|
||||
|
||||
cond := builder.And(builder.Neq{"`user`.id": posterID})
|
||||
cond := builder.And(builder.Neq{"`user`.id": posterID}).
|
||||
And(builder.Eq{"`user`.is_active": true})
|
||||
|
||||
if repo.IsPrivate || repo.Owner.Visibility == api.VisibleTypePrivate {
|
||||
// This a private repository:
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
@ -25,8 +26,17 @@ func TestRepoAssignees(t *testing.T) {
|
|||
repo21 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 21})
|
||||
users, err = repo_model.GetRepoAssignees(db.DefaultContext, repo21)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, users, 4)
|
||||
assert.ElementsMatch(t, []int64{10, 15, 16, 18}, []int64{users[0].ID, users[1].ID, users[2].ID, users[3].ID})
|
||||
if assert.Len(t, users, 4) {
|
||||
assert.ElementsMatch(t, []int64{10, 15, 16, 18}, []int64{users[0].ID, users[1].ID, users[2].ID, users[3].ID})
|
||||
}
|
||||
|
||||
// do not return deactivated users
|
||||
assert.NoError(t, user_model.UpdateUserCols(db.DefaultContext, &user_model.User{ID: 15, IsActive: false}, "is_active"))
|
||||
users, err = repo_model.GetRepoAssignees(db.DefaultContext, repo21)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, users, 3) {
|
||||
assert.NotContains(t, []int64{users[0].ID, users[1].ID, users[2].ID}, 15)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoGetReviewers(t *testing.T) {
|
||||
|
@ -38,17 +48,19 @@ func TestRepoGetReviewers(t *testing.T) {
|
|||
ctx := db.DefaultContext
|
||||
reviewers, err := repo_model.GetReviewers(ctx, repo1, 2, 2)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, reviewers, 4)
|
||||
if assert.Len(t, reviewers, 3) {
|
||||
assert.ElementsMatch(t, []int64{1, 4, 11}, []int64{reviewers[0].ID, reviewers[1].ID, reviewers[2].ID})
|
||||
}
|
||||
|
||||
// should include doer if doer is not PR poster.
|
||||
reviewers, err = repo_model.GetReviewers(ctx, repo1, 11, 2)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, reviewers, 4)
|
||||
assert.Len(t, reviewers, 3)
|
||||
|
||||
// should not include PR poster, if PR poster would be otherwise eligible
|
||||
reviewers, err = repo_model.GetReviewers(ctx, repo1, 11, 4)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, reviewers, 3)
|
||||
assert.Len(t, reviewers, 2)
|
||||
|
||||
// test private user repo
|
||||
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
|
|
|
@ -121,29 +121,25 @@ func RenderIssueTitle(ctx context.Context, text string, metas map[string]string)
|
|||
// RenderLabel renders a label
|
||||
// locale is needed due to an import cycle with our context providing the `Tr` function
|
||||
func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_model.Label) template.HTML {
|
||||
var (
|
||||
archivedCSSClass string
|
||||
textColor = util.ContrastColor(label.Color)
|
||||
labelScope = label.ExclusiveScope()
|
||||
)
|
||||
|
||||
description := emoji.ReplaceAliases(template.HTMLEscapeString(label.Description))
|
||||
var extraCSSClasses string
|
||||
textColor := util.ContrastColor(label.Color)
|
||||
labelScope := label.ExclusiveScope()
|
||||
descriptionText := emoji.ReplaceAliases(label.Description)
|
||||
|
||||
if label.IsArchived() {
|
||||
archivedCSSClass = "archived-label"
|
||||
description = fmt.Sprintf("(%s) %s", locale.TrString("archived"), description)
|
||||
extraCSSClasses = "archived-label"
|
||||
descriptionText = fmt.Sprintf("(%s) %s", locale.TrString("archived"), descriptionText)
|
||||
}
|
||||
|
||||
if labelScope == "" {
|
||||
// Regular label
|
||||
s := fmt.Sprintf("<div class='ui label %s' style='color: %s !important; background-color: %s !important;' data-tooltip-content title='%s'>%s</div>",
|
||||
archivedCSSClass, textColor, label.Color, description, RenderEmoji(ctx, label.Name))
|
||||
return template.HTML(s)
|
||||
return HTMLFormat(`<div class="ui label %s" style="color: %s !important; background-color: %s !important;" data-tooltip-content title="%s">%s</div>`,
|
||||
extraCSSClasses, textColor, label.Color, descriptionText, RenderEmoji(ctx, label.Name))
|
||||
}
|
||||
|
||||
// Scoped label
|
||||
scopeText := RenderEmoji(ctx, labelScope)
|
||||
itemText := RenderEmoji(ctx, label.Name[len(labelScope)+1:])
|
||||
scopeHTML := RenderEmoji(ctx, labelScope)
|
||||
itemHTML := RenderEmoji(ctx, label.Name[len(labelScope)+1:])
|
||||
|
||||
// Make scope and item background colors slightly darker and lighter respectively.
|
||||
// More contrast needed with higher luminance, empirically tweaked.
|
||||
|
@ -171,14 +167,13 @@ func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_m
|
|||
itemColor := "#" + hex.EncodeToString(itemBytes)
|
||||
scopeColor := "#" + hex.EncodeToString(scopeBytes)
|
||||
|
||||
s := fmt.Sprintf("<span class='ui label %s scope-parent' data-tooltip-content title='%s'>"+
|
||||
"<div class='ui label scope-left' style='color: %s !important; background-color: %s !important'>%s</div>"+
|
||||
"<div class='ui label scope-right' style='color: %s !important; background-color: %s !important'>%s</div>"+
|
||||
"</span>",
|
||||
archivedCSSClass, description,
|
||||
textColor, scopeColor, scopeText,
|
||||
textColor, itemColor, itemText)
|
||||
return template.HTML(s)
|
||||
return HTMLFormat(`<span class="ui label %s scope-parent" data-tooltip-content title="%s">`+
|
||||
`<div class="ui label scope-left" style="color: %s !important; background-color: %s !important">%s</div>`+
|
||||
`<div class="ui label scope-right" style="color: %s !important; background-color: %s !important">%s</div>`+
|
||||
`</span>`,
|
||||
extraCSSClasses, descriptionText,
|
||||
textColor, scopeColor, scopeHTML,
|
||||
textColor, itemColor, itemHTML)
|
||||
}
|
||||
|
||||
// RenderEmoji renders html text with emoji post processors
|
||||
|
|
|
@ -42,7 +42,6 @@
|
|||
"postcss": "8.4.38",
|
||||
"postcss-loader": "8.1.1",
|
||||
"postcss-nesting": "12.1.2",
|
||||
"pretty-ms": "9.0.0",
|
||||
"sortablejs": "1.15.2",
|
||||
"swagger-ui-dist": "5.17.2",
|
||||
"tailwindcss": "3.4.3",
|
||||
|
@ -9170,17 +9169,6 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-ms": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
|
||||
"integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
|
@ -9772,20 +9760,6 @@
|
|||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-ms": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.0.0.tgz",
|
||||
"integrity": "sha512-E9e9HJ9R9NasGOgPaPE8VMeiPKAyWR5jcFpNnwIejslIhWqdqOrb2wShBsncMPUb+BcCd2OPYfh7p2W6oemTng==",
|
||||
"dependencies": {
|
||||
"parse-ms": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/printable-characters": {
|
||||
"version": "1.0.42",
|
||||
"resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz",
|
||||
|
|
|
@ -41,7 +41,6 @@
|
|||
"postcss": "8.4.38",
|
||||
"postcss-loader": "8.1.1",
|
||||
"postcss-nesting": "12.1.2",
|
||||
"pretty-ms": "9.0.0",
|
||||
"sortablejs": "1.15.2",
|
||||
"swagger-ui-dist": "5.17.2",
|
||||
"tailwindcss": "3.4.3",
|
||||
|
|
|
@ -2177,7 +2177,10 @@ func GetIssueInfo(ctx *context.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToIssue(ctx, ctx.Doer, issue))
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"convertedIssue": convert.ToIssue(ctx, ctx.Doer, issue),
|
||||
"renderedLabels": templates.RenderLabels(ctx, ctx.Locale, issue.Labels, ctx.Repo.RepoLink, issue),
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateIssueTitle change issue's title
|
||||
|
|
|
@ -12,6 +12,14 @@
|
|||
|
||||
<!-- mobile right menu, it must be here because in mobile view, each item is a flex column, the first item is a full row column -->
|
||||
<div class="ui secondary menu item navbar-mobile-right only-mobile">
|
||||
{{if and .IsSigned EnableTimetracking .ActiveStopwatch}}
|
||||
<a id="mobile-stopwatch-icon" class="active-stopwatch item tw-mx-0" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}" data-seconds="{{.ActiveStopwatch.Seconds}}">
|
||||
<div class="tw-relative">
|
||||
{{svg "octicon-stopwatch"}}
|
||||
<span class="header-stopwatch-dot"></span>
|
||||
</div>
|
||||
</a>
|
||||
{{end}}
|
||||
{{if .IsSigned}}
|
||||
<a id="mobile-notifications-icon" class="item tw-w-auto tw-p-2" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}">
|
||||
<div class="tw-relative">
|
||||
|
@ -74,41 +82,13 @@
|
|||
</div><!-- end content avatar menu -->
|
||||
</div><!-- end dropdown avatar menu -->
|
||||
{{else if .IsSigned}}
|
||||
{{if EnableTimetracking}}
|
||||
<a class="active-stopwatch-trigger item tw-mx-0{{if not .ActiveStopwatch}} tw-hidden{{end}}" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}">
|
||||
{{if and EnableTimetracking .ActiveStopwatch}}
|
||||
<a class="item not-mobile active-stopwatch tw-mx-0" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}" data-seconds="{{.ActiveStopwatch.Seconds}}">
|
||||
<div class="tw-relative">
|
||||
{{svg "octicon-stopwatch"}}
|
||||
<span class="header-stopwatch-dot"></span>
|
||||
</div>
|
||||
<span class="only-mobile tw-ml-2">{{ctx.Locale.Tr "active_stopwatch"}}</span>
|
||||
</a>
|
||||
<div class="active-stopwatch-popup item tippy-target tw-p-2">
|
||||
<div class="tw-flex tw-items-center">
|
||||
<a class="stopwatch-link tw-flex tw-items-center" href="{{.ActiveStopwatch.IssueLink}}">
|
||||
{{svg "octicon-issue-opened" 16 "tw-mr-2"}}
|
||||
<span class="stopwatch-issue">{{.ActiveStopwatch.RepoSlug}}#{{.ActiveStopwatch.IssueIndex}}</span>
|
||||
<span class="ui primary label stopwatch-time tw-my-0 tw-mx-4" data-seconds="{{.ActiveStopwatch.Seconds}}">
|
||||
{{if .ActiveStopwatch}}{{Sec2Time .ActiveStopwatch.Seconds}}{{end}}
|
||||
</span>
|
||||
</a>
|
||||
<form class="stopwatch-commit" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/toggle">
|
||||
{{.CsrfTokenHtml}}
|
||||
<button
|
||||
type="submit"
|
||||
class="ui button mini compact basic icon"
|
||||
data-tooltip-content="{{ctx.Locale.Tr "repo.issues.stop_tracking"}}"
|
||||
>{{svg "octicon-square-fill"}}</button>
|
||||
</form>
|
||||
<form class="stopwatch-cancel" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/cancel">
|
||||
{{.CsrfTokenHtml}}
|
||||
<button
|
||||
type="submit"
|
||||
class="ui button mini compact basic icon"
|
||||
data-tooltip-content="{{ctx.Locale.Tr "repo.issues.cancel_tracking"}}"
|
||||
>{{svg "octicon-trash"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<a class="item not-mobile tw-mx-0" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}">
|
||||
|
@ -202,4 +182,33 @@
|
|||
</a>
|
||||
{{end}}
|
||||
</div><!-- end full right menu -->
|
||||
|
||||
{{if and .IsSigned EnableTimetracking .ActiveStopwatch}}
|
||||
<div class="active-stopwatch-popup tippy-target">
|
||||
<div class="tw-flex tw-items-center tw-gap-2 tw-p-3">
|
||||
<a class="stopwatch-link tw-flex tw-items-center tw-gap-2 muted" href="{{.ActiveStopwatch.IssueLink}}">
|
||||
{{svg "octicon-issue-opened" 16}}
|
||||
<span class="stopwatch-issue">{{.ActiveStopwatch.RepoSlug}}#{{.ActiveStopwatch.IssueIndex}}</span>
|
||||
</a>
|
||||
<div class="tw-flex tw-gap-1">
|
||||
<form class="stopwatch-commit" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/toggle">
|
||||
{{.CsrfTokenHtml}}
|
||||
<button
|
||||
type="submit"
|
||||
class="ui button mini compact basic icon tw-mr-0"
|
||||
data-tooltip-content="{{ctx.Locale.Tr "repo.issues.stop_tracking"}}"
|
||||
>{{svg "octicon-square-fill"}}</button>
|
||||
</form>
|
||||
<form class="stopwatch-cancel" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/cancel">
|
||||
{{.CsrfTokenHtml}}
|
||||
<button
|
||||
type="submit"
|
||||
class="ui button mini compact basic icon tw-mr-0"
|
||||
data-tooltip-content="{{ctx.Locale.Tr "repo.issues.cancel_tracking"}}"
|
||||
>{{svg "octicon-trash"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</nav>
|
||||
|
|
|
@ -40,8 +40,9 @@
|
|||
</a>
|
||||
{{end}}
|
||||
{{if .IsOrganizationOwner}}
|
||||
<span class="item-flex-space"></span>
|
||||
<a class="{{if .PageIsOrgSettings}}active {{end}}item" href="{{.OrgLink}}/settings">
|
||||
{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}}
|
||||
{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
|
|
|
@ -216,6 +216,7 @@
|
|||
{{template "custom/extra_tabs" .}}
|
||||
|
||||
{{if .Permission.IsAdmin}}
|
||||
<span class="item-flex-space"></span>
|
||||
<a class="{{if .PageIsRepoSettings}}active {{end}} item" href="{{.RepoLink}}/settings">
|
||||
{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}}
|
||||
</a>
|
||||
|
|
|
@ -684,7 +684,9 @@ func TestAPIRepoGetReviewers(t *testing.T) {
|
|||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var reviewers []*api.User
|
||||
DecodeJSON(t, resp, &reviewers)
|
||||
assert.Len(t, reviewers, 4)
|
||||
if assert.Len(t, reviewers, 3) {
|
||||
assert.ElementsMatch(t, []int64{1, 4, 11}, []int64{reviewers[0].ID, reviewers[1].ID, reviewers[2].ID})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIRepoGetAssignees(t *testing.T) {
|
||||
|
|
|
@ -6,6 +6,7 @@ package integration
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
|
@ -573,10 +574,14 @@ func TestGetIssueInfo(t *testing.T) {
|
|||
urlStr := fmt.Sprintf("/%s/%s/issues/%d/info", owner.Name, repo.Name, issue.Index)
|
||||
req := NewRequest(t, "GET", urlStr)
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
var apiIssue api.Issue
|
||||
DecodeJSON(t, resp, &apiIssue)
|
||||
var respStruct struct {
|
||||
ConvertedIssue api.Issue
|
||||
RenderedLabels template.HTML
|
||||
}
|
||||
DecodeJSON(t, resp, &respStruct)
|
||||
|
||||
assert.EqualValues(t, issue.ID, apiIssue.ID)
|
||||
assert.EqualValues(t, issue.ID, respStruct.ConvertedIssue.ID)
|
||||
assert.Contains(t, string(respStruct.RenderedLabels), `"labels-list"`)
|
||||
}
|
||||
|
||||
func TestUpdateIssueDeadline(t *testing.T) {
|
||||
|
|
|
@ -942,6 +942,23 @@ overflow-menu .overflow-menu-items .item {
|
|||
margin-bottom: 0 !important; /* reset fomantic's margin, because the active menu has special bottom border */
|
||||
}
|
||||
|
||||
overflow-menu .overflow-menu-items .item-flex-space {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
overflow-menu .overflow-menu-button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: inherit;
|
||||
text-align: center;
|
||||
width: 32px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
overflow-menu .overflow-menu-button:hover {
|
||||
color: var(--color-text-dark);
|
||||
}
|
||||
|
||||
overflow-menu .ui.label {
|
||||
margin-left: 7px !important; /* save some space */
|
||||
}
|
||||
|
|
|
@ -6,38 +6,6 @@
|
|||
max-width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.ui.ui.ui.container:not(.fluid) {
|
||||
width: auto;
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) and (max-width: 991.98px) {
|
||||
.ui.ui.ui.container:not(.fluid) {
|
||||
width: 723px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) and (max-width: 1199.98px) {
|
||||
.ui.ui.ui.container:not(.fluid) {
|
||||
width: 933px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.ui.ui.ui.container:not(.fluid) {
|
||||
width: 1127px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.ui.fluid.container {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
@ -103,19 +103,12 @@
|
|||
width: 50%;
|
||||
min-height: 48px;
|
||||
}
|
||||
#navbar #mobile-stopwatch-icon,
|
||||
#navbar #mobile-notifications-icon {
|
||||
margin-right: 6px !important;
|
||||
}
|
||||
}
|
||||
|
||||
#navbar a.item .notification_count {
|
||||
color: var(--color-nav-bg);
|
||||
padding: 0 3.75px;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
#navbar a.item:hover .notification_count,
|
||||
#navbar a.item:hover .header-stopwatch-dot {
|
||||
border-color: var(--color-nav-hover-bg);
|
||||
|
@ -123,6 +116,11 @@
|
|||
|
||||
#navbar a.item .notification_count,
|
||||
#navbar a.item .header-stopwatch-dot {
|
||||
color: var(--color-nav-bg);
|
||||
padding: 0 3.75px;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
background: var(--color-primary);
|
||||
border: 2px solid var(--color-nav-bg);
|
||||
position: absolute;
|
||||
|
@ -135,6 +133,8 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1; /* prevent menu button background from overlaying icon */
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.secondary-nav {
|
||||
|
|
|
@ -16,8 +16,8 @@
|
|||
|
||||
.tippy-box {
|
||||
position: relative;
|
||||
background-color: var(--color-body);
|
||||
color: var(--color-secondary-dark-6);
|
||||
background-color: var(--color-menu);
|
||||
color: var(--color-text);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 1rem;
|
||||
|
@ -25,7 +25,6 @@
|
|||
|
||||
.tippy-content {
|
||||
position: relative;
|
||||
padding: 1rem; /* if you need different padding, use different data-theme */
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
|
@ -166,5 +165,5 @@
|
|||
}
|
||||
|
||||
.tippy-svg-arrow-inner {
|
||||
fill: var(--color-body);
|
||||
fill: var(--color-menu);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
import {SvgIcon} from '../svg.js';
|
||||
import {contrastColor} from '../utils/color.js';
|
||||
import {GET} from '../modules/fetch.js';
|
||||
|
||||
const {appSubUrl, i18n} = window.config;
|
||||
|
@ -10,6 +9,7 @@ export default {
|
|||
data: () => ({
|
||||
loading: false,
|
||||
issue: null,
|
||||
renderedLabels: '',
|
||||
i18nErrorOccurred: i18n.error_occurred,
|
||||
i18nErrorMessage: null,
|
||||
}),
|
||||
|
@ -56,14 +56,6 @@ export default {
|
|||
}
|
||||
return 'red'; // Closed Issue
|
||||
},
|
||||
|
||||
labels() {
|
||||
return this.issue.labels.map((label) => ({
|
||||
name: label.name,
|
||||
color: `#${label.color}`,
|
||||
textColor: contrastColor(`#${label.color}`),
|
||||
}));
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.root.addEventListener('ce-load-context-popup', (e) => {
|
||||
|
@ -79,13 +71,14 @@ export default {
|
|||
this.i18nErrorMessage = null;
|
||||
|
||||
try {
|
||||
const response = await GET(`${appSubUrl}/${data.owner}/${data.repo}/issues/${data.index}/info`);
|
||||
const response = await GET(`${appSubUrl}/${data.owner}/${data.repo}/issues/${data.index}/info`); // backend: GetIssueInfo
|
||||
const respJson = await response.json();
|
||||
if (!response.ok) {
|
||||
this.i18nErrorMessage = respJson.message ?? i18n.network_error;
|
||||
return;
|
||||
}
|
||||
this.issue = respJson;
|
||||
this.issue = respJson.convertedIssue;
|
||||
this.renderedLabels = respJson.renderedLabels;
|
||||
} catch {
|
||||
this.i18nErrorMessage = i18n.network_error;
|
||||
} finally {
|
||||
|
@ -102,16 +95,8 @@ export default {
|
|||
<p><small>{{ issue.repository.full_name }} on {{ createdAt }}</small></p>
|
||||
<p><svg-icon :name="icon" :class="['text', color]"/> <strong>{{ issue.title }}</strong> #{{ issue.number }}</p>
|
||||
<p>{{ body }}</p>
|
||||
<div class="labels-list">
|
||||
<div
|
||||
v-for="label in labels"
|
||||
:key="label.name"
|
||||
class="ui label"
|
||||
:style="{ color: label.textColor, backgroundColor: label.color }"
|
||||
>
|
||||
{{ label.name }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-html="renderedLabels"/>
|
||||
</div>
|
||||
<div v-if="!loading && issue === null">
|
||||
<p><small>{{ i18nErrorOccurred }}</small></p>
|
||||
|
|
|
@ -251,9 +251,9 @@ const sfc = {
|
|||
this.repos = json.data.map((webSearchRepo) => {
|
||||
return {
|
||||
...webSearchRepo.repository,
|
||||
latest_commit_status_state: webSearchRepo.latest_commit_status.State,
|
||||
latest_commit_status_state: webSearchRepo.latest_commit_status?.State, // if latest_commit_status is null, it means there is no commit status
|
||||
latest_commit_status_state_link: webSearchRepo.latest_commit_status?.TargetURL,
|
||||
locale_latest_commit_status_state: webSearchRepo.locale_latest_commit_status,
|
||||
latest_commit_status_state_link: webSearchRepo.latest_commit_status.TargetURL,
|
||||
};
|
||||
});
|
||||
const count = response.headers.get('X-Total-Count');
|
||||
|
|
|
@ -53,7 +53,7 @@ export function initCommonIssueListQuickGoto() {
|
|||
// try to check whether the parsed goto link is valid
|
||||
let targetUrl = parseIssueListQuickGotoLink(repoLink, searchText);
|
||||
if (targetUrl) {
|
||||
const res = await GET(`${targetUrl}/info`);
|
||||
const res = await GET(`${targetUrl}/info`); // backend: GetIssueInfo, it only checks whether the issue exists by status code
|
||||
if (res.status !== 200) targetUrl = '';
|
||||
}
|
||||
// if the input value has changed, then ignore the result
|
||||
|
|
|
@ -18,6 +18,7 @@ export function attachRefIssueContextPopup(refIssues) {
|
|||
if (!owner) return;
|
||||
|
||||
const el = document.createElement('div');
|
||||
el.classList.add('tw-p-3');
|
||||
refIssue.parentNode.insertBefore(el, refIssue.nextSibling);
|
||||
|
||||
const view = createApp(ContextPopup);
|
||||
|
@ -30,6 +31,7 @@ export function attachRefIssueContextPopup(refIssues) {
|
|||
}
|
||||
|
||||
createTippy(refIssue, {
|
||||
theme: 'default',
|
||||
content: el,
|
||||
placement: 'top-start',
|
||||
interactive: true,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import $ from 'jquery';
|
||||
import {GET} from '../modules/fetch.js';
|
||||
import {toggleElem} from '../utils/dom.js';
|
||||
import {logoutFromWorker} from '../modules/worker.js';
|
||||
|
||||
const {appSubUrl, notificationSettings, assetVersionEncoded} = window.config;
|
||||
let notificationSequenceNumber = 0;
|
||||
|
@ -95,7 +96,7 @@ export function initNotificationCount() {
|
|||
type: 'close',
|
||||
});
|
||||
worker.port.close();
|
||||
window.location.href = `${appSubUrl}/`;
|
||||
logoutFromWorker();
|
||||
} else if (event.data.type === 'close') {
|
||||
worker.port.postMessage({
|
||||
type: 'close',
|
||||
|
|
|
@ -113,6 +113,7 @@ function showLineButton() {
|
|||
btn.closest('.code-view').append(menu.cloneNode(true));
|
||||
|
||||
createTippy(btn, {
|
||||
theme: 'menu',
|
||||
trigger: 'click',
|
||||
hideOnClick: true,
|
||||
content: menu,
|
||||
|
|
|
@ -502,6 +502,7 @@ export function initRepoPullRequestReview() {
|
|||
if ($reviewBtn.length && $panel.length) {
|
||||
const tippy = createTippy($reviewBtn[0], {
|
||||
content: $panel[0],
|
||||
theme: 'default',
|
||||
placement: 'bottom',
|
||||
trigger: 'click',
|
||||
maxWidth: 'none',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import prettyMilliseconds from 'pretty-ms';
|
||||
import {createTippy} from '../modules/tippy.js';
|
||||
import {GET} from '../modules/fetch.js';
|
||||
import {hideElem, showElem} from '../utils/dom.js';
|
||||
import {logoutFromWorker} from '../modules/worker.js';
|
||||
|
||||
const {appSubUrl, notificationSettings, enableTimeTracking, assetVersionEncoded} = window.config;
|
||||
|
||||
|
@ -10,28 +10,31 @@ export function initStopwatch() {
|
|||
return;
|
||||
}
|
||||
|
||||
const stopwatchEl = document.querySelector('.active-stopwatch-trigger');
|
||||
const stopwatchEls = document.querySelectorAll('.active-stopwatch');
|
||||
const stopwatchPopup = document.querySelector('.active-stopwatch-popup');
|
||||
|
||||
if (!stopwatchEl || !stopwatchPopup) {
|
||||
if (!stopwatchEls.length || !stopwatchPopup) {
|
||||
return;
|
||||
}
|
||||
|
||||
stopwatchEl.removeAttribute('href'); // intended for noscript mode only
|
||||
|
||||
createTippy(stopwatchEl, {
|
||||
content: stopwatchPopup,
|
||||
placement: 'bottom-end',
|
||||
trigger: 'click',
|
||||
maxWidth: 'none',
|
||||
interactive: true,
|
||||
hideOnClick: true,
|
||||
});
|
||||
|
||||
// global stop watch (in the head_navbar), it should always work in any case either the EventSource or the PeriodicPoller is used.
|
||||
const currSeconds = document.querySelector('.stopwatch-time')?.getAttribute('data-seconds');
|
||||
if (currSeconds) {
|
||||
updateStopwatchTime(currSeconds);
|
||||
const seconds = stopwatchEls[0]?.getAttribute('data-seconds');
|
||||
if (seconds) {
|
||||
updateStopwatchTime(parseInt(seconds));
|
||||
}
|
||||
|
||||
for (const stopwatchEl of stopwatchEls) {
|
||||
stopwatchEl.removeAttribute('href'); // intended for noscript mode only
|
||||
|
||||
createTippy(stopwatchEl, {
|
||||
content: stopwatchPopup.cloneNode(true),
|
||||
placement: 'bottom-end',
|
||||
trigger: 'click',
|
||||
maxWidth: 'none',
|
||||
interactive: true,
|
||||
hideOnClick: true,
|
||||
theme: 'default',
|
||||
});
|
||||
}
|
||||
|
||||
let usingPeriodicPoller = false;
|
||||
|
@ -75,7 +78,7 @@ export function initStopwatch() {
|
|||
type: 'close',
|
||||
});
|
||||
worker.port.close();
|
||||
window.location.href = `${appSubUrl}/`;
|
||||
logoutFromWorker();
|
||||
} else if (event.data.type === 'close') {
|
||||
worker.port.postMessage({
|
||||
type: 'close',
|
||||
|
@ -124,10 +127,9 @@ async function updateStopwatch() {
|
|||
|
||||
function updateStopwatchData(data) {
|
||||
const watch = data[0];
|
||||
const btnEl = document.querySelector('.active-stopwatch-trigger');
|
||||
const btnEls = document.querySelectorAll('.active-stopwatch');
|
||||
if (!watch) {
|
||||
clearStopwatchTimer();
|
||||
hideElem(btnEl);
|
||||
hideElem(btnEls);
|
||||
} else {
|
||||
const {repo_owner_name, repo_name, issue_index, seconds} = watch;
|
||||
const issueUrl = `${appSubUrl}/${repo_owner_name}/${repo_name}/issues/${issue_index}`;
|
||||
|
@ -137,31 +139,28 @@ function updateStopwatchData(data) {
|
|||
const stopwatchIssue = document.querySelector('.stopwatch-issue');
|
||||
if (stopwatchIssue) stopwatchIssue.textContent = `${repo_owner_name}/${repo_name}#${issue_index}`;
|
||||
updateStopwatchTime(seconds);
|
||||
showElem(btnEl);
|
||||
showElem(btnEls);
|
||||
}
|
||||
return Boolean(data.length);
|
||||
}
|
||||
|
||||
let updateTimeIntervalId = null; // holds setInterval id when active
|
||||
function clearStopwatchTimer() {
|
||||
if (updateTimeIntervalId !== null) {
|
||||
clearInterval(updateTimeIntervalId);
|
||||
updateTimeIntervalId = null;
|
||||
// TODO: This flickers on page load, we could avoid this by making a custom
|
||||
// element to render time periods. Feeding a datetime in backend does not work
|
||||
// when time zone between server and client differs.
|
||||
function updateStopwatchTime(seconds) {
|
||||
if (!Number.isFinite(seconds)) return;
|
||||
const datetime = (new Date(Date.now() - seconds * 1000)).toISOString();
|
||||
for (const parent of document.querySelectorAll('.header-stopwatch-dot')) {
|
||||
const existing = parent.querySelector(':scope > relative-time');
|
||||
if (existing) {
|
||||
existing.setAttribute('datetime', datetime);
|
||||
} else {
|
||||
const el = document.createElement('relative-time');
|
||||
el.setAttribute('format', 'micro');
|
||||
el.setAttribute('datetime', datetime);
|
||||
el.setAttribute('lang', 'en-US');
|
||||
el.setAttribute('title', ''); // make <relative-time> show no title and therefor no tooltip
|
||||
parent.append(el);
|
||||
}
|
||||
}
|
||||
}
|
||||
function updateStopwatchTime(seconds) {
|
||||
const secs = parseInt(seconds);
|
||||
if (!Number.isFinite(secs)) return;
|
||||
|
||||
clearStopwatchTimer();
|
||||
const stopwatch = document.querySelector('.stopwatch-time');
|
||||
// TODO: replace with <relative-time> similar to how system status up time is shown
|
||||
const start = Date.now();
|
||||
const updateUi = () => {
|
||||
const delta = Date.now() - start;
|
||||
const dur = prettyMilliseconds(secs * 1000 + delta, {compact: true});
|
||||
if (stopwatch) stopwatch.textContent = dur;
|
||||
};
|
||||
updateUi();
|
||||
updateTimeIntervalId = setInterval(updateUi, 1000);
|
||||
}
|
||||
|
|
|
@ -37,8 +37,10 @@ export function createTippy(target, opts = {}) {
|
|||
return onShow?.(instance);
|
||||
},
|
||||
arrow: arrow || (theme === 'bare' ? false : arrowSvg),
|
||||
role: role || 'menu', // HTML role attribute
|
||||
theme: theme || role || 'menu', // CSS theme, either "tooltip", "menu", "box-with-header" or "bare"
|
||||
// HTML role attribute, ideally the default role would be "popover" but it does not exist
|
||||
role: role || 'menu',
|
||||
// CSS theme, either "default", "tooltip", "menu", "box-with-header" or "bare"
|
||||
theme: theme || role || 'default',
|
||||
plugins: [followCursor],
|
||||
...other,
|
||||
});
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import {sleep} from '../utils.js';
|
||||
|
||||
const {appSubUrl} = window.config;
|
||||
|
||||
export async function logoutFromWorker() {
|
||||
// wait for a while because other requests (eg: logout) may be in the flight
|
||||
await sleep(5000);
|
||||
window.location.href = `${appSubUrl}/`;
|
||||
}
|
|
@ -8,7 +8,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
|
|||
if (!this.tippyContent) {
|
||||
const div = document.createElement('div');
|
||||
div.classList.add('tippy-target');
|
||||
div.tabIndex = '-1'; // for initial focus, programmatic focus only
|
||||
div.tabIndex = -1; // for initial focus, programmatic focus only
|
||||
div.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Tab') {
|
||||
const items = this.tippyContent.querySelectorAll('[role="menuitem"]');
|
||||
|
@ -60,21 +60,35 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
|
|||
this.tippyContent = div;
|
||||
}
|
||||
|
||||
const itemFlexSpace = this.menuItemsEl.querySelector('.item-flex-space');
|
||||
|
||||
// move items in tippy back into the menu items for subsequent measurement
|
||||
for (const item of this.tippyItems || []) {
|
||||
this.menuItemsEl.append(item);
|
||||
if (!itemFlexSpace || item.getAttribute('data-after-flex-space')) {
|
||||
this.menuItemsEl.append(item);
|
||||
} else {
|
||||
itemFlexSpace.insertAdjacentElement('beforebegin', item);
|
||||
}
|
||||
}
|
||||
|
||||
// measure which items are partially outside the element and move them into the button menu
|
||||
itemFlexSpace?.style.setProperty('display', 'none', 'important');
|
||||
this.tippyItems = [];
|
||||
const menuRight = this.offsetLeft + this.offsetWidth;
|
||||
const menuItems = this.menuItemsEl.querySelectorAll('.item');
|
||||
const menuItems = this.menuItemsEl.querySelectorAll('.item, .item-flex-space');
|
||||
let afterFlexSpace = false;
|
||||
for (const item of menuItems) {
|
||||
if (item.classList.contains('item-flex-space')) {
|
||||
afterFlexSpace = true;
|
||||
continue;
|
||||
}
|
||||
if (afterFlexSpace) item.setAttribute('data-after-flex-space', 'true');
|
||||
const itemRight = item.offsetLeft + item.offsetWidth;
|
||||
if (menuRight - itemRight < 38) { // roughly the width of .overflow-menu-button
|
||||
if (menuRight - itemRight < 38) { // roughly the width of .overflow-menu-button with some extra space
|
||||
this.tippyItems.push(item);
|
||||
}
|
||||
}
|
||||
itemFlexSpace?.style.removeProperty('display');
|
||||
|
||||
// if there are no overflown items, remove any previously created button
|
||||
if (!this.tippyItems?.length) {
|
||||
|
@ -105,7 +119,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
|
|||
|
||||
// create button initially
|
||||
const btn = document.createElement('button');
|
||||
btn.classList.add('overflow-menu-button', 'btn', 'tw-px-2', 'hover:tw-text-text-dark');
|
||||
btn.classList.add('overflow-menu-button');
|
||||
btn.setAttribute('aria-label', window.config.i18n.more_items);
|
||||
btn.innerHTML = octiconKebabHorizontal;
|
||||
this.append(btn);
|
||||
|
@ -117,6 +131,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
|
|||
interactive: true,
|
||||
placement: 'bottom-end',
|
||||
role: 'menu',
|
||||
theme: 'menu',
|
||||
content: this.tippyContent,
|
||||
onShow: () => { // FIXME: onShown doesn't work (never be called)
|
||||
setTimeout(() => {
|
||||
|
|
Loading…
Reference in New Issue