Merge branch 'main' into lunny/automerge_support_delete_branch

This commit is contained in:
Lunny Xiao 2024-11-08 10:36:47 -08:00
commit c879891fe1
39 changed files with 855 additions and 791 deletions

View File

@ -1443,11 +1443,11 @@ func ViewIssue(ctx *context.Context) {
}
if issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) {
ctx.Data["IssueType"] = "pulls"
ctx.Data["IssueDependencySearchType"] = "pulls"
} else if !issue.IsPull && !ctx.Repo.CanRead(unit.TypePullRequests) {
ctx.Data["IssueType"] = "issues"
ctx.Data["IssueDependencySearchType"] = "issues"
} else {
ctx.Data["IssueType"] = "all"
ctx.Data["IssueDependencySearchType"] = "all"
}
ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(unit.TypeProjects)

View File

@ -318,9 +318,10 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo
}
releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
RepoID: repo.ID,
TagNames: tags,
IncludeTags: true,
RepoID: repo.ID,
TagNames: tags,
IncludeDrafts: true,
IncludeTags: true,
})
if err != nil {
return fmt.Errorf("db.Find[repo_model.Release]: %w", err)
@ -407,13 +408,17 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo
newReleases = append(newReleases, rel)
} else {
rel.Title = parts[0]
rel.Note = note
rel.Sha1 = commit.ID.String()
rel.CreatedUnix = timeutil.TimeStamp(createdAt.Unix())
rel.NumCommits = commitsCount
if rel.IsTag && author != nil {
rel.PublisherID = author.ID
if rel.IsTag {
rel.Title = parts[0]
rel.Note = note
if author != nil {
rel.PublisherID = author.ID
}
} else {
rel.IsDraft = false
}
if err = repo_model.UpdateRelease(ctx, rel); err != nil {
return fmt.Errorf("Update: %w", err)

View File

@ -156,7 +156,7 @@ func (d discordConvertor) Push(p *api.PushPayload) (DiscordPayload, error) {
// for each commit, generate attachment text
for i, commit := range p.Commits {
// limit the commit message display to just the summary, otherwise it would be hard to read
message := strings.TrimRight(strings.SplitN(commit.Message, "\n", 1)[0], "\r")
message := strings.TrimRight(strings.SplitN(commit.Message, "\n", 2)[0], "\r")
// a limit of 50 is set because GitHub does the same
if utf8.RuneCountInString(message) > 50 {

View File

@ -80,12 +80,26 @@ func TestDiscordPayload(t *testing.T) {
assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
})
t.Run("PushWithLongCommitMessage", func(t *testing.T) {
t.Run("PushWithMultilineCommitMessage", func(t *testing.T) {
p := pushTestMultilineCommitMessagePayload()
pl, err := dc.Push(p)
require.NoError(t, err)
assert.Len(t, pl.Embeds, 1)
assert.Equal(t, "[test/repo:test] 2 new commits", pl.Embeds[0].Title)
assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) chore: This is a commit summary - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) chore: This is a commit summary - user1", pl.Embeds[0].Description)
assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
})
t.Run("PushWithLongCommitSummary", func(t *testing.T) {
p := pushTestPayloadWithCommitMessage("This is a commit summary ⚠️⚠️⚠️⚠️ containing 你好 ⚠️⚠️️\n\nThis is the message body")
pl, err := dc.Push(p)
require.NoError(t, err)
assert.Len(t, pl.Embeds, 1)
assert.Equal(t, "[test/repo:test] 2 new commits", pl.Embeds[0].Title)
assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) This is a commit summary ⚠️⚠️⚠️⚠️ containing 你好... - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) This is a commit summary ⚠️⚠️⚠️⚠️ containing 你好... - user1", pl.Embeds[0].Description)

View File

@ -68,7 +68,7 @@ func pushTestPayload() *api.PushPayload {
}
func pushTestMultilineCommitMessagePayload() *api.PushPayload {
return pushTestPayloadWithCommitMessage("This is a commit summary ⚠️⚠️⚠️⚠️ containing 你好 ⚠️⚠️️\n\nThis is the message body.")
return pushTestPayloadWithCommitMessage("chore: This is a commit summary\n\nThis is a commit description.")
}
func pushTestPayloadWithCommitMessage(message string) *api.PushPayload {

View File

@ -17,12 +17,12 @@
{{end}}
</div>
<div class="right-links" role="group" aria-label="{{ctx.Locale.Tr "aria.footer.links"}}">
<div class="ui dropdown upward language">
<div class="ui dropdown upward">
<span class="flex-text-inline">{{svg "octicon-globe" 14}} {{ctx.Locale.LangName}}</span>
<div class="menu language-menu">
{{range .AllLangs}}
<a lang="{{.Lang}}" data-url="{{AppSubUrl}}/?lang={{.Lang}}" class="item {{if eq ctx.Locale.Lang .Lang}}active selected{{end}}">{{.Name}}</a>
{{end}}
{{range .AllLangs -}}
<a lang="{{.Lang}}" data-url="{{AppSubUrl}}/?lang={{.Lang}}" class="item {{if eq ctx.Locale.Lang .Lang}}selected{{end}}">{{.Name}}</a>
{{end -}}
</div>
</div>
<a href="{{AssetUrlPrefix}}/licenses.txt">{{ctx.Locale.Tr "licenses"}}</a>

View File

@ -44,6 +44,5 @@
</div>
</div>
</div>
<div class="divider"></div>
{{end}}

View File

@ -0,0 +1,13 @@
{{if and .Issue.IsPull .IsIssuePoster (not .Issue.IsClosed) .Issue.PullRequest.HeadRepo}}
{{if and (not (eq .Issue.PullRequest.HeadRepo.FullName .Issue.PullRequest.BaseRepo.FullName)) .CanWriteToHeadRepo}}
<div class="divider"></div>
<div class="ui checkbox loading-icon-2px" id="allow-edits-from-maintainers"
data-url="{{.Issue.Link}}"
data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers_desc"}}"
data-prompt-error="{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers_err"}}"
>
<label><strong>{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers"}}</strong></label>
<input type="checkbox" {{if .Issue.PullRequest.AllowMaintainerEdit}}checked{{end}}>
</div>
{{end}}
{{end}}

View File

@ -0,0 +1,46 @@
<div class="divider"></div>
<input id="assignee_id" name="assignee_id" type="hidden" value="{{.assignee_id}}">
<div class="ui {{if or (not .HasIssuesOrPullsWritePermission) .Repository.IsArchived}}disabled{{end}} floating jump select-assignees-modify dropdown">
<a class="text muted flex-text-block">
<strong>{{ctx.Locale.Tr "repo.issues.new.assignees"}}</strong>
{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
{{svg "octicon-gear" 16 "tw-ml-1"}}
{{end}}
</a>
<div class="filter menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/assignee">
<div class="ui icon search input">
<i class="icon">{{svg "octicon-search" 16}}</i>
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_assignees"}}">
</div>
<div class="no-select item">{{ctx.Locale.Tr "repo.issues.new.clear_assignees"}}</div>
{{range .Assignees}}
{{$AssigneeID := .ID}}
<a class="item{{range $.Issue.Assignees}}{{if eq .ID $AssigneeID}} checked{{end}}{{end}}" href="#" data-id="{{.ID}}" data-id-selector="#assignee_{{.ID}}">
{{$checked := false}}
{{range $.Issue.Assignees}}
{{if eq .ID $AssigneeID}}
{{$checked = true}}
{{end}}
{{end}}
<span class="octicon-check {{if not $checked}}tw-invisible{{end}}">{{svg "octicon-check"}}</span>
<span class="text">
{{ctx.AvatarUtils.Avatar . 20 "tw-mr-2"}}{{template "repo/search_name" .}}
</span>
</a>
{{end}}
</div>
</div>
<div class="ui assignees list">
<span class="no-select item {{if .Issue.Assignees}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_assignees"}}</span>
<div class="selected">
{{range .Issue.Assignees}}
<div class="item">
<a class="muted sidebar-item-link" href="{{$.RepoLink}}/{{if $.Issue.IsPull}}pulls{{else}}issues{{end}}?assignee={{.ID}}">
{{ctx.AvatarUtils.Avatar . 28 "tw-mr-2"}}
{{.GetDisplayName}}
</a>
</div>
{{end}}
</div>
</div>

View File

@ -0,0 +1,29 @@
<div class="divider"></div>
<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.due_date"}}</strong></span>
<div class="ui form tw-mt-2">
{{if .Issue.DeadlineUnix}}
<div class="tw-flex tw-justify-between tw-items-center tw-gap-2">
<div class="due-date {{if .Issue.IsOverdue}}text red{{end}}" {{if .Issue.IsOverdue}}data-tooltip-content="{{ctx.Locale.Tr "repo.issues.due_date_overdue"}}"{{end}}>
{{svg "octicon-calendar"}} {{DateUtils.AbsoluteLong .Issue.DeadlineUnix}}
</div>
<div class="flex-text-block">
{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
<a class="issue-due-edit muted" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.due_date_form_edit"}}">{{svg "octicon-pencil"}}</a>
<a class="issue-due-remove muted" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.due_date_form_remove"}}">{{svg "octicon-trash"}}</a>
{{end}}
</div>
</div>
{{else}}
{{ctx.Locale.Tr "repo.issues.due_date_not_set"}}
{{end}}
{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
<form class="ui fluid action input issue-due-form form-fetch-action tw-mt-2 {{if .Issue.DeadlineUnix}}tw-hidden{{end}}"
method="post" action="{{AppSubUrl}}/{{PathEscape .Repository.Owner.Name}}/{{PathEscape .Repository.Name}}/issues/{{.Issue.Index}}/deadline"
>
{{$.CsrfTokenHtml}}
<input required type="date" name="deadline" placeholder="{{ctx.Locale.Tr "repo.issues.due_date_form"}}" {{if .Issue.DeadlineUnix}}value="{{.Issue.DeadlineUnix.FormatDate}}"{{end}}>
<button class="ui icon button">{{Iif .Issue.DeadlineUnix (svg "octicon-pencil") (svg "octicon-plus")}}</button>
</form>
{{end}}
</div>

View File

@ -0,0 +1,149 @@
{{if .Repository.IsDependenciesEnabled ctx}}
<div class="divider"></div>
<div class="ui depending">
{{if (and (not .BlockedByDependencies) (not .BlockedByDependenciesNotPermitted) (not .BlockingDependencies) (not .BlockingDependenciesNotPermitted))}}
<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.dependency.title"}}</strong></span>
<br>
<p>
{{if .Issue.IsPull}}
{{ctx.Locale.Tr "repo.issues.dependency.pr_no_dependencies"}}
{{else}}
{{ctx.Locale.Tr "repo.issues.dependency.issue_no_dependencies"}}
{{end}}
</p>
{{end}}
{{if or .BlockingDependencies .BlockingDependenciesNotPermitted}}
<span class="text" data-tooltip-content="{{if .Issue.IsPull}}{{ctx.Locale.Tr "repo.issues.dependency.pr_close_blocks"}}{{else}}{{ctx.Locale.Tr "repo.issues.dependency.issue_close_blocks"}}{{end}}">
<strong>{{ctx.Locale.Tr "repo.issues.dependency.blocks_short"}}</strong>
</span>
<div class="ui divided list">
{{range .BlockingDependencies}}
<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-items-center tw-justify-between">
<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
<a class="muted gt-ellipsis" href="{{.Issue.Link}}" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}">
#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}
</a>
<div class="text small gt-ellipsis" data-tooltip-content="{{.Repository.OwnerName}}/{{.Repository.Name}}">
{{.Repository.OwnerName}}/{{.Repository.Name}}
</div>
</div>
<div class="item-right tw-flex tw-items-center tw-m-1">
{{if and $.CanCreateIssueDependencies (not $.Repository.IsArchived)}}
<a class="delete-dependency-button ci muted" data-id="{{.Issue.ID}}" data-type="blocking" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.remove_info"}}">
{{svg "octicon-trash" 16}}
</a>
{{end}}
</div>
</div>
{{end}}
{{if .BlockingDependenciesNotPermitted}}
<div class="item tw-flex tw-items-center tw-justify-between gt-ellipsis">
<span>{{ctx.Locale.TrN (len .BlockingDependenciesNotPermitted) "repo.issues.dependency.no_permission_1" "repo.issues.dependency.no_permission_n" (len .BlockingDependenciesNotPermitted)}}</span>
</div>
{{end}}
</div>
{{end}}
{{if or .BlockedByDependencies .BlockedByDependenciesNotPermitted}}
<span class="text" data-tooltip-content="{{if .Issue.IsPull}}{{ctx.Locale.Tr "repo.issues.dependency.pr_closing_blockedby"}}{{else}}{{ctx.Locale.Tr "repo.issues.dependency.issue_closing_blockedby"}}{{end}}">
<strong>{{ctx.Locale.Tr "repo.issues.dependency.blocked_by_short"}}</strong>
</span>
<div class="ui divided list">
{{range .BlockedByDependencies}}
<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-items-center tw-justify-between">
<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
<a class="muted gt-ellipsis" href="{{.Issue.Link}}" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}">
#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}
</a>
<div class="text small gt-ellipsis" data-tooltip-content="{{.Repository.OwnerName}}/{{.Repository.Name}}">
{{.Repository.OwnerName}}/{{.Repository.Name}}
</div>
</div>
<div class="item-right tw-flex tw-items-center tw-m-1">
{{if and $.CanCreateIssueDependencies (not $.Repository.IsArchived)}}
<a class="delete-dependency-button ci muted" data-id="{{.Issue.ID}}" data-type="blockedBy" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.remove_info"}}">
{{svg "octicon-trash" 16}}
</a>
{{end}}
</div>
</div>
{{end}}
{{if $.CanCreateIssueDependencies}}
{{range .BlockedByDependenciesNotPermitted}}
<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-items-center tw-justify-between">
<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
<div class="gt-ellipsis">
<span data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.no_permission.can_remove"}}">{{svg "octicon-lock" 16}}</span>
<span class="gt-ellipsis" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}">
#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}
</span>
</div>
<div class="text small gt-ellipsis" data-tooltip-content="{{.Repository.OwnerName}}/{{.Repository.Name}}">
{{.Repository.OwnerName}}/{{.Repository.Name}}
</div>
</div>
<div class="item-right tw-flex tw-items-center tw-m-1">
{{if and $.CanCreateIssueDependencies (not $.Repository.IsArchived)}}
<a class="delete-dependency-button ci muted" data-id="{{.Issue.ID}}" data-type="blocking" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.remove_info"}}">
{{svg "octicon-trash" 16}}
</a>
{{end}}
</div>
</div>
{{end}}
{{else if .BlockedByDependenciesNotPermitted}}
<div class="item tw-flex tw-items-center tw-justify-between gt-ellipsis">
<span>{{ctx.Locale.TrN (len .BlockedByDependenciesNotPermitted) "repo.issues.dependency.no_permission_1" "repo.issues.dependency.no_permission_n" (len .BlockedByDependenciesNotPermitted)}}</span>
</div>
{{end}}
</div>
{{end}}
{{if and .CanCreateIssueDependencies (not .Repository.IsArchived)}}
<div>
<form method="post" action="{{.Issue.Link}}/dependency/add" id="addDependencyForm">
{{$.CsrfTokenHtml}}
<div class="ui fluid action input">
<div class="ui search selection dropdown" id="new-dependency-drop-list" data-issue-id="{{.Issue.ID}}">
<input name="newDependency" type="hidden">
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<input type="text" class="search">
<div class="default text">{{ctx.Locale.Tr "repo.issues.dependency.add"}}</div>
</div>
<button class="ui icon button">
{{svg "octicon-plus"}}
</button>
</div>
</form>
</div>
{{end}}
</div>
{{if and .CanCreateIssueDependencies (not .Repository.IsArchived)}}
<input type="hidden" id="crossRepoSearch" value="{{.AllowCrossRepositoryDependencies}}">
<div class="ui g-modal-confirm modal remove-dependency">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "repo.issues.dependency.remove_header"}}
</div>
<div class="content">
<form method="post" action="{{.Issue.Link}}/dependency/delete" id="removeDependencyForm">
{{$.CsrfTokenHtml}}
<input type="hidden" value="" name="removeDependencyID" id="removeDependencyID">
<input type="hidden" value="" name="dependencyType" id="dependencyType">
</form>
<p>{{if .Issue.IsPull}}
{{ctx.Locale.Tr "repo.issues.dependency.pr_remove_text"}}
{{else}}
{{ctx.Locale.Tr "repo.issues.dependency.issue_remove_text"}}
{{end}}</p>
</div>
{{$ModalButtonCancelText := ctx.Locale.Tr "repo.issues.dependency.cancel"}}
{{$ModalButtonOkText := ctx.Locale.Tr "repo.issues.dependency.remove"}}
{{template "base/modal_actions_confirm" (dict "." . "ModalButtonCancelText" $ModalButtonCancelText "ModalButtonOkText" $ModalButtonOkText)}}
</div>
{{end}}
{{end}}

View File

@ -0,0 +1,118 @@
{{if and .IsRepoAdmin (not .Repository.IsArchived)}}
<div class="divider"></div>
{{if or .PinEnabled .Issue.IsPinned}}
<form class="tw-mt-1 form-fetch-action single-button-form" method="post" {{if $.NewPinAllowed}}action="{{.Issue.Link}}/pin"{{else}}data-tooltip-content="{{ctx.Locale.Tr "repo.issues.max_pinned"}}"{{end}}>
{{$.CsrfTokenHtml}}
<button class="fluid ui button {{if not $.NewPinAllowed}}disabled{{end}}">
{{if not .Issue.IsPinned}}
{{svg "octicon-pin" 16 "tw-mr-2"}}
{{ctx.Locale.Tr "pin"}}
{{else}}
{{svg "octicon-pin-slash" 16 "tw-mr-2"}}
{{ctx.Locale.Tr "unpin"}}
{{end}}
</button>
</form>
{{end}}
<button class="tw-mt-1 fluid ui show-modal button{{if .Issue.IsLocked}} red{{end}}" data-modal="#lock">
{{if .Issue.IsLocked}}
{{svg "octicon-key"}}
{{ctx.Locale.Tr "repo.issues.unlock"}}
{{else}}
{{svg "octicon-lock"}}
{{ctx.Locale.Tr "repo.issues.lock"}}
{{end}}
</button>
<div class="ui tiny modal" id="lock">
<div class="header">
{{if .Issue.IsLocked}}
{{ctx.Locale.Tr "repo.issues.unlock.title"}}
{{else}}
{{ctx.Locale.Tr "repo.issues.lock.title"}}
{{end}}
</div>
<div class="content">
<div class="ui warning message">
{{if .Issue.IsLocked}}
{{ctx.Locale.Tr "repo.issues.unlock.notice_1"}}<br>
{{ctx.Locale.Tr "repo.issues.unlock.notice_2"}}<br>
{{else}}
{{ctx.Locale.Tr "repo.issues.lock.notice_1"}}<br>
{{ctx.Locale.Tr "repo.issues.lock.notice_2"}}<br>
{{ctx.Locale.Tr "repo.issues.lock.notice_3"}}<br>
{{end}}
</div>
<form class="ui form form-fetch-action" action="{{.Issue.Link}}{{if .Issue.IsLocked}}/unlock{{else}}/lock{{end}}"
method="post">
{{.CsrfTokenHtml}}
{{if not .Issue.IsLocked}}
<div class="field">
<strong> {{ctx.Locale.Tr "repo.issues.lock.reason"}} </strong>
</div>
<div class="field">
<div class="ui fluid dropdown selection">
<select name="reason">
<option value=""> </option>
{{range .LockReasons}}
<option value="{{.}}">{{.}}</option>
{{end}}
</select>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="default text"> </div>
<div class="menu">
{{range .LockReasons}}
<div class="item" data-value="{{.}}">{{.}}</div>
{{end}}
</div>
</div>
</div>
{{end}}
<div class="text right actions">
<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button class="ui red button">
{{if .Issue.IsLocked}}
{{ctx.Locale.Tr "repo.issues.unlock_confirm"}}
{{else}}
{{ctx.Locale.Tr "repo.issues.lock_confirm"}}
{{end}}
</button>
</div>
</form>
</div>
</div>
<button class="tw-mt-1 fluid ui show-modal button" data-modal="#sidebar-delete-issue">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "repo.issues.delete"}}
</button>
<div class="ui g-modal-confirm modal" id="sidebar-delete-issue">
<div class="header">
{{if .Issue.IsPull}}
{{ctx.Locale.Tr "repo.pulls.delete.title"}}
{{else}}
{{ctx.Locale.Tr "repo.issues.delete.title"}}
{{end}}
</div>
<div class="content">
<p>
{{if .Issue.IsPull}}
{{ctx.Locale.Tr "repo.pulls.delete.text"}}
{{else}}
{{ctx.Locale.Tr "repo.issues.delete.text"}}
{{end}}
</p>
</div>
<form action="{{.Issue.Link}}/delete" method="post">
{{.CsrfTokenHtml}}
{{template "base/modal_actions_confirm" .}}
</form>
</div>
{{end}}

View File

@ -0,0 +1,23 @@
<div class="divider"></div>
<div class="ui {{if or (not .HasIssuesOrPullsWritePermission) .Repository.IsArchived}}disabled{{end}} floating jump select-milestone dropdown">
<a class="text muted flex-text-block">
<strong>{{ctx.Locale.Tr "repo.issues.new.milestone"}}</strong>
{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
{{svg "octicon-gear" 16 "tw-ml-1"}}
{{end}}
</a>
<div class="menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/milestone">
{{template "repo/issue/milestone/select_menu" .}}
</div>
</div>
<div class="ui select-milestone list">
<span class="no-select item {{if .Issue.Milestone}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_milestone"}}</span>
<div class="selected">
{{if .Issue.Milestone}}
<a class="item muted sidebar-item-link" href="{{.RepoLink}}/milestone/{{.Issue.Milestone.ID}}">
{{svg "octicon-milestone" 18 "tw-mr-2"}}
{{.Issue.Milestone.Name}}
</a>
{{end}}
</div>
</div>

View File

@ -0,0 +1,11 @@
{{if .Participants}}
<div class="divider"></div>
<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.num_participants" .NumParticipants}}</strong></span>
<div class="ui list tw-flex tw-flex-wrap">
{{range .Participants}}
<a {{if gt .ID 0}}href="{{.HomeLink}}"{{end}} data-tooltip-content="{{.GetDisplayName}}">
{{ctx.AvatarUtils.Avatar . 28 "tw-my-0.5 tw-mr-1"}}
</a>
{{end}}
</div>
{{end}}

View File

@ -0,0 +1,53 @@
{{if .IsProjectsEnabled}}
<div class="divider"></div>
<div class="ui {{if or (not .HasIssuesOrPullsWritePermission) .Repository.IsArchived}}disabled{{end}} floating jump select-project dropdown">
<a class="text muted flex-text-block">
<strong>{{ctx.Locale.Tr "repo.issues.new.projects"}}</strong>
{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
{{svg "octicon-gear" 16 "tw-ml-1"}}
{{end}}
</a>
<div class="menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/projects">
{{if or .OpenProjects .ClosedProjects}}
<div class="ui icon search input">
<i class="icon">{{svg "octicon-search" 16}}</i>
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_projects"}}">
</div>
{{end}}
<div class="no-select item">{{ctx.Locale.Tr "repo.issues.new.clear_projects"}}</div>
{{if .OpenProjects}}
<div class="divider"></div>
<div class="header">
{{ctx.Locale.Tr "repo.issues.new.open_projects"}}
</div>
{{range .OpenProjects}}
<a class="item muted sidebar-item-link" data-id="{{.ID}}" data-href="{{.Link ctx}}">
{{svg .IconName 18 "tw-mr-2"}}{{.Title}}
</a>
{{end}}
{{end}}
{{if .ClosedProjects}}
<div class="divider"></div>
<div class="header">
{{ctx.Locale.Tr "repo.issues.new.closed_projects"}}
</div>
{{range .ClosedProjects}}
<a class="item muted sidebar-item-link" data-id="{{.ID}}" data-href="{{.Link ctx}}">
{{svg .IconName 18 "tw-mr-2"}}{{.Title}}
</a>
{{end}}
{{end}}
</div>
</div>
<div class="ui select-project list">
<span class="no-select item {{if .Issue.Project}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</span>
<div class="selected">
{{if .Issue.Project}}
<a class="item muted sidebar-item-link" href="{{.Issue.Project.Link ctx}}">
{{svg .Issue.Project.IconName 18 "tw-mr-2"}}{{.Issue.Project.Title}}
</a>
{{end}}
</div>
</div>
{{end}}

View File

@ -0,0 +1,8 @@
<div class="divider"></div>
<div class="ui equal width compact grid">
{{$issueReferenceLink := printf "%s#%d" .Issue.Repo.FullName .Issue.Index}}
<div class="row tw-items-center" data-tooltip-content="{{$issueReferenceLink}}">
<span class="text column truncate">{{ctx.Locale.Tr "repo.issues.reference_link" $issueReferenceLink}}</span>
<button class="ui two wide button column tw-p-2" data-clipboard-text="{{$issueReferenceLink}}">{{svg "octicon-copy" 14}}</button>
</div>
</div>

View File

@ -0,0 +1,116 @@
<input id="reviewer_id" name="reviewer_id" type="hidden" value="{{.reviewer_id}}">
<div class="ui {{if or (and (not .Reviewers) (not .TeamReviewers)) (not .CanChooseReviewer) .Repository.IsArchived}}disabled{{end}} floating jump select-reviewers-modify dropdown">
<a class="text tw-flex tw-items-center muted">
<strong>{{ctx.Locale.Tr "repo.issues.review.reviewers"}}</strong>
{{if and .CanChooseReviewer (not .Repository.IsArchived)}}
{{svg "octicon-gear" 16 "tw-ml-1"}}
{{end}}
</a>
<div class="filter menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/request_review">
{{if .Reviewers}}
<div class="ui icon search input">
<i class="icon">{{svg "octicon-search" 16}}</i>
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_reviewers"}}">
</div>
{{end}}
{{if .Reviewers}}
{{range .Reviewers}}
{{if .User}}
<a class="{{if not .CanChange}}ui{{end}} item {{if .Checked}}checked{{end}} {{if not .CanChange}}ban-change{{end}}" href="#" data-id="{{.ItemID}}" data-id-selector="#review_request_{{.ItemID}}" {{if not .CanChange}} data-tooltip-content="{{ctx.Locale.Tr "repo.issues.remove_request_review_block"}}"{{end}}>
<span class="octicon-check {{if not .Checked}}tw-invisible{{end}}">{{svg "octicon-check"}}</span>
<span class="text">
{{ctx.AvatarUtils.Avatar .User 28 "tw-mr-2"}}{{template "repo/search_name" .User}}
</span>
</a>
{{end}}
{{end}}
{{end}}
{{if .TeamReviewers}}
{{if .Reviewers}}
<div class="divider"></div>
{{end}}
{{range .TeamReviewers}}
{{if .Team}}
<a class="{{if not .CanChange}}ui{{end}} item {{if .Checked}}checked{{end}} {{if not .CanChange}}ban-change{{end}}" href="#" data-id="{{.ItemID}}" data-id-selector="#review_request_team_{{.Team.ID}}" {{if not .CanChange}} data-tooltip-content="{{ctx.Locale.Tr "repo.issues.remove_request_review_block"}}"{{end}}>
<span class="octicon-check {{if not .Checked}}tw-invisible{{end}}">{{svg "octicon-check" 16}}</span>
<span class="text">
{{svg "octicon-people" 16 "tw-ml-4 tw-mr-1"}}{{$.Issue.Repo.OwnerName}}/{{.Team.Name}}
</span>
</a>
{{end}}
{{end}}
{{end}}
</div>
</div>
<div class="ui assignees list">
<span class="no-select item {{if or .OriginalReviews .PullReviewers}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_reviewers"}}</span>
<div class="selected">
{{range .PullReviewers}}
<div class="item tw-flex tw-items-center tw-py-2">
<div class="tw-flex tw-items-center tw-flex-1">
{{if .User}}
<a class="muted sidebar-item-link" href="{{.User.HomeLink}}">{{ctx.AvatarUtils.Avatar .User 20 "tw-mr-2"}}{{.User.GetDisplayName}}</a>
{{else if .Team}}
<span class="text">{{svg "octicon-people" 20 "tw-mr-2"}}{{$.Issue.Repo.OwnerName}}/{{.Team.Name}}</span>
{{end}}
</div>
<div class="tw-flex tw-items-center tw-gap-2">
{{if (and $.Permission.IsAdmin (or (eq .Review.Type 1) (eq .Review.Type 3)) (not $.Issue.IsClosed) (not $.Issue.PullRequest.HasMerged))}}
<a href="#" class="ui muted icon tw-flex tw-items-center show-modal" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dismiss_review"}}" data-modal="#dismiss-review-modal-{{.Review.ID}}">
{{svg "octicon-x" 20}}
</a>
<div class="ui small modal" id="dismiss-review-modal-{{.Review.ID}}">
<div class="header">
{{ctx.Locale.Tr "repo.issues.dismiss_review"}}
</div>
<div class="content">
<div class="ui warning message">
{{ctx.Locale.Tr "repo.issues.dismiss_review_warning"}}
</div>
<form class="ui form dismiss-review-form" id="dismiss-review-{{.Review.ID}}" action="{{$.RepoLink}}/issues/dismiss_review" method="post">
{{$.CsrfTokenHtml}}
<input type="hidden" name="review_id" value="{{.Review.ID}}">
<div class="field">
<label for="message">{{ctx.Locale.Tr "action.review_dismissed_reason"}}</label>
<input id="message" name="message">
</div>
<div class="text right actions">
<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button class="ui red button" type="submit">{{ctx.Locale.Tr "ok"}}</button>
</div>
</form>
</div>
</div>
{{end}}
{{if .Review.Stale}}
<span data-tooltip-content="{{ctx.Locale.Tr "repo.issues.is_stale"}}">
{{svg "octicon-hourglass" 16}}
</span>
{{end}}
{{if and .CanChange (or .Checked (and (not $.Issue.IsClosed) (not $.Issue.PullRequest.HasMerged)))}}
<a href="#" class="ui muted icon re-request-review{{if .Checked}} checked{{end}}" data-tooltip-content="{{if .Checked}}{{ctx.Locale.Tr "repo.issues.remove_request_review"}}{{else}}{{ctx.Locale.Tr "repo.issues.re_request_review"}}{{end}}" data-issue-id="{{$.Issue.ID}}" data-id="{{.ItemID}}" data-update-url="{{$.RepoLink}}/issues/request_review">{{svg (Iif .Checked "octicon-trash" "octicon-sync")}}</a>
{{end}}
<span {{if .Review.TooltipContent}}data-tooltip-content="{{ctx.Locale.Tr .Review.TooltipContent}}"{{end}}>
{{svg (printf "octicon-%s" .Review.Type.Icon) 16 (printf "text %s" (.Review.HTMLTypeColorName))}}
</span>
</div>
</div>
{{end}}
{{range .OriginalReviews}}
<div class="item tw-flex tw-items-center tw-py-2">
<div class="tw-flex tw-items-center tw-flex-1">
<a class="muted" href="{{$.Repository.OriginalURL}}" data-tooltip-content="{{ctx.Locale.Tr "repo.migrated_from_fake" $.Repository.GetOriginalURLHostname}}">
{{svg (MigrationIcon $.Repository.GetOriginalURLHostname) 20 "tw-mr-2"}}
{{.OriginalAuthor}}
</a>
</div>
<div class="tw-flex tw-items-center tw-gap-2">
<span {{if .TooltipContent}}data-tooltip-content="{{ctx.Locale.Tr .TooltipContent}}"{{end}}>
{{svg (printf "octicon-%s" .Type.Icon) 16 (printf "text %s" (.HTMLTypeColorName))}}
</span>
</div>
</div>
{{end}}
</div>
</div>

View File

@ -0,0 +1,75 @@
{{if .Repository.IsTimetrackerEnabled ctx}}
{{if and .CanUseTimetracker (not .Repository.IsArchived)}}
<div class="divider"></div>
<div class="ui timetrack">
<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.tracker"}}</strong></span>
<div class="tw-mt-2">
<form method="post" action="{{.Issue.Link}}/times/stopwatch/toggle" id="toggle_stopwatch_form">
{{$.CsrfTokenHtml}}
</form>
<form method="post" action="{{.Issue.Link}}/times/stopwatch/cancel" id="cancel_stopwatch_form">
{{$.CsrfTokenHtml}}
</form>
{{if $.IsStopwatchRunning}}
<button class="ui fluid button issue-stop-time">
{{svg "octicon-stopwatch" 16 "tw-mr-2"}}
{{ctx.Locale.Tr "repo.issues.stop_tracking"}}
</button>
<button class="ui fluid button issue-cancel-time tw-mt-2">
{{svg "octicon-trash" 16 "tw-mr-2"}}
{{ctx.Locale.Tr "repo.issues.cancel_tracking"}}
</button>
{{else}}
{{if .HasUserStopwatch}}
<div class="ui warning message">
{{ctx.Locale.Tr "repo.issues.tracking_already_started" .OtherStopwatchURL}}
</div>
{{end}}
<button class="ui fluid button issue-start-time" data-tooltip-content='{{ctx.Locale.Tr "repo.issues.start_tracking"}}'>
{{svg "octicon-stopwatch" 16 "tw-mr-2"}}
{{ctx.Locale.Tr "repo.issues.start_tracking_short"}}
</button>
<div class="ui mini modal issue-start-time-modal">
<div class="header">{{ctx.Locale.Tr "repo.issues.add_time"}}</div>
<div class="content">
<form method="post" id="add_time_manual_form" action="{{.Issue.Link}}/times/add" class="ui input fluid tw-gap-2">
{{$.CsrfTokenHtml}}
<input placeholder='{{ctx.Locale.Tr "repo.issues.add_time_hours"}}' type="number" name="hours">
<input placeholder='{{ctx.Locale.Tr "repo.issues.add_time_minutes"}}' type="number" name="minutes" class="ui compact">
</form>
</div>
<div class="actions">
<button class="ui primary approve button">{{ctx.Locale.Tr "repo.issues.add_time_short"}}</button>
<button class="ui cancel button">{{ctx.Locale.Tr "repo.issues.add_time_cancel"}}</button>
</div>
</div>
<button class="ui fluid button issue-add-time tw-mt-2" data-tooltip-content='{{ctx.Locale.Tr "repo.issues.add_time"}}'>
{{svg "octicon-plus" 16 "tw-mr-2"}}
{{ctx.Locale.Tr "repo.issues.add_time_short"}}
</button>
{{end}}
</div>
</div>
{{end}}
{{if .WorkingUsers}}
<div class="divider"></div>
<div class="ui comments">
<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.time_spent_from_all_authors" ($.Issue.TotalTrackedTime | Sec2Time)}}</strong></span>
<div>
{{range $user, $trackedtime := .WorkingUsers}}
<div class="comment tw-mt-2">
<a class="avatar">
{{ctx.AvatarUtils.Avatar $user}}
</a>
<div class="content">
{{template "shared/user/authorlink" $user}}
<div class="text">
{{$trackedtime|Sec2Time}}
</div>
</div>
</div>
{{end}}
</div>
</div>
{{end}}
{{end}}

View File

@ -0,0 +1,9 @@
{{if and $.IssueWatch (not .Repository.IsArchived)}}
<div class="divider"></div>
<div class="ui watching">
<span class="text"><strong>{{ctx.Locale.Tr "notification.notifications"}}</strong></span>
<div class="tw-mt-2">
{{template "repo/issue/view_content/watching" .}}
</div>
</div>
{{end}}

View File

@ -0,0 +1,7 @@
{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .HasMerged) (not .Issue.IsClosed) (not .IsPullWorkInProgress)}}
<div class="toggle-wip" data-title="{{.Issue.Title}}" data-wip-prefix="{{index .PullRequestWorkInProgressPrefixes 0}}" data-update-url="{{.Issue.Link}}/title">
<a class="muted">
{{ctx.Locale.Tr "repo.pulls.still_in_progress"}} {{ctx.Locale.Tr "repo.pulls.add_prefix" (index .PullRequestWorkInProgressPrefixes 0)}}
</a>
</div>
{{end}}

View File

@ -1,11 +1,4 @@
<div class="issue-content">
<!-- I know, there is probably a better way to do this (moved from sidebar.tmpl, original author: 6543 @ 2021-02-28) -->
<!-- Agree, there should be a better way, eg: introduce window.config.pageData (original author: wxiaoguang @ 2021-09-05) -->
<input type="hidden" id="repolink" value="{{$.RepoRelPath}}">
<input type="hidden" id="repoId" value="{{.Repository.ID}}">
<input type="hidden" id="issueIndex" value="{{.Issue.Index}}">
<input type="hidden" id="type" value="{{.IssueType}}">
{{$createdStr:= DateUtils.TimeSince .Issue.CreatedUnix}}
<div class="issue-content-left comment-list prevent-before-timeline">
<div class="ui timeline">

View File

@ -1,682 +1,24 @@
<div class="issue-content-right ui segment">
{{template "repo/issue/branch_selector_field" .}}
{{if .Issue.IsPull}}
<input id="reviewer_id" name="reviewer_id" type="hidden" value="{{.reviewer_id}}">
<div class="ui {{if or (and (not .Reviewers) (not .TeamReviewers)) (not .CanChooseReviewer) .Repository.IsArchived}}disabled{{end}} floating jump select-reviewers-modify dropdown">
<a class="text tw-flex tw-items-center muted">
<strong>{{ctx.Locale.Tr "repo.issues.review.reviewers"}}</strong>
{{if and .CanChooseReviewer (not .Repository.IsArchived)}}
{{svg "octicon-gear" 16 "tw-ml-1"}}
{{end}}
</a>
<div class="filter menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/request_review">
{{if .Reviewers}}
<div class="ui icon search input">
<i class="icon">{{svg "octicon-search" 16}}</i>
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_reviewers"}}">
</div>
{{end}}
{{if .Reviewers}}
{{range .Reviewers}}
{{if .User}}
<a class="{{if not .CanChange}}ui{{end}} item {{if .Checked}}checked{{end}} {{if not .CanChange}}ban-change{{end}}" href="#" data-id="{{.ItemID}}" data-id-selector="#review_request_{{.ItemID}}" {{if not .CanChange}} data-tooltip-content="{{ctx.Locale.Tr "repo.issues.remove_request_review_block"}}"{{end}}>
<span class="octicon-check {{if not .Checked}}tw-invisible{{end}}">{{svg "octicon-check"}}</span>
<span class="text">
{{ctx.AvatarUtils.Avatar .User 28 "tw-mr-2"}}{{template "repo/search_name" .User}}
</span>
</a>
{{end}}
{{end}}
{{end}}
{{if .TeamReviewers}}
{{if .Reviewers}}
<div class="divider"></div>
{{end}}
{{range .TeamReviewers}}
{{if .Team}}
<a class="{{if not .CanChange}}ui{{end}} item {{if .Checked}}checked{{end}} {{if not .CanChange}}ban-change{{end}}" href="#" data-id="{{.ItemID}}" data-id-selector="#review_request_team_{{.Team.ID}}" {{if not .CanChange}} data-tooltip-content="{{ctx.Locale.Tr "repo.issues.remove_request_review_block"}}"{{end}}>
<span class="octicon-check {{if not .Checked}}tw-invisible{{end}}">{{svg "octicon-check" 16}}</span>
<span class="text">
{{svg "octicon-people" 16 "tw-ml-4 tw-mr-1"}}{{$.Issue.Repo.OwnerName}}/{{.Team.Name}}
</span>
</a>
{{end}}
{{end}}
{{end}}
</div>
</div>
{{template "repo/issue/branch_selector_field" $}}
<div class="ui assignees list">
<span class="no-select item {{if or .OriginalReviews .PullReviewers}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_reviewers"}}</span>
<div class="selected">
{{range .PullReviewers}}
<div class="item tw-flex tw-items-center tw-py-2">
<div class="tw-flex tw-items-center tw-flex-1">
{{if .User}}
<a class="muted sidebar-item-link" href="{{.User.HomeLink}}">{{ctx.AvatarUtils.Avatar .User 20 "tw-mr-2"}}{{.User.GetDisplayName}}</a>
{{else if .Team}}
<span class="text">{{svg "octicon-people" 20 "tw-mr-2"}}{{$.Issue.Repo.OwnerName}}/{{.Team.Name}}</span>
{{end}}
</div>
<div class="tw-flex tw-items-center tw-gap-2">
{{if (and $.Permission.IsAdmin (or (eq .Review.Type 1) (eq .Review.Type 3)) (not $.Issue.IsClosed) (not $.Issue.PullRequest.HasMerged))}}
<a href="#" class="ui muted icon tw-flex tw-items-center show-modal" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dismiss_review"}}" data-modal="#dismiss-review-modal-{{.Review.ID}}">
{{svg "octicon-x" 20}}
</a>
<div class="ui small modal" id="dismiss-review-modal-{{.Review.ID}}">
<div class="header">
{{ctx.Locale.Tr "repo.issues.dismiss_review"}}
</div>
<div class="content">
<div class="ui warning message">
{{ctx.Locale.Tr "repo.issues.dismiss_review_warning"}}
</div>
<form class="ui form dismiss-review-form" id="dismiss-review-{{.Review.ID}}" action="{{$.RepoLink}}/issues/dismiss_review" method="post">
{{$.CsrfTokenHtml}}
<input type="hidden" name="review_id" value="{{.Review.ID}}">
<div class="field">
<label for="message">{{ctx.Locale.Tr "action.review_dismissed_reason"}}</label>
<input id="message" name="message">
</div>
<div class="text right actions">
<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button class="ui red button" type="submit">{{ctx.Locale.Tr "ok"}}</button>
</div>
</form>
</div>
</div>
{{end}}
{{if .Review.Stale}}
<span data-tooltip-content="{{ctx.Locale.Tr "repo.issues.is_stale"}}">
{{svg "octicon-hourglass" 16}}
</span>
{{end}}
{{if and .CanChange (or .Checked (and (not $.Issue.IsClosed) (not $.Issue.PullRequest.HasMerged)))}}
<a href="#" class="ui muted icon re-request-review{{if .Checked}} checked{{end}}" data-tooltip-content="{{if .Checked}}{{ctx.Locale.Tr "repo.issues.remove_request_review"}}{{else}}{{ctx.Locale.Tr "repo.issues.re_request_review"}}{{end}}" data-issue-id="{{$.Issue.ID}}" data-id="{{.ItemID}}" data-update-url="{{$.RepoLink}}/issues/request_review">{{svg (Iif .Checked "octicon-trash" "octicon-sync")}}</a>
{{end}}
<span {{if .Review.TooltipContent}}data-tooltip-content="{{ctx.Locale.Tr .Review.TooltipContent}}"{{end}}>
{{svg (printf "octicon-%s" .Review.Type.Icon) 16 (printf "text %s" (.Review.HTMLTypeColorName))}}
</span>
</div>
</div>
{{end}}
{{range .OriginalReviews}}
<div class="item tw-flex tw-items-center tw-py-2">
<div class="tw-flex tw-items-center tw-flex-1">
<a class="muted" href="{{$.Repository.OriginalURL}}" data-tooltip-content="{{ctx.Locale.Tr "repo.migrated_from_fake" $.Repository.GetOriginalURLHostname}}">
{{svg (MigrationIcon $.Repository.GetOriginalURLHostname) 20 "tw-mr-2"}}
{{.OriginalAuthor}}
</a>
</div>
<div class="tw-flex tw-items-center tw-gap-2">
<span {{if .TooltipContent}}data-tooltip-content="{{ctx.Locale.Tr .TooltipContent}}"{{end}}>
{{svg (printf "octicon-%s" .Type.Icon) 16 (printf "text %s" (.HTMLTypeColorName))}}
</span>
</div>
</div>
{{end}}
</div>
</div>
{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .HasMerged) (not .Issue.IsClosed) (not .IsPullWorkInProgress)}}
<div class="toggle-wip" data-title="{{.Issue.Title}}" data-wip-prefix="{{index .PullRequestWorkInProgressPrefixes 0}}" data-update-url="{{.Issue.Link}}/title">
<a class="muted">
{{ctx.Locale.Tr "repo.pulls.still_in_progress"}} {{ctx.Locale.Tr "repo.pulls.add_prefix" (index .PullRequestWorkInProgressPrefixes 0)}}
</a>
</div>
{{end}}
{{if .Issue.IsPull}}
{{template "repo/issue/sidebar/reviewer_list" $}}
{{template "repo/issue/sidebar/wip_switch" $}}
<div class="divider"></div>
{{end}}
{{template "repo/issue/labels/labels_selector_field" .}}
{{template "repo/issue/labels/labels_selector_field" $}}
{{template "repo/issue/labels/labels_sidebar" dict "root" $}}
<div class="divider"></div>
<div class="ui {{if or (not .HasIssuesOrPullsWritePermission) .Repository.IsArchived}}disabled{{end}} floating jump select-milestone dropdown">
<a class="text muted flex-text-block">
<strong>{{ctx.Locale.Tr "repo.issues.new.milestone"}}</strong>
{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
{{svg "octicon-gear" 16 "tw-ml-1"}}
{{end}}
</a>
<div class="menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/milestone">
{{template "repo/issue/milestone/select_menu" .}}
</div>
</div>
<div class="ui select-milestone list">
<span class="no-select item {{if .Issue.Milestone}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_milestone"}}</span>
<div class="selected">
{{if .Issue.Milestone}}
<a class="item muted sidebar-item-link" href="{{.RepoLink}}/milestone/{{.Issue.Milestone.ID}}">
{{svg "octicon-milestone" 18 "tw-mr-2"}}
{{.Issue.Milestone.Name}}
</a>
{{end}}
</div>
</div>
{{if .IsProjectsEnabled}}
<div class="divider"></div>
<div class="ui {{if or (not .HasIssuesOrPullsWritePermission) .Repository.IsArchived}}disabled{{end}} floating jump select-project dropdown">
<a class="text muted flex-text-block">
<strong>{{ctx.Locale.Tr "repo.issues.new.projects"}}</strong>
{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
{{svg "octicon-gear" 16 "tw-ml-1"}}
{{end}}
</a>
<div class="menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/projects">
{{if or .OpenProjects .ClosedProjects}}
<div class="ui icon search input">
<i class="icon">{{svg "octicon-search" 16}}</i>
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_projects"}}">
</div>
{{end}}
<div class="no-select item">{{ctx.Locale.Tr "repo.issues.new.clear_projects"}}</div>
{{if .OpenProjects}}
<div class="divider"></div>
<div class="header">
{{ctx.Locale.Tr "repo.issues.new.open_projects"}}
</div>
{{range .OpenProjects}}
<a class="item muted sidebar-item-link" data-id="{{.ID}}" data-href="{{.Link ctx}}">
{{svg .IconName 18 "tw-mr-2"}}{{.Title}}
</a>
{{end}}
{{end}}
{{if .ClosedProjects}}
<div class="divider"></div>
<div class="header">
{{ctx.Locale.Tr "repo.issues.new.closed_projects"}}
</div>
{{range .ClosedProjects}}
<a class="item muted sidebar-item-link" data-id="{{.ID}}" data-href="{{.Link ctx}}">
{{svg .IconName 18 "tw-mr-2"}}{{.Title}}
</a>
{{end}}
{{end}}
</div>
</div>
<div class="ui select-project list">
<span class="no-select item {{if .Issue.Project}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</span>
<div class="selected">
{{if .Issue.Project}}
<a class="item muted sidebar-item-link" href="{{.Issue.Project.Link ctx}}">
{{svg .Issue.Project.IconName 18 "tw-mr-2"}}{{.Issue.Project.Title}}
</a>
{{end}}
</div>
</div>
{{end}}
<div class="divider"></div>
<input id="assignee_id" name="assignee_id" type="hidden" value="{{.assignee_id}}">
<div class="ui {{if or (not .HasIssuesOrPullsWritePermission) .Repository.IsArchived}}disabled{{end}} floating jump select-assignees-modify dropdown">
<a class="text muted flex-text-block">
<strong>{{ctx.Locale.Tr "repo.issues.new.assignees"}}</strong>
{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
{{svg "octicon-gear" 16 "tw-ml-1"}}
{{end}}
</a>
<div class="filter menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/assignee">
<div class="ui icon search input">
<i class="icon">{{svg "octicon-search" 16}}</i>
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_assignees"}}">
</div>
<div class="no-select item">{{ctx.Locale.Tr "repo.issues.new.clear_assignees"}}</div>
{{range .Assignees}}
{{$AssigneeID := .ID}}
<a class="item{{range $.Issue.Assignees}}{{if eq .ID $AssigneeID}} checked{{end}}{{end}}" href="#" data-id="{{.ID}}" data-id-selector="#assignee_{{.ID}}">
{{$checked := false}}
{{range $.Issue.Assignees}}
{{if eq .ID $AssigneeID}}
{{$checked = true}}
{{end}}
{{end}}
<span class="octicon-check {{if not $checked}}tw-invisible{{end}}">{{svg "octicon-check"}}</span>
<span class="text">
{{ctx.AvatarUtils.Avatar . 20 "tw-mr-2"}}{{template "repo/search_name" .}}
</span>
</a>
{{end}}
</div>
</div>
<div class="ui assignees list">
<span class="no-select item {{if .Issue.Assignees}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_assignees"}}</span>
<div class="selected">
{{range .Issue.Assignees}}
<div class="item">
<a class="muted sidebar-item-link" href="{{$.RepoLink}}/{{if $.Issue.IsPull}}pulls{{else}}issues{{end}}?assignee={{.ID}}">
{{ctx.AvatarUtils.Avatar . 28 "tw-mr-2"}}
{{.GetDisplayName}}
</a>
</div>
{{end}}
</div>
</div>
<div class="divider"></div>
{{if .Participants}}
<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.num_participants" .NumParticipants}}</strong></span>
<div class="ui list tw-flex tw-flex-wrap">
{{range .Participants}}
<a {{if gt .ID 0}}href="{{.HomeLink}}"{{end}} data-tooltip-content="{{.GetDisplayName}}">
{{ctx.AvatarUtils.Avatar . 28 "tw-my-0.5 tw-mr-1"}}
</a>
{{end}}
</div>
{{end}}
{{if and $.IssueWatch (not .Repository.IsArchived)}}
<div class="divider"></div>
<div class="ui watching">
<span class="text"><strong>{{ctx.Locale.Tr "notification.notifications"}}</strong></span>
<div class="tw-mt-2">
{{template "repo/issue/view_content/watching" .}}
</div>
</div>
{{end}}
{{if .Repository.IsTimetrackerEnabled ctx}}
{{if and .CanUseTimetracker (not .Repository.IsArchived)}}
<div class="divider"></div>
<div class="ui timetrack">
<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.tracker"}}</strong></span>
<div class="tw-mt-2">
<form method="post" action="{{.Issue.Link}}/times/stopwatch/toggle" id="toggle_stopwatch_form">
{{$.CsrfTokenHtml}}
</form>
<form method="post" action="{{.Issue.Link}}/times/stopwatch/cancel" id="cancel_stopwatch_form">
{{$.CsrfTokenHtml}}
</form>
{{if $.IsStopwatchRunning}}
<button class="ui fluid button issue-stop-time">
{{svg "octicon-stopwatch" 16 "tw-mr-2"}}
{{ctx.Locale.Tr "repo.issues.stop_tracking"}}
</button>
<button class="ui fluid button issue-cancel-time tw-mt-2">
{{svg "octicon-trash" 16 "tw-mr-2"}}
{{ctx.Locale.Tr "repo.issues.cancel_tracking"}}
</button>
{{else}}
{{if .HasUserStopwatch}}
<div class="ui warning message">
{{ctx.Locale.Tr "repo.issues.tracking_already_started" .OtherStopwatchURL}}
</div>
{{end}}
<button class="ui fluid button issue-start-time" data-tooltip-content='{{ctx.Locale.Tr "repo.issues.start_tracking"}}'>
{{svg "octicon-stopwatch" 16 "tw-mr-2"}}
{{ctx.Locale.Tr "repo.issues.start_tracking_short"}}
</button>
<div class="ui mini modal issue-start-time-modal">
<div class="header">{{ctx.Locale.Tr "repo.issues.add_time"}}</div>
<div class="content">
<form method="post" id="add_time_manual_form" action="{{.Issue.Link}}/times/add" class="ui input fluid tw-gap-2">
{{$.CsrfTokenHtml}}
<input placeholder='{{ctx.Locale.Tr "repo.issues.add_time_hours"}}' type="number" name="hours">
<input placeholder='{{ctx.Locale.Tr "repo.issues.add_time_minutes"}}' type="number" name="minutes" class="ui compact">
</form>
</div>
<div class="actions">
<button class="ui primary approve button">{{ctx.Locale.Tr "repo.issues.add_time_short"}}</button>
<button class="ui cancel button">{{ctx.Locale.Tr "repo.issues.add_time_cancel"}}</button>
</div>
</div>
<button class="ui fluid button issue-add-time tw-mt-2" data-tooltip-content='{{ctx.Locale.Tr "repo.issues.add_time"}}'>
{{svg "octicon-plus" 16 "tw-mr-2"}}
{{ctx.Locale.Tr "repo.issues.add_time_short"}}
</button>
{{end}}
</div>
</div>
{{end}}
{{if .WorkingUsers}}
<div class="divider"></div>
<div class="ui comments">
<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.time_spent_from_all_authors" ($.Issue.TotalTrackedTime | Sec2Time)}}</strong></span>
<div>
{{range $user, $trackedtime := .WorkingUsers}}
<div class="comment tw-mt-2">
<a class="avatar">
{{ctx.AvatarUtils.Avatar $user}}
</a>
<div class="content">
{{template "shared/user/authorlink" $user}}
<div class="text">
{{$trackedtime|Sec2Time}}
</div>
</div>
</div>
{{end}}
</div>
</div>
{{end}}
{{end}}
<div class="divider"></div>
<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.due_date"}}</strong></span>
<div class="ui form tw-mt-2">
{{if .Issue.DeadlineUnix}}
<div class="tw-flex tw-justify-between tw-items-center tw-gap-2">
<div class="due-date {{if .Issue.IsOverdue}}text red{{end}}" {{if .Issue.IsOverdue}}data-tooltip-content="{{ctx.Locale.Tr "repo.issues.due_date_overdue"}}"{{end}}>
{{svg "octicon-calendar"}} {{DateUtils.AbsoluteLong .Issue.DeadlineUnix}}
</div>
<div class="flex-text-block">
{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
<a class="issue-due-edit muted" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.due_date_form_edit"}}">{{svg "octicon-pencil"}}</a>
<a class="issue-due-remove muted" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.due_date_form_remove"}}">{{svg "octicon-trash"}}</a>
{{end}}
</div>
</div>
{{else}}
{{ctx.Locale.Tr "repo.issues.due_date_not_set"}}
{{end}}
{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
<form class="ui fluid action input issue-due-form form-fetch-action tw-mt-2 {{if .Issue.DeadlineUnix}}tw-hidden{{end}}"
method="post" action="{{AppSubUrl}}/{{PathEscape .Repository.Owner.Name}}/{{PathEscape .Repository.Name}}/issues/{{.Issue.Index}}/deadline"
>
{{$.CsrfTokenHtml}}
<input required type="date" name="deadline" placeholder="{{ctx.Locale.Tr "repo.issues.due_date_form"}}" {{if .Issue.DeadlineUnix}}value="{{.Issue.DeadlineUnix.FormatDate}}"{{end}}>
<button class="ui icon button">{{Iif .Issue.DeadlineUnix (svg "octicon-pencil") (svg "octicon-plus")}}</button>
</form>
{{end}}
</div>
{{if .Repository.IsDependenciesEnabled ctx}}
<div class="divider"></div>
<div class="ui depending">
{{if (and (not .BlockedByDependencies) (not .BlockedByDependenciesNotPermitted) (not .BlockingDependencies) (not .BlockingDependenciesNotPermitted))}}
<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.dependency.title"}}</strong></span>
<br>
<p>
{{if .Issue.IsPull}}
{{ctx.Locale.Tr "repo.issues.dependency.pr_no_dependencies"}}
{{else}}
{{ctx.Locale.Tr "repo.issues.dependency.issue_no_dependencies"}}
{{end}}
</p>
{{end}}
{{if or .BlockingDependencies .BlockingDependenciesNotPermitted}}
<span class="text" data-tooltip-content="{{if .Issue.IsPull}}{{ctx.Locale.Tr "repo.issues.dependency.pr_close_blocks"}}{{else}}{{ctx.Locale.Tr "repo.issues.dependency.issue_close_blocks"}}{{end}}">
<strong>{{ctx.Locale.Tr "repo.issues.dependency.blocks_short"}}</strong>
</span>
<div class="ui relaxed divided list">
{{range .BlockingDependencies}}
<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-items-center tw-justify-between">
<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
<a class="title muted" href="{{.Issue.Link}}" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}">
#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}
</a>
<div class="text small gt-ellipsis" data-tooltip-content="{{.Repository.OwnerName}}/{{.Repository.Name}}">
{{.Repository.OwnerName}}/{{.Repository.Name}}
</div>
</div>
<div class="item-right tw-flex tw-items-center tw-m-1">
{{if and $.CanCreateIssueDependencies (not $.Repository.IsArchived)}}
<a class="delete-dependency-button ci muted" data-id="{{.Issue.ID}}" data-type="blocking" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.remove_info"}}">
{{svg "octicon-trash" 16}}
</a>
{{end}}
</div>
</div>
{{end}}
{{if .BlockingDependenciesNotPermitted}}
<div class="item tw-flex tw-items-center tw-justify-between gt-ellipsis">
<span>{{ctx.Locale.TrN (len .BlockingDependenciesNotPermitted) "repo.issues.dependency.no_permission_1" "repo.issues.dependency.no_permission_n" (len .BlockingDependenciesNotPermitted)}}</span>
</div>
{{end}}
</div>
{{end}}
{{if or .BlockedByDependencies .BlockedByDependenciesNotPermitted}}
<span class="text" data-tooltip-content="{{if .Issue.IsPull}}{{ctx.Locale.Tr "repo.issues.dependency.pr_closing_blockedby"}}{{else}}{{ctx.Locale.Tr "repo.issues.dependency.issue_closing_blockedby"}}{{end}}">
<strong>{{ctx.Locale.Tr "repo.issues.dependency.blocked_by_short"}}</strong>
</span>
<div class="ui relaxed divided list">
{{range .BlockedByDependencies}}
<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-items-center tw-justify-between">
<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
<a class="title muted" href="{{.Issue.Link}}" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}">
#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}
</a>
<div class="text small gt-ellipsis" data-tooltip-content="{{.Repository.OwnerName}}/{{.Repository.Name}}">
{{.Repository.OwnerName}}/{{.Repository.Name}}
</div>
</div>
<div class="item-right tw-flex tw-items-center tw-m-1">
{{if and $.CanCreateIssueDependencies (not $.Repository.IsArchived)}}
<a class="delete-dependency-button ci muted" data-id="{{.Issue.ID}}" data-type="blockedBy" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.remove_info"}}">
{{svg "octicon-trash" 16}}
</a>
{{end}}
</div>
</div>
{{end}}
{{if $.CanCreateIssueDependencies}}
{{range .BlockedByDependenciesNotPermitted}}
<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-items-center tw-justify-between">
<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
<div class="gt-ellipsis">
<span data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.no_permission.can_remove"}}">{{svg "octicon-lock" 16}}</span>
<span class="title" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}">
#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}
</span>
</div>
<div class="text small gt-ellipsis" data-tooltip-content="{{.Repository.OwnerName}}/{{.Repository.Name}}">
{{.Repository.OwnerName}}/{{.Repository.Name}}
</div>
</div>
<div class="item-right tw-flex tw-items-center tw-m-1">
{{if and $.CanCreateIssueDependencies (not $.Repository.IsArchived)}}
<a class="delete-dependency-button ci muted" data-id="{{.Issue.ID}}" data-type="blocking" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.remove_info"}}">
{{svg "octicon-trash" 16}}
</a>
{{end}}
</div>
</div>
{{end}}
{{else if .BlockedByDependenciesNotPermitted}}
<div class="item tw-flex tw-items-center tw-justify-between gt-ellipsis">
<span>{{ctx.Locale.TrN (len .BlockedByDependenciesNotPermitted) "repo.issues.dependency.no_permission_1" "repo.issues.dependency.no_permission_n" (len .BlockedByDependenciesNotPermitted)}}</span>
</div>
{{end}}
</div>
{{end}}
{{if and .CanCreateIssueDependencies (not .Repository.IsArchived)}}
<div>
<form method="post" action="{{.Issue.Link}}/dependency/add" id="addDependencyForm">
{{$.CsrfTokenHtml}}
<div class="ui fluid action input">
<div class="ui search selection dropdown" id="new-dependency-drop-list" data-issue-id="{{.Issue.ID}}">
<input name="newDependency" type="hidden">
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<input type="text" class="search">
<div class="default text">{{ctx.Locale.Tr "repo.issues.dependency.add"}}</div>
</div>
<button class="ui icon button">
{{svg "octicon-plus"}}
</button>
</div>
</form>
</div>
{{end}}
</div>
{{if and .CanCreateIssueDependencies (not .Repository.IsArchived)}}
<input type="hidden" id="crossRepoSearch" value="{{.AllowCrossRepositoryDependencies}}">
<div class="ui g-modal-confirm modal remove-dependency">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "repo.issues.dependency.remove_header"}}
</div>
<div class="content">
<form method="post" action="{{.Issue.Link}}/dependency/delete" id="removeDependencyForm">
{{$.CsrfTokenHtml}}
<input type="hidden" value="" name="removeDependencyID" id="removeDependencyID">
<input type="hidden" value="" name="dependencyType" id="dependencyType">
</form>
<p>{{if .Issue.IsPull}}
{{ctx.Locale.Tr "repo.issues.dependency.pr_remove_text"}}
{{else}}
{{ctx.Locale.Tr "repo.issues.dependency.issue_remove_text"}}
{{end}}</p>
</div>
{{$ModalButtonCancelText := ctx.Locale.Tr "repo.issues.dependency.cancel"}}
{{$ModalButtonOkText := ctx.Locale.Tr "repo.issues.dependency.remove"}}
{{template "base/modal_actions_confirm" (dict "." . "ModalButtonCancelText" $ModalButtonCancelText "ModalButtonOkText" $ModalButtonOkText)}}
</div>
{{end}}
{{end}}
<div class="divider"></div>
<div class="ui equal width compact grid">
{{$issueReferenceLink := printf "%s#%d" .Issue.Repo.FullName .Issue.Index}}
<div class="row tw-items-center" data-tooltip-content="{{$issueReferenceLink}}">
<span class="text column truncate">{{ctx.Locale.Tr "repo.issues.reference_link" $issueReferenceLink}}</span>
<button class="ui two wide button column tw-p-2" data-clipboard-text="{{$issueReferenceLink}}">{{svg "octicon-copy" 14}}</button>
</div>
</div>
{{if and .IsRepoAdmin (not .Repository.IsArchived)}}
<div class="divider"></div>
{{if or .PinEnabled .Issue.IsPinned}}
<form class="tw-mt-1 form-fetch-action single-button-form" method="post" {{if $.NewPinAllowed}}action="{{.Issue.Link}}/pin"{{else}}data-tooltip-content="{{ctx.Locale.Tr "repo.issues.max_pinned"}}"{{end}}>
{{$.CsrfTokenHtml}}
<button class="fluid ui button {{if not $.NewPinAllowed}}disabled{{end}}">
{{if not .Issue.IsPinned}}
{{svg "octicon-pin" 16 "tw-mr-2"}}
{{ctx.Locale.Tr "pin"}}
{{else}}
{{svg "octicon-pin-slash" 16 "tw-mr-2"}}
{{ctx.Locale.Tr "unpin"}}
{{end}}
</button>
</form>
{{end}}
<button class="tw-mt-1 fluid ui show-modal button{{if .Issue.IsLocked}} red{{end}}" data-modal="#lock">
{{if .Issue.IsLocked}}
{{svg "octicon-key"}}
{{ctx.Locale.Tr "repo.issues.unlock"}}
{{else}}
{{svg "octicon-lock"}}
{{ctx.Locale.Tr "repo.issues.lock"}}
{{end}}
</button>
<div class="ui tiny modal" id="lock">
<div class="header">
{{if .Issue.IsLocked}}
{{ctx.Locale.Tr "repo.issues.unlock.title"}}
{{else}}
{{ctx.Locale.Tr "repo.issues.lock.title"}}
{{end}}
</div>
<div class="content">
<div class="ui warning message">
{{if .Issue.IsLocked}}
{{ctx.Locale.Tr "repo.issues.unlock.notice_1"}}<br>
{{ctx.Locale.Tr "repo.issues.unlock.notice_2"}}<br>
{{else}}
{{ctx.Locale.Tr "repo.issues.lock.notice_1"}}<br>
{{ctx.Locale.Tr "repo.issues.lock.notice_2"}}<br>
{{ctx.Locale.Tr "repo.issues.lock.notice_3"}}<br>
{{end}}
</div>
<form class="ui form form-fetch-action" action="{{.Issue.Link}}{{if .Issue.IsLocked}}/unlock{{else}}/lock{{end}}"
method="post">
{{.CsrfTokenHtml}}
{{if not .Issue.IsLocked}}
<div class="field">
<strong> {{ctx.Locale.Tr "repo.issues.lock.reason"}} </strong>
</div>
<div class="field">
<div class="ui fluid dropdown selection">
<select name="reason">
<option value=""> </option>
{{range .LockReasons}}
<option value="{{.}}">{{.}}</option>
{{end}}
</select>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="default text"> </div>
<div class="menu">
{{range .LockReasons}}
<div class="item" data-value="{{.}}">{{.}}</div>
{{end}}
</div>
</div>
</div>
{{end}}
<div class="text right actions">
<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button class="ui red button">
{{if .Issue.IsLocked}}
{{ctx.Locale.Tr "repo.issues.unlock_confirm"}}
{{else}}
{{ctx.Locale.Tr "repo.issues.lock_confirm"}}
{{end}}
</button>
</div>
</form>
</div>
</div>
<button class="tw-mt-1 fluid ui show-modal button" data-modal="#sidebar-delete-issue">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "repo.issues.delete"}}
</button>
<div class="ui g-modal-confirm modal" id="sidebar-delete-issue">
<div class="header">
{{if .Issue.IsPull}}
{{ctx.Locale.Tr "repo.pulls.delete.title"}}
{{else}}
{{ctx.Locale.Tr "repo.issues.delete.title"}}
{{end}}
</div>
<div class="content">
<p>
{{if .Issue.IsPull}}
{{ctx.Locale.Tr "repo.pulls.delete.text"}}
{{else}}
{{ctx.Locale.Tr "repo.issues.delete.text"}}
{{end}}
</p>
</div>
<form action="{{.Issue.Link}}/delete" method="post">
{{.CsrfTokenHtml}}
{{template "base/modal_actions_confirm" .}}
</form>
</div>
{{end}}
{{if and .Issue.IsPull .IsIssuePoster (not .Issue.IsClosed) .Issue.PullRequest.HeadRepo}}
{{if and (not (eq .Issue.PullRequest.HeadRepo.FullName .Issue.PullRequest.BaseRepo.FullName)) .CanWriteToHeadRepo}}
<div class="divider"></div>
<div class="inline field">
<div class="ui checkbox loading-icon-2px" id="allow-edits-from-maintainers"
data-url="{{.Issue.Link}}"
data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers_desc"}}"
data-prompt-error="{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers_err"}}"
>
<label><strong>{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers"}}</strong></label>
<input type="checkbox" {{if .Issue.PullRequest.AllowMaintainerEdit}}checked{{end}}>
</div>
</div>
{{end}}
{{end}}
{{template "repo/issue/sidebar/milestone_list" $}}
{{template "repo/issue/sidebar/project_list" $}}
{{template "repo/issue/sidebar/assignee_list" $}}
{{template "repo/issue/sidebar/participant_list" $}}
{{template "repo/issue/sidebar/watch_notification" $}}
{{template "repo/issue/sidebar/stopwatch_timetracker" $}}
{{template "repo/issue/sidebar/due_date" $}}
{{template "repo/issue/sidebar/issue_dependencies" $}}
{{template "repo/issue/sidebar/reference_link" $}}
{{template "repo/issue/sidebar/issue_management" $}}
{{template "repo/issue/sidebar/allow_maintainer_edit" $}}
</div>

View File

@ -3,6 +3,12 @@
{{template "base/alert" .}}
</div>
{{end}}
<div class="tw-hidden" id="issue-page-info"
data-issue-index="{{$.Issue.Index}}"
data-issue-dependency-search-type="{{$.IssueDependencySearchType}}"
data-issue-repo-link="{{$.RepoLink}}"
data-issue-repo-id="{{$.Repository.ID}}"
></div>
<div class="issue-title-header">
{{$canEditIssueTitle := and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}
<div class="issue-title" id="issue-title-display">

View File

@ -1,8 +1,4 @@
{{template "base/head" .}}
<input type="hidden" id="repolink" value="{{$.RepoRelPath}}">
<input type="hidden" id="issueIndex" value="{{.Issue.Index}}">
<div role="main" aria-label="{{.Title}}" class="page-content repository view issue pull files diff">
{{template "repo/header" .}}
<div class="ui container fluid padded">

View File

@ -4,17 +4,20 @@
package integration
import (
"fmt"
"net/http"
"net/url"
"testing"
"code.gitea.io/gitea/models"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
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/git"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/release"
"code.gitea.io/gitea/tests"
@ -117,3 +120,47 @@ func TestCreateNewTagProtected(t *testing.T) {
assert.NoError(t, err)
}
}
func TestRepushTag(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, owner.LowerName)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
httpContext := NewAPITestContext(t, owner.Name, repo.Name)
dstPath := t.TempDir()
u.Path = httpContext.GitPath()
u.User = url.UserPassword(owner.Name, userPassword)
doGitClone(dstPath, u)(t)
// create and push a tag
_, _, err := git.NewCommand(git.DefaultContext, "tag", "v2.0").RunStdString(&git.RunOpts{Dir: dstPath})
assert.NoError(t, err)
_, _, err = git.NewCommand(git.DefaultContext, "push", "origin", "--tags", "v2.0").RunStdString(&git.RunOpts{Dir: dstPath})
assert.NoError(t, err)
// create a release for the tag
createdRelease := createNewReleaseUsingAPI(t, token, owner, repo, "v2.0", "", "Release of v2.0", "desc")
assert.False(t, createdRelease.IsDraft)
// delete the tag
_, _, err = git.NewCommand(git.DefaultContext, "push", "origin", "--delete", "v2.0").RunStdString(&git.RunOpts{Dir: dstPath})
assert.NoError(t, err)
// query the release by API and it should be a draft
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, "v2.0"))
resp := MakeRequest(t, req, http.StatusOK)
var respRelease *api.Release
DecodeJSON(t, resp, &respRelease)
assert.True(t, respRelease.IsDraft)
// re-push the tag
_, _, err = git.NewCommand(git.DefaultContext, "push", "origin", "--tags", "v2.0").RunStdString(&git.RunOpts{Dir: dstPath})
assert.NoError(t, err)
// query the release by API and it should not be a draft
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, "v2.0"))
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &respRelease)
assert.False(t, respRelease.IsDraft)
})
}

View File

@ -73,7 +73,7 @@
margin-left: 5px;
}
.page-footer .ui.dropdown.language .menu {
.page-footer .ui.dropdown .menu.language-menu {
max-height: min(500px, calc(100vh - 60px));
overflow-y: auto;
margin-bottom: 10px;

View File

@ -62,23 +62,6 @@
}
}
.repository .issue-content-right .ui.list .dependency {
padding: 0;
white-space: nowrap;
}
.repository .issue-content-right .ui.list .title {
overflow: hidden;
text-overflow: ellipsis;
}
.repository .issue-content-right #deadlineForm input {
width: 12.8rem;
border-radius: var(--border-radius) 0 0 var(--border-radius);
border-right: 0;
white-space: nowrap;
}
.repository .issue-content-right .filter.menu {
max-height: 500px;
overflow-x: auto;

View File

@ -1,6 +1,7 @@
import $ from 'jquery';
import {GET} from '../modules/fetch.ts';
import {showGlobalErrorMessage} from '../bootstrap.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';
import {queryElems} from '../utils/dom.ts';
const {appUrl} = window.config;
@ -17,18 +18,18 @@ export function initHeadNavbarContentToggle() {
}
export function initFootLanguageMenu() {
async function linkLanguageAction() {
const $this = $(this);
await GET($this.data('url'));
document.querySelector('.ui.dropdown .menu.language-menu')?.addEventListener('click', async (e) => {
const item = (e.target as HTMLElement).closest('.item');
if (!item) return;
e.preventDefault();
await GET(item.getAttribute('data-url'));
window.location.reload();
}
$('.language-menu a[lang]').on('click', linkLanguageAction);
});
}
export function initGlobalDropdown() {
// Semantic UI modules.
const $uiDropdowns = $('.ui.dropdown');
const $uiDropdowns = fomanticQuery('.ui.dropdown');
// do not init "custom" dropdowns, "custom" dropdowns are managed by their own code.
$uiDropdowns.filter(':not(.custom)').dropdown();
@ -46,14 +47,14 @@ export function initGlobalDropdown() {
},
onHide() {
this._tippy?.enable();
// eslint-disable-next-line unicorn/no-this-assignment
const elDropdown = this;
// hide all tippy elements of items after a while. eg: use Enter to click "Copy Link" in the Issue Context Menu
setTimeout(() => {
const $dropdown = $(this);
const $dropdown = fomanticQuery(elDropdown);
if ($dropdown.dropdown('is hidden')) {
$(this).find('.menu > .item').each((_, item) => {
item._tippy?.hide();
});
queryElems(elDropdown, '.menu > .item', (el) => el._tippy?.hide());
}
}, 2000);
},
@ -71,7 +72,7 @@ export function initGlobalDropdown() {
}
export function initGlobalTabularMenu() {
$('.ui.menu.tabular:not(.custom) .item').tab({autoTabActivation: false});
fomanticQuery('.ui.menu.tabular:not(.custom) .item').tab({autoTabActivation: false});
}
/**

View File

@ -1,14 +1,14 @@
import $ from 'jquery';
import {GET} from '../modules/fetch.ts';
import {hideElem, loadElem, queryElemChildren, queryElems} from '../utils/dom.ts';
import {parseDom} from '../utils.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';
function getDefaultSvgBoundsIfUndefined(text, src) {
const defaultSize = 300;
const maxSize = 99999;
const svgDoc = parseDom(text, 'image/svg+xml');
const svg = svgDoc.documentElement;
const svg = (svgDoc.documentElement as unknown) as SVGSVGElement;
const width = svg?.width?.baseVal;
const height = svg?.height?.baseVal;
if (width === undefined || height === undefined) {
@ -68,12 +68,14 @@ function createContext(imageAfter, imageBefore) {
}
class ImageDiff {
async init(containerEl) {
containerEl: HTMLElement;
diffContainerWidth: number;
async init(containerEl: HTMLElement) {
this.containerEl = containerEl;
containerEl.setAttribute('data-image-diff-loaded', 'true');
// the only jQuery usage in this file
$(containerEl).find('.ui.menu.tabular .item').tab({autoTabActivation: false});
fomanticQuery(containerEl).find('.ui.menu.tabular .item').tab({autoTabActivation: false});
// the container may be hidden by "viewed" checkbox, so use the parent's width for reference
this.diffContainerWidth = Math.max(containerEl.closest('.diff-file-box').clientWidth - 300, 100);
@ -81,12 +83,12 @@ class ImageDiff {
const imageInfos = [{
path: containerEl.getAttribute('data-path-after'),
mime: containerEl.getAttribute('data-mime-after'),
images: containerEl.querySelectorAll('img.image-after'), // matches 3 <img>
images: containerEl.querySelectorAll<HTMLImageElement>('img.image-after'), // matches 3 <img>
boundsInfo: containerEl.querySelector('.bounds-info-after'),
}, {
path: containerEl.getAttribute('data-path-before'),
mime: containerEl.getAttribute('data-mime-before'),
images: containerEl.querySelectorAll('img.image-before'), // matches 3 <img>
images: containerEl.querySelectorAll<HTMLImageElement>('img.image-before'), // matches 3 <img>
boundsInfo: containerEl.querySelector('.bounds-info-before'),
}];
@ -102,8 +104,8 @@ class ImageDiff {
const bounds = getDefaultSvgBoundsIfUndefined(text, info.path);
if (bounds) {
for (const el of info.images) {
el.setAttribute('width', bounds.width);
el.setAttribute('height', bounds.height);
el.setAttribute('width', String(bounds.width));
el.setAttribute('height', String(bounds.height));
}
hideElem(info.boundsInfo);
}
@ -151,7 +153,7 @@ class ImageDiff {
const boundsInfoBeforeHeight = this.containerEl.querySelector('.bounds-info-before .bounds-info-height');
if (boundsInfoBeforeHeight) {
boundsInfoBeforeHeight.textContent = `${sizes.imageBefore.naturalHeight}px`;
boundsInfoBeforeHeight.classList.add('red', heightChanged);
boundsInfoBeforeHeight.classList.toggle('red', heightChanged);
}
}
@ -205,7 +207,7 @@ class ImageDiff {
}
// extra height for inner "position: absolute" elements
const swipe = this.containerEl.querySelector('.diff-swipe');
const swipe = this.containerEl.querySelector<HTMLElement>('.diff-swipe');
if (swipe) {
swipe.style.width = `${sizes.maxSize.width * factor + 2}px`;
swipe.style.height = `${sizes.maxSize.height * factor + 30}px`;
@ -225,7 +227,7 @@ class ImageDiff {
const rect = swipeFrame.getBoundingClientRect();
const value = Math.max(0, Math.min(e.clientX - rect.left, width));
swipeBar.style.left = `${value}px`;
this.containerEl.querySelector('.swipe-container').style.width = `${swipeFrame.clientWidth - value}px`;
this.containerEl.querySelector<HTMLElement>('.swipe-container').style.width = `${swipeFrame.clientWidth - value}px`;
};
const removeEventListeners = () => {
document.removeEventListener('mousemove', onSwipeMouseMove);
@ -264,11 +266,11 @@ class ImageDiff {
overlayFrame.style.height = `${sizes.maxSize.height * factor + 2}px`;
}
const rangeInput = this.containerEl.querySelector('input[type="range"]');
const rangeInput = this.containerEl.querySelector<HTMLInputElement>('input[type="range"]');
function updateOpacity() {
if (sizes.imageAfter) {
sizes.imageAfter.parentNode.style.opacity = `${rangeInput.value / 100}`;
sizes.imageAfter.parentNode.style.opacity = `${Number(rangeInput.value) / 100}`;
}
}
@ -278,7 +280,7 @@ class ImageDiff {
}
export function initImageDiff() {
for (const el of queryElems('.image-diff:not([data-image-diff-loaded])')) {
for (const el of queryElems<HTMLImageElement>(document, '.image-diff:not([data-image-diff-loaded])')) {
(new ImageDiff()).init(el); // it is async, but we don't need to await for it
}
}

View File

@ -31,7 +31,7 @@ async function onDownloadArchive(e) {
}
export function initRepoArchiveLinks() {
queryElems('a.archive-link[href]', (el) => el.addEventListener('click', onDownloadArchive));
queryElems(document, 'a.archive-link[href]', (el) => el.addEventListener('click', onDownloadArchive));
}
export function initRepoActivityTopAuthorsChart() {

View File

@ -45,17 +45,17 @@ export function initRepoEditor() {
const dropzoneUpload = document.querySelector('.page-content.repository.editor.upload .dropzone');
if (dropzoneUpload) initDropzone(dropzoneUpload);
const editArea = document.querySelector('.page-content.repository.editor textarea#edit_area');
const editArea = document.querySelector<HTMLTextAreaElement>('.page-content.repository.editor textarea#edit_area');
if (!editArea) return;
for (const el of queryElems('.js-quick-pull-choice-option')) {
for (const el of queryElems<HTMLInputElement>(document, '.js-quick-pull-choice-option')) {
el.addEventListener('input', () => {
if (el.value === 'commit-to-new-branch') {
showElem('.quick-pull-branch-name');
document.querySelector('.quick-pull-branch-name input').required = true;
document.querySelector<HTMLInputElement>('.quick-pull-branch-name input').required = true;
} else {
hideElem('.quick-pull-branch-name');
document.querySelector('.quick-pull-branch-name input').required = false;
document.querySelector<HTMLInputElement>('.quick-pull-branch-name input').required = false;
}
document.querySelector('#commit-button').textContent = el.getAttribute('data-button-text');
});
@ -71,13 +71,13 @@ export function initRepoEditor() {
if (filenameInput.value) {
parts.push(filenameInput.value);
}
document.querySelector('#tree_path').value = parts.join('/');
document.querySelector<HTMLInputElement>('#tree_path').value = parts.join('/');
}
filenameInput.addEventListener('input', function () {
const parts = filenameInput.value.split('/');
const links = Array.from(document.querySelectorAll('.breadcrumb span.section'));
const dividers = Array.from(document.querySelectorAll('.breadcrumb .breadcrumb-divider'));
let warningDiv = document.querySelector('.ui.warning.message.flash-message.flash-warning.space-related');
let warningDiv = document.querySelector<HTMLDivElement>('.ui.warning.message.flash-message.flash-warning.space-related');
let containSpace = false;
if (parts.length > 1) {
for (let i = 0; i < parts.length; ++i) {
@ -110,14 +110,14 @@ export function initRepoEditor() {
filenameInput.value = value;
}
this.setSelectionRange(0, 0);
containSpace |= (trimValue !== value && trimValue !== '');
containSpace = containSpace || (trimValue !== value && trimValue !== '');
}
}
containSpace |= Array.from(links).some((link) => {
containSpace = containSpace || Array.from(links).some((link) => {
const value = link.querySelector('a').textContent;
return value.trim() !== value;
});
containSpace |= parts[parts.length - 1].trim() !== parts[parts.length - 1];
containSpace = containSpace || parts[parts.length - 1].trim() !== parts[parts.length - 1];
if (containSpace) {
if (!warningDiv) {
warningDiv = document.createElement('div');
@ -135,8 +135,8 @@ export function initRepoEditor() {
joinTreePath();
});
filenameInput.addEventListener('keydown', function (e) {
const sections = queryElems('.breadcrumb span.section');
const dividers = queryElems('.breadcrumb .breadcrumb-divider');
const sections = queryElems(document, '.breadcrumb span.section');
const dividers = queryElems(document, '.breadcrumb .breadcrumb-divider');
// Jump back to last directory once the filename is empty
if (e.code === 'Backspace' && filenameInput.selectionStart === 0 && sections.length > 0) {
e.preventDefault();
@ -159,7 +159,7 @@ export function initRepoEditor() {
// Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage
// to enable or disable the commit button
const commitButton = document.querySelector('#commit-button');
const commitButton = document.querySelector<HTMLButtonElement>('#commit-button');
const $editForm = $('.ui.edit.form');
const dirtyFileClass = 'dirty-file';

View File

@ -3,8 +3,8 @@ import {svg} from '../svg.ts';
import {showErrorToast} from '../modules/toast.ts';
import {GET, POST} from '../modules/fetch.ts';
import {showElem} from '../utils/dom.ts';
import {parseIssuePageInfo} from '../utils.ts';
const {appSubUrl} = window.config;
let i18nTextEdited;
let i18nTextOptions;
let i18nTextDeleteFromHistory;
@ -121,15 +121,14 @@ function showContentHistoryMenu(issueBaseUrl, $item, commentId) {
}
export async function initRepoIssueContentHistory() {
const issueIndex = $('#issueIndex').val();
if (!issueIndex) return;
const issuePageInfo = parseIssuePageInfo();
if (!issuePageInfo.issueNumber) return;
const $itemIssue = $('.repository.issue .timeline-item.comment.first'); // issue(PR) main content
const $comments = $('.repository.issue .comment-list .comment'); // includes: issue(PR) comments, review comments, code comments
if (!$itemIssue.length && !$comments.length) return;
const repoLink = $('#repolink').val();
const issueBaseUrl = `${appSubUrl}/${repoLink}/issues/${issueIndex}`;
const issueBaseUrl = `${issuePageInfo.repoLink}/issues/${issuePageInfo.issueNumber}`;
try {
const response = await GET(`${issueBaseUrl}/content-history/overview`);

View File

@ -4,7 +4,7 @@ import {createTippy, showTemporaryTooltip} from '../modules/tippy.ts';
import {hideElem, showElem, toggleElem} from '../utils/dom.ts';
import {setFileFolding} from './file-fold.ts';
import {ComboMarkdownEditor, getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
import {toAbsoluteUrl} from '../utils.ts';
import {parseIssuePageInfo, toAbsoluteUrl} from '../utils.ts';
import {GET, POST} from '../modules/fetch.ts';
import {showErrorToast} from '../modules/toast.ts';
import {initRepoIssueSidebar} from './repo-issue-sidebar.ts';
@ -57,13 +57,11 @@ function excludeLabel(item) {
}
export function initRepoIssueSidebarList() {
const repolink = $('#repolink').val();
const repoId = $('#repoId').val();
const issuePageInfo = parseIssuePageInfo();
const crossRepoSearch = $('#crossRepoSearch').val();
const tp = $('#type').val();
let issueSearchUrl = `${appSubUrl}/${repolink}/issues/search?q={query}&type=${tp}`;
let issueSearchUrl = `${issuePageInfo.repoLink}/issues/search?q={query}&type=${issuePageInfo.issueDependencySearchType}`;
if (crossRepoSearch === 'true') {
issueSearchUrl = `${appSubUrl}/issues/search?q={query}&priority_repo_id=${repoId}&type=${tp}`;
issueSearchUrl = `${appSubUrl}/issues/search?q={query}&priority_repo_id=${issuePageInfo.repoId}&type=${issuePageInfo.issueDependencySearchType}`;
}
$('#new-dependency-drop-list')
.dropdown({

View File

@ -8,7 +8,7 @@ const {appSubUrl, csrfToken} = window.config;
function initRepoSettingsCollaboration() {
// Change collaborator access mode
for (const dropdownEl of queryElems('.page-content.repository .ui.dropdown.access-mode')) {
for (const dropdownEl of queryElems(document, '.page-content.repository .ui.dropdown.access-mode')) {
const textEl = dropdownEl.querySelector(':scope > .text');
$(dropdownEl).dropdown({
async action(text, value) {

View File

@ -1,3 +1,4 @@
import $ from 'jquery';
let ariaIdCounter = 0;
export function generateAriaId() {
@ -16,3 +17,6 @@ export function linkLabelAndInput(label, input) {
label.setAttribute('for', id);
}
}
// eslint-disable-next-line no-jquery/variable-pattern
export const fomanticQuery = $;

View File

@ -179,11 +179,9 @@ export function initGlobalTooltips() {
}
export function showTemporaryTooltip(target: Element, content: Content) {
// if the target is inside a dropdown, don't show the tooltip because when the dropdown
// closes, the tippy would be pushed unsightly to the top-left of the screen like seen
// on the issue comment menu.
if (target.closest('.ui.dropdown > .menu')) return;
// if the target is inside a dropdown, the menu will be hidden soon
// so display the tooltip on the dropdown instead
target = target.closest('.ui.dropdown') || target;
const tippy = target._tippy ?? attachTooltip(target, content);
tippy.setContent(content);
if (!tippy.state.isShown) tippy.show();

View File

@ -37,6 +37,13 @@ export type IssuePathInfo = {
indexString?: string,
}
export type IssuePageInfo = {
repoLink: string,
repoId: number,
issueNumber: number,
issueDependencySearchType: string,
}
export type Issue = {
id: number;
number: number;

View File

@ -1,5 +1,5 @@
import {encode, decode} from 'uint8-to-base64';
import type {IssuePathInfo} from './types.ts';
import {decode, encode} from 'uint8-to-base64';
import type {IssuePageInfo, IssuePathInfo} from './types.ts';
// transform /path/to/file.ext to file.ext
export function basename(path: string): string {
@ -43,6 +43,16 @@ export function parseIssueNewHref(href: string): IssuePathInfo {
return {ownerName, repoName, pathType, indexString};
}
export function parseIssuePageInfo(): IssuePageInfo {
const el = document.querySelector('#issue-page-info');
return {
issueNumber: parseInt(el?.getAttribute('data-issue-index')),
issueDependencySearchType: el?.getAttribute('data-issue-dependency-search-type') || '',
repoId: parseInt(el?.getAttribute('data-issue-repo-id')),
repoLink: el?.getAttribute('data-issue-repo-link') || '',
};
}
// parse a URL, either relative '/path' or absolute 'https://localhost/path'
export function parseUrl(str: string): URL {
return new URL(str, str.startsWith('http') ? undefined : window.location.origin);

View File

@ -5,7 +5,7 @@ import type $ from 'jquery';
type ElementArg = Element | string | NodeListOf<Element> | Array<Element> | ReturnType<typeof $>;
type ElementsCallback = (el: Element) => Promisable<any>;
type ElementsCallbackWithArgs = (el: Element, ...args: any[]) => Promisable<any>;
type IterableElements = NodeListOf<Element> | Array<Element>;
type ArrayLikeIterable<T> = ArrayLike<T> & Iterable<T>; // for NodeListOf and Array
function elementsCall(el: ElementArg, func: ElementsCallbackWithArgs, ...args: any[]) {
if (typeof el === 'string' || el instanceof String) {
@ -15,7 +15,7 @@ function elementsCall(el: ElementArg, func: ElementsCallbackWithArgs, ...args: a
func(el, ...args);
} else if (el.length !== undefined) {
// this works for: NodeList, HTMLCollection, Array, jQuery
for (const e of (el as IterableElements)) {
for (const e of (el as ArrayLikeIterable<Element>)) {
func(e, ...args);
}
} else {
@ -58,7 +58,7 @@ export function isElemHidden(el: ElementArg) {
return res[0];
}
function applyElemsCallback(elems: IterableElements, fn?: ElementsCallback) {
function applyElemsCallback<T extends Element>(elems: ArrayLikeIterable<T>, fn?: ElementsCallback): ArrayLikeIterable<T> {
if (fn) {
for (const el of elems) {
fn(el);
@ -67,19 +67,22 @@ function applyElemsCallback(elems: IterableElements, fn?: ElementsCallback) {
return elems;
}
export function queryElemSiblings(el: Element, selector = '*', fn?: ElementsCallback) {
return applyElemsCallback(Array.from(el.parentNode.children).filter((child: Element) => {
export function queryElemSiblings<T extends Element>(el: Element, selector = '*', fn?: ElementsCallback): ArrayLikeIterable<T> {
const elems = Array.from(el.parentNode.children) as T[];
return applyElemsCallback<T>(elems.filter((child: Element) => {
return child !== el && child.matches(selector);
}), fn);
}
// it works like jQuery.children: only the direct children are selected
export function queryElemChildren(parent: Element | ParentNode, selector = '*', fn?: ElementsCallback) {
return applyElemsCallback(parent.querySelectorAll(`:scope > ${selector}`), fn);
export function queryElemChildren<T extends Element>(parent: Element | ParentNode, selector = '*', fn?: ElementsCallback): ArrayLikeIterable<T> {
return applyElemsCallback<T>(parent.querySelectorAll(`:scope > ${selector}`), fn);
}
export function queryElems(selector: string, fn?: ElementsCallback) {
return applyElemsCallback(document.querySelectorAll(selector), fn);
// it works like parent.querySelectorAll: all descendants are selected
// in the future, all "queryElems(document, ...)" should be refactored to use a more specific parent
export function queryElems<T extends Element>(parent: Element | ParentNode, selector: string, fn?: ElementsCallback): ArrayLikeIterable<T> {
return applyElemsCallback<T>(parent.querySelectorAll(selector), fn);
}
export function onDomReady(cb: () => Promisable<void>) {
@ -92,7 +95,7 @@ export function onDomReady(cb: () => Promisable<void>) {
// checks whether an element is owned by the current document, and whether it is a document fragment or element node
// if it is, it means it is a "normal" element managed by us, which can be modified safely.
export function isDocumentFragmentOrElementNode(el: Element | Node) {
export function isDocumentFragmentOrElementNode(el: Node) {
try {
return el.ownerDocument === document && el.nodeType === Node.ELEMENT_NODE || el.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
} catch {