mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-23 19:47:07 +09:00
refactor: issue comment context menu and buttons (#39771)
1. Support show "Quote reply" shortcut in help menu, fix the TODO 2. Help #36862 to reuse existing "issue/comment context menu"
This commit is contained in:
1 parent
b015cd67e2
commit
ccc9b928fe
16 files changed
+205
-191
No files matched your search
@@ -27,19 +27,19 @@
|
|||||||
<div class="field footer">
|
<div class="field footer">
|
||||||
<div class="flex-text-block tw-justify-end">
|
<div class="flex-text-block tw-justify-end">
|
||||||
{{if $.reply}}
|
{{if $.reply}}
|
||||||
<button class="ui submit primary tiny button btn-reply" type="submit">{{ctx.Locale.Tr "repo.diff.comment.reply"}}</button>
|
<button class="ui submit primary tiny button" type="submit">{{ctx.Locale.Tr "repo.diff.comment.reply"}}</button>
|
||||||
<input type="hidden" name="reply" value="{{$.reply}}">
|
<input type="hidden" name="reply" value="{{$.reply}}">
|
||||||
<input type="hidden" name="single_review" value="true">
|
<input type="hidden" name="single_review" value="true">
|
||||||
{{else}}
|
{{else}}
|
||||||
{{if $.root.CurrentReview}}
|
{{if $.root.CurrentReview}}
|
||||||
<button name="pending_review" type="submit" class="ui submit primary tiny button btn-add-comment">{{ctx.Locale.Tr "repo.diff.comment.add_review_comment"}}</button>
|
<button name="pending_review" type="submit" class="ui submit primary tiny button">{{ctx.Locale.Tr "repo.diff.comment.add_review_comment"}}</button>
|
||||||
{{else}}
|
{{else}}
|
||||||
<button name="pending_review" type="submit" class="ui submit primary tiny button btn-start-review">{{ctx.Locale.Tr "repo.diff.comment.start_review"}}</button>
|
<button name="pending_review" type="submit" class="ui submit primary tiny button">{{ctx.Locale.Tr "repo.diff.comment.start_review"}}</button>
|
||||||
<button name="single_review" value="true" type="submit" class="ui submit tiny basic button btn-add-single">{{ctx.Locale.Tr "repo.diff.comment.add_single_comment"}}</button>
|
<button name="single_review" value="true" type="submit" class="ui submit tiny basic button">{{ctx.Locale.Tr "repo.diff.comment.add_single_comment"}}</button>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if or (not $.HasComments) $.hidden}}
|
{{if or (not $.HasComments) $.hidden}}
|
||||||
<button type="button" class="ui submit tiny basic button btn-cancel cancel-code-comment">{{ctx.Locale.Tr "cancel"}}</button>
|
<button type="button" class="ui submit tiny basic button" data-global-click="onCancelCodeComment">{{ctx.Locale.Tr "cancel"}}</button>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{{range .comments}}
|
{{range $comment := .comments}}
|
||||||
|
|
||||||
{{$createdStr:= DateUtils.TimeSince .CreatedUnix}}
|
{{$createdStr:= DateUtils.TimeSince .CreatedUnix}}
|
||||||
<div class="comment" id="{{.HashTag}}">
|
<div class="comment" id="{{.HashTag}}">
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
{{if not $.root.Repository.IsArchived}}
|
{{if not $.root.Repository.IsArchived}}
|
||||||
{{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/comments/%d/reactions" $.root.RepoLink .ID)}}
|
{{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/comments/%d/reactions" $.root.RepoLink .ID)}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{template "repo/issue/view_content/context_menu" dict "item" . "delete" true "issue" false "diff" true "IsCommentPoster" (and $.root.IsSigned (eq $.root.SignedUserID .PosterID))}}
|
{{template "repo/issue/view_content/context_menu" dict "ConvContentType" "issue_comment" "ConvContentPage" "pr-files" "ConvContentItem" $comment}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui attached segment comment-body">
|
<div class="ui attached segment comment-body">
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
<div id="review-box" {{if $.Repository.IsArchived}}data-tooltip-content="{{ctx.Locale.Tr "repo.archive.pull.nocomment"}}"{{end}}>
|
{{$disabledReviewTooltip := "" -}}
|
||||||
<button type="button" class="ui tiny primary button tw-pr-1 js-btn-review {{if not $.IsShowingAllCommits}}disabled{{end}}"
|
{{if $.Repository.IsArchived -}}
|
||||||
{{if not $.IsShowingAllCommits}}data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.review_only_possible_for_full_diff"}}"{{end}}
|
{{$disabledReviewTooltip = ctx.Locale.Tr "repo.archive.pull.nocomment" -}}
|
||||||
{{if $.Repository.IsArchived}}disabled{{end}}
|
{{else if not $.IsShowingAllCommits -}}
|
||||||
>
|
{{$disabledReviewTooltip = ctx.Locale.Tr "repo.pulls.review_only_possible_for_full_diff" -}}
|
||||||
|
{{end -}}
|
||||||
|
<div data-global-init="initPullReviewBox">
|
||||||
|
<div {{if $disabledReviewTooltip}}data-tooltip-content="{{$disabledReviewTooltip}}"{{end}}>
|
||||||
|
<button type="button" class="ui tiny primary button review-box-button" {{if $disabledReviewTooltip}}disabled{{end}}>
|
||||||
{{ctx.Locale.Tr "repo.diff.review"}}
|
{{ctx.Locale.Tr "repo.diff.review"}}
|
||||||
<span class="ui small label review-comments-counter" data-pending-comment-number="{{.PendingCodeCommentNumber}}">{{.PendingCodeCommentNumber}}</span>
|
<span class="ui small label review-comments-counter" data-pending-comment-number="{{.PendingCodeCommentNumber}}">{{.PendingCodeCommentNumber}}</span>
|
||||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||||
@@ -15,7 +19,7 @@
|
|||||||
<input type="hidden" name="commit_id" value="{{.AfterCommitID}}">
|
<input type="hidden" name="commit_id" value="{{.AfterCommitID}}">
|
||||||
<div class="field flex-text-block">
|
<div class="field flex-text-block">
|
||||||
<div class="tw-flex-1">{{ctx.Locale.Tr "repo.diff.review.header"}}</div>
|
<div class="tw-flex-1">{{ctx.Locale.Tr "repo.diff.review.header"}}</div>
|
||||||
<a class="muted close">{{svg "octicon-x" 16}}</a>
|
<a class="muted close-review-panel">{{svg "octicon-x" 16}}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
{{template "shared/combomarkdowneditor" (dict
|
{{template "shared/combomarkdowneditor" (dict
|
||||||
@@ -55,3 +59,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
</div>
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
{{if not $.Repository.IsArchived}}
|
{{if not $.Repository.IsArchived}}
|
||||||
{{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index)}}
|
{{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index)}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{template "repo/issue/view_content/context_menu" dict "item" .Issue "delete" false "issue" true "diff" false "IsCommentPoster" $.IsIssuePoster}}
|
{{template "repo/issue/view_content/context_menu" dict "ConvContentType" "issue_main" "ConvContentItem" .Issue}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui attached segment comment-body" role="article">
|
<div class="ui attached segment comment-body" role="article">
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
{{if not $.Repository.IsArchived}}
|
{{if not $.Repository.IsArchived}}
|
||||||
{{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
|
{{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{template "repo/issue/view_content/context_menu" dict "item" . "delete" true "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}}
|
{{template "repo/issue/view_content/context_menu" dict "ConvContentType" "issue_comment" "ConvContentItem" $comment}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui attached segment comment-body" role="article">
|
<div class="ui attached segment comment-body" role="article">
|
||||||
@@ -424,7 +424,7 @@
|
|||||||
{{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}}
|
{{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}}
|
||||||
{{if not $.Repository.IsArchived}}
|
{{if not $.Repository.IsArchived}}
|
||||||
{{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
|
{{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
|
||||||
{{template "repo/issue/view_content/context_menu" dict "item" . "delete" false "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}}
|
{{template "repo/issue/view_content/context_menu" dict "ConvContentType" "pull_review" "ConvContentItem" $comment}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,44 +1,45 @@
|
|||||||
<div class="item action ui dropdown jump pointing top right context-dropdown">
|
{{$type := $.ConvContentType -}}
|
||||||
<a class="context-menu muted">
|
{{$item := $.ConvContentItem -}}
|
||||||
{{svg "octicon-kebab-horizontal"}}
|
{{$page := $.ConvContentPage -}}
|
||||||
</a>
|
|
||||||
|
{{$supportDelete := eq $type "issue_comment" -}}
|
||||||
|
{{$isPagePrFiles := eq $page "pr-files" -}}
|
||||||
|
{{$issueHTMLURL := ctx.RootData.Issue.HTMLURL ctx -}}
|
||||||
|
{{$referenceHTMLURL := printf "%s%s#%s" $issueHTMLURL (Iif $isPagePrFiles "/files" "") $item.HashTag -}}
|
||||||
|
|
||||||
|
{{$isCommentPoster := and ctx.RootData.IsSigned (eq ctx.RootData.SignedUserID $item.PosterID) -}}
|
||||||
|
{{$canEditContent := or ctx.RootData.Permission.IsAdmin ctx.RootData.HasIssuesOrPullsWritePermission $isCommentPoster -}}
|
||||||
|
|
||||||
|
<div class="item action ui dropdown jump pointing top right">
|
||||||
|
{{svg "octicon-kebab-horizontal"}}
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
{{$issueHTMLURL := ctx.RootData.Issue.HTMLURL ctx}}
|
<div class="item js-aria-clickable" data-clipboard-text="{{$referenceHTMLURL}}">{{ctx.Locale.Tr "copy_link"}}</div>
|
||||||
{{$referenceHTMLURL := ""}}
|
<div class="item js-aria-clickable" data-clipboard-target="#{{$item.HashTag}}-raw">{{ctx.Locale.Tr "repo.issues.context.copy_source"}}</div>
|
||||||
{{if .issue}}{{/* boolean value: is issue */}}
|
|
||||||
{{$referenceHTMLURL = printf "%s#%s" $issueHTMLURL .item.HashTag}}
|
|
||||||
{{else}}
|
|
||||||
{{$referenceHTMLURL = printf "%s/files#%s" $issueHTMLURL .item.HashTag}}
|
|
||||||
{{end}}
|
|
||||||
<div class="item context js-aria-clickable" data-clipboard-text="{{$referenceHTMLURL}}">{{ctx.Locale.Tr "copy_link"}}</div>
|
|
||||||
<div class="item context js-aria-clickable" data-clipboard-target="#{{.item.HashTag}}-raw">{{ctx.Locale.Tr "repo.issues.context.copy_source"}}</div>
|
|
||||||
{{if ctx.RootData.IsSigned}}
|
{{if ctx.RootData.IsSigned}}
|
||||||
{{$needDivider := false}}
|
|
||||||
{{if not ctx.RootData.Repository.IsArchived}}
|
{{if not ctx.RootData.Repository.IsArchived}}
|
||||||
{{$needDivider = true}}
|
<div class="item js-aria-clickable" data-global-click="onConvContentQuoteReply" data-shortcut-keys="r" data-shortcut-handler="quoteSelectedReply" data-target="{{$item.HashTag}}-raw">{{ctx.Locale.Tr "repo.issues.context.quote_reply"}}</div>
|
||||||
<div class="item context js-aria-clickable quote-reply {{if .diff}}quote-reply-diff{{end}}" data-target="{{.item.HashTag}}-raw">{{ctx.Locale.Tr "repo.issues.context.quote_reply"}}</div>
|
|
||||||
{{if not ctx.Consts.RepoUnitTypeIssues.UnitGlobalDisabled}}
|
{{if not ctx.Consts.RepoUnitTypeIssues.UnitGlobalDisabled}}
|
||||||
<div class="item context js-aria-clickable reference-issue" data-target="{{.item.HashTag}}-raw" data-modal="#reference-issue-modal" data-poster="{{.item.Poster.GetDisplayName}}" data-poster-username="{{.item.Poster.Name}}" data-reference="{{$referenceHTMLURL}}">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</div>
|
<div class="item js-aria-clickable reference-issue" data-target="{{$item.HashTag}}-raw" data-modal="#reference-issue-modal" data-poster="{{$item.Poster.GetDisplayName}}" data-poster-username="{{$item.Poster.Name}}" data-reference="{{$referenceHTMLURL}}">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if or ctx.RootData.Permission.IsAdmin .IsCommentPoster ctx.RootData.HasIssuesOrPullsWritePermission}}
|
{{if $canEditContent}}
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<div class="item context js-aria-clickable edit-content">{{ctx.Locale.Tr "repo.issues.context.edit"}}</div>
|
<div class="item js-aria-clickable" data-global-click="onConvContentEdit">{{ctx.Locale.Tr "repo.issues.context.edit"}}</div>
|
||||||
{{if .delete}}
|
{{if $supportDelete}}
|
||||||
<div class="item context js-aria-clickable delete-comment" data-comment-id={{.item.HashTag}} data-url="{{ctx.RootData.RepoLink}}/comments/{{.item.ID}}/delete" data-locale="{{ctx.Locale.Tr "repo.issues.delete_comment_confirm"}}">{{ctx.Locale.Tr "repo.issues.context.delete"}}</div>
|
<div class="item js-aria-clickable" data-global-click="onConvContentDelete" data-comment-id={{$item.HashTag}} data-url="{{ctx.RootData.RepoLink}}/comments/{{$item.ID}}/delete" data-locale="{{ctx.Locale.Tr "repo.issues.delete_comment_confirm"}}">{{ctx.Locale.Tr "repo.issues.context.delete"}}</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{$canUserBlock := call ctx.RootData.CanBlockUser ctx.RootData.SignedUser .item.Poster}}
|
|
||||||
{{$canOrgBlock := and ctx.RootData.Repository.Owner.IsOrganization (call ctx.RootData.CanBlockUser ctx.RootData.Repository.Owner .item.Poster)}}
|
{{$canUserBlock := call ctx.RootData.CanBlockUser ctx.RootData.SignedUser $item.Poster -}}
|
||||||
|
{{$canOrgBlock := and ctx.RootData.Repository.Owner.IsOrganization (call ctx.RootData.CanBlockUser ctx.RootData.Repository.Owner $item.Poster) -}}
|
||||||
{{if or $canOrgBlock $canUserBlock}}
|
{{if or $canOrgBlock $canUserBlock}}
|
||||||
{{if $needDivider}}
|
<div class="divider"></div>
|
||||||
<div class="divider"></div>
|
|
||||||
{{end}}
|
|
||||||
{{if $canUserBlock}}
|
{{if $canUserBlock}}
|
||||||
<div class="item context js-aria-clickable show-modal" data-modal="#block-user-modal" data-modal-modal-blockee="{{.item.Poster.Name}}" data-modal-modal-blockee-name="{{.item.Poster.GetDisplayName}}" data-modal-modal-form.url="{{AppSubUrl}}/user/settings/blocked_users">{{ctx.Locale.Tr "user.block.block.user"}}</div>
|
<div class="item js-aria-clickable show-modal" data-modal="#block-user-modal" data-modal-modal-blockee="{{$item.Poster.Name}}" data-modal-modal-blockee-name="{{$item.Poster.GetDisplayName}}" data-modal-modal-form.url="{{AppSubUrl}}/user/settings/blocked_users">{{ctx.Locale.Tr "user.block.block.user"}}</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if $canOrgBlock}}
|
{{if $canOrgBlock}}
|
||||||
<div class="item context js-aria-clickable show-modal" data-modal="#block-user-modal" data-modal-modal-blockee="{{.item.Poster.Name}}" data-modal-modal-blockee-name="{{.item.Poster.GetDisplayName}}" data-modal-modal-form.url="{{ctx.RootData.Repository.Owner.OrganisationLink}}/settings/blocked_users">{{ctx.Locale.Tr "user.block.block.org"}}</div>
|
<div class="item js-aria-clickable show-modal" data-modal="#block-user-modal" data-modal-modal-blockee="{{$item.Poster.Name}}" data-modal-modal-blockee-name="{{$item.Poster.GetDisplayName}}" data-modal-modal-form.url="{{ctx.RootData.Repository.Owner.OrganisationLink}}/settings/blocked_users">{{ctx.Locale.Tr "user.block.block.org"}}</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ At the moment, two kinds of request handler call this template:
|
|||||||
* Other compare and diff pages -> ... -> {section_unified.tmpl|section_split.tmpl} -> this)
|
* Other compare and diff pages -> ... -> {section_unified.tmpl|section_split.tmpl} -> this)
|
||||||
The variables in "ctx.Data" are different in each case, making this template fragile, hard to read and maintain. */}}
|
The variables in "ctx.Data" are different in each case, making this template fragile, hard to read and maintain. */}}
|
||||||
{{if len .comments}}
|
{{if len .comments}}
|
||||||
{{$comment := index .comments 0}}
|
{{$firstComment := index .comments 0}}
|
||||||
{{$invalid := $comment.Invalidated}}
|
{{$invalid := $firstComment.Invalidated}}
|
||||||
{{$resolved := $comment.IsResolved}}
|
{{$resolved := $firstComment.IsResolved}}
|
||||||
{{$resolveDoer := $comment.ResolveDoer}}
|
{{$resolveDoer := $firstComment.ResolveDoer}}
|
||||||
{{$hasReview := and $comment.Review}}
|
{{$hasReview := and $firstComment.Review}}
|
||||||
{{$isReviewPending := and $hasReview (eq $comment.Review.Type 0)}}
|
{{$isReviewPending := and $hasReview (eq $firstComment.Review.Type 0)}}
|
||||||
<div class="ui segments conversation-holder">
|
<div class="ui segments conversation-holder">
|
||||||
<div class="ui segment collapsible-comment-box tw-py-2 flex-left-right">
|
<div class="ui segment collapsible-comment-box tw-py-2 flex-left-right">
|
||||||
<div class="tw-flex tw-items-center">
|
<div class="tw-flex tw-items-center">
|
||||||
<a href="{{$comment.CodeCommentLink ctx}}" class="file-comment tw-ml-2 tw-break-anywhere">{{$comment.TreePath}}</a>
|
<a href="{{$firstComment.CodeCommentLink ctx}}" class="file-comment tw-ml-2 tw-break-anywhere">{{$firstComment.TreePath}}</a>
|
||||||
{{if $invalid}}
|
{{if $invalid}}
|
||||||
<span class="ui label basic small tw-ml-2" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}">
|
<span class="ui label basic small tw-ml-2" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}">
|
||||||
{{ctx.Locale.Tr "repo.issues.review.outdated"}}
|
{{ctx.Locale.Tr "repo.issues.review.outdated"}}
|
||||||
@@ -22,7 +22,7 @@ The variables in "ctx.Data" are different in each case, making this template fra
|
|||||||
</div>
|
</div>
|
||||||
<div class="tw-flex tw-items-center">
|
<div class="tw-flex tw-items-center">
|
||||||
{{if or $invalid $resolved}}
|
{{if or $invalid $resolved}}
|
||||||
<button type="button" id="show-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="{{if not $resolved}}tw-hidden{{end}} btn tiny show-outdated">
|
<button type="button" id="show-outdated-{{$firstComment.ID}}" data-comment="{{$firstComment.ID}}" class="{{if not $resolved}}tw-hidden{{end}} btn tiny show-outdated">
|
||||||
{{svg "octicon-unfold" 16 "tw-mr-2"}}
|
{{svg "octicon-unfold" 16 "tw-mr-2"}}
|
||||||
{{if $resolved}}
|
{{if $resolved}}
|
||||||
{{ctx.Locale.Tr "repo.issues.review.show_resolved"}}
|
{{ctx.Locale.Tr "repo.issues.review.show_resolved"}}
|
||||||
@@ -30,7 +30,7 @@ The variables in "ctx.Data" are different in each case, making this template fra
|
|||||||
{{ctx.Locale.Tr "repo.issues.review.show_outdated"}}
|
{{ctx.Locale.Tr "repo.issues.review.show_outdated"}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" id="hide-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="{{if $resolved}}tw-hidden {{end}} btn tiny hide-outdated">
|
<button type="button" id="hide-outdated-{{$firstComment.ID}}" data-comment="{{$firstComment.ID}}" class="{{if $resolved}}tw-hidden {{end}} btn tiny hide-outdated">
|
||||||
{{svg "octicon-fold" 16 "tw-mr-2"}}
|
{{svg "octicon-fold" 16 "tw-mr-2"}}
|
||||||
{{if $resolved}}
|
{{if $resolved}}
|
||||||
{{ctx.Locale.Tr "repo.issues.review.hide_resolved"}}
|
{{ctx.Locale.Tr "repo.issues.review.hide_resolved"}}
|
||||||
@@ -41,10 +41,10 @@ The variables in "ctx.Data" are different in each case, making this template fra
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{$diff := (CommentMustAsDiff ctx $comment)}}
|
{{$diff := (CommentMustAsDiff ctx $firstComment)}}
|
||||||
{{if $diff}}
|
{{if $diff}}
|
||||||
{{$file := (index $diff.Files 0)}}
|
{{$file := (index $diff.Files 0)}}
|
||||||
<div id="code-preview-{{$comment.ID}}" class="ui table segment{{if $resolved}} tw-hidden{{end}}">
|
<div id="code-preview-{{$firstComment.ID}}" class="ui table segment{{if $resolved}} tw-hidden{{end}}">
|
||||||
<div class="diff-file-box file-content {{TabSizeClass $.Editorconfig $file.Name}}">
|
<div class="diff-file-box file-content {{TabSizeClass $.Editorconfig $file.Name}}">
|
||||||
<div class="file-body file-code code-view code-diff code-diff-unified unicode-escaped">
|
<div class="file-body file-code code-view code-diff code-diff-unified unicode-escaped">
|
||||||
<table>
|
<table>
|
||||||
@@ -56,9 +56,9 @@ The variables in "ctx.Data" are different in each case, making this template fra
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
<div id="code-comments-{{$comment.ID}}" class="comment-code-cloud ui segment{{if $resolved}} tw-hidden{{end}}">
|
<div id="code-comments-{{$firstComment.ID}}" class="comment-code-cloud ui segment{{if $resolved}} tw-hidden{{end}}">
|
||||||
<div class="ui comments tw-mb-0">
|
<div class="ui comments tw-mb-0">
|
||||||
{{range .comments}}
|
{{range $comment := .comments}}
|
||||||
{{$createdSubStr:= DateUtils.TimeSince .CreatedUnix}}
|
{{$createdSubStr:= DateUtils.TimeSince .CreatedUnix}}
|
||||||
<div class="comment code-comment" id="{{.HashTag}}">
|
<div class="comment code-comment" id="{{.HashTag}}">
|
||||||
<div class="content comment-container">
|
<div class="content comment-container">
|
||||||
@@ -88,7 +88,7 @@ The variables in "ctx.Data" are different in each case, making this template fra
|
|||||||
{{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}}
|
{{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}}
|
||||||
{{if not $.Repository.IsArchived}}
|
{{if not $.Repository.IsArchived}}
|
||||||
{{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
|
{{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
|
||||||
{{template "repo/issue/view_content/context_menu" dict "item" . "delete" true "issue" true "diff" true "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}}
|
{{template "repo/issue/view_content/context_menu" dict "ConvContentType" "issue_comment" "ConvContentItem" $comment}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -125,7 +125,7 @@ The variables in "ctx.Data" are different in each case, making this template fra
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-text-block">
|
<div class="flex-text-block">
|
||||||
{{if and $.CanMarkConversation $hasReview (not $isReviewPending)}}
|
{{if and $.CanMarkConversation $hasReview (not $isReviewPending)}}
|
||||||
<button type="button" class="ui tiny basic button resolve-conversation" data-origin="timeline" data-action="{{if not $resolved}}Resolve{{else}}UnResolve{{end}}" data-comment-id="{{$comment.ID}}" data-update-url="{{$.RepoLink}}/issues/resolve_conversation">
|
<button type="button" class="ui tiny basic button resolve-conversation" data-origin="timeline" data-action="{{if not $resolved}}Resolve{{else}}UnResolve{{end}}" data-comment-id="{{$firstComment.ID}}" data-update-url="{{$.RepoLink}}/issues/resolve_conversation">
|
||||||
{{if $resolved}}
|
{{if $resolved}}
|
||||||
{{ctx.Locale.Tr "repo.issues.review.un_resolve_conversation"}}
|
{{ctx.Locale.Tr "repo.issues.review.un_resolve_conversation"}}
|
||||||
{{else}}
|
{{else}}
|
||||||
@@ -140,7 +140,7 @@ The variables in "ctx.Data" are different in each case, making this template fra
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{template "repo/diff/comment_form_datahandler" dict "hidden" true "reply" $comment.ReviewID "root" $ "comment" $comment}}
|
{{template "repo/diff/comment_form_datahandler" dict "hidden" true "reply" $firstComment.ReviewID "root" $ "comment" $firstComment}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ test('pr review flow', async ({page, request}) => {
|
|||||||
request,
|
request,
|
||||||
poster,
|
poster,
|
||||||
repoName,
|
repoName,
|
||||||
[{path: 'added.txt', content: 'new content\n'}],
|
[{path: 'added.txt', content: 'new content\nsecond line\n'}],
|
||||||
{branch: 'main', newBranch: 'feat', headers: posterHeaders},
|
{branch: 'main', newBranch: 'feat', headers: posterHeaders},
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
@@ -80,10 +80,10 @@ test('pr review flow', async ({page, request}) => {
|
|||||||
|
|
||||||
await page.goto(`${pullUrl}/files`);
|
await page.goto(`${pullUrl}/files`);
|
||||||
|
|
||||||
// diff viewer renders the added file with its header and one added-line row
|
// diff viewer renders the added file with its header and added-line rows
|
||||||
const fileBox = page.locator('.diff-file-box[data-new-filename="added.txt"]');
|
const fileBox = page.locator('.diff-file-box[data-new-filename="added.txt"]');
|
||||||
await expect(fileBox.locator('.diff-file-header .file-link')).toHaveText('added.txt');
|
await expect(fileBox.locator('.diff-file-header .file-link')).toHaveText('added.txt');
|
||||||
await expect(fileBox.locator('tr.add-code')).toHaveCount(1);
|
await expect(fileBox.locator('tr.add-code')).toHaveCount(2);
|
||||||
|
|
||||||
// commits tab badge reflects the single PR commit, and the diff stats header counts one changed file
|
// commits tab badge reflects the single PR commit, and the diff stats header counts one changed file
|
||||||
const commitsTab = page.locator('.ui.pull.tabular.menu a.item', {has: page.locator('.octicon-git-commit')});
|
const commitsTab = page.locator('.ui.pull.tabular.menu a.item', {has: page.locator('.octicon-git-commit')});
|
||||||
@@ -101,11 +101,29 @@ test('pr review flow', async ({page, request}) => {
|
|||||||
await logout(page);
|
await logout(page);
|
||||||
await loginUser(page, reviewer);
|
await loginUser(page, reviewer);
|
||||||
await page.goto(`${pullUrl}/files`);
|
await page.goto(`${pullUrl}/files`);
|
||||||
await page.locator('#review-box .js-btn-review').click();
|
await fileBox.locator('tr.add-code').nth(1).getByRole('button', {name: 'Add line comment', exact: true}).click();
|
||||||
|
const pendingForm = fileBox.locator('form').filter({has: page.getByRole('button', {name: 'Start review', exact: true})});
|
||||||
|
await pendingForm.getByRole('textbox', {name: 'Leave a comment', exact: true}).fill('pending review comment');
|
||||||
|
await pendingForm.getByRole('button', {name: 'Start review', exact: true}).click();
|
||||||
|
const counter = page.locator('.review-box-button .review-comments-counter');
|
||||||
|
await expect(counter).toHaveAttribute('data-pending-comment-number', '1');
|
||||||
|
await expect(counter).toHaveText('1');
|
||||||
|
const pendingComment = fileBox.locator('.comment').filter({hasText: 'pending review comment'});
|
||||||
|
await expect(pendingComment.locator('[data-clipboard-text]')).toHaveAttribute('data-clipboard-text', /\/files#issuecomment-\d+$/);
|
||||||
|
await pendingComment.getByRole('menu').filter({has: page.getByRole('menuitem', {name: 'Delete', exact: true, includeHidden: true})}).click();
|
||||||
|
page.once('dialog', (dialog) => dialog.accept());
|
||||||
|
await pendingComment.getByRole('menuitem', {name: 'Delete', exact: true}).click();
|
||||||
|
await expect(pendingComment).toHaveCount(0);
|
||||||
|
await expect(counter).toHaveAttribute('data-pending-comment-number', '0');
|
||||||
|
await expect(counter).toHaveText('0');
|
||||||
|
|
||||||
|
await page.getByRole('button', {name: 'Review', exact: true}).click();
|
||||||
const panel = page.locator('.review-box-panel');
|
const panel = page.locator('.review-box-panel');
|
||||||
await panel.locator('textarea[name="content"]').fill(`First approval from ${reviewer}`);
|
await panel.locator('textarea[name="content"]').fill(`First approval from ${reviewer}`);
|
||||||
await panel.getByRole('button', {name: 'Approve', exact: true}).click();
|
await panel.getByRole('button', {name: 'Approve', exact: true}).click();
|
||||||
await expect(page.locator('.timeline-item').filter({hasText: `First approval from ${reviewer}`})).toBeVisible();
|
const reviewComment = page.locator('.timeline-item').filter({hasText: `First approval from ${reviewer}`});
|
||||||
|
await expect(reviewComment).toBeVisible();
|
||||||
|
await expect(reviewComment.locator('[data-clipboard-text]')).toHaveAttribute('data-clipboard-text', /\/pulls\/\d+#issuecomment-\d+$/);
|
||||||
|
|
||||||
await reRequestAndApprove(reviewer, 'Uncounted approval', 'tw-text-text-light', false);
|
await reRequestAndApprove(reviewer, 'Uncounted approval', 'tw-text-text-light', false);
|
||||||
|
|
||||||
|
|||||||
@@ -40,8 +40,9 @@ func testPullDiffSingleHeadCommitReviewFormAction(t *testing.T) {
|
|||||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
doc := NewHTMLParser(t, resp.Body)
|
doc := NewHTMLParser(t, resp.Body)
|
||||||
|
|
||||||
btn := doc.Find(".js-btn-review")
|
btn := doc.Find(".review-box-button")
|
||||||
assert.True(t, btn.Length() == 1 && !btn.HasClass("disabled"))
|
require.Equal(t, 1, btn.Length())
|
||||||
|
assert.False(t, btn.Is("[disabled]"))
|
||||||
form := doc.Find(".review-box-panel form")
|
form := doc.Find(".review-box-panel form")
|
||||||
assert.Equal(t, 1, form.Length())
|
assert.Equal(t, 1, form.Length())
|
||||||
assert.Equal(t, "/user2/commitsonpr/pulls/1/files/reviews/submit", form.AttrOr("action", ""))
|
assert.Equal(t, "/user2/commitsonpr/pulls/1/files/reviews/submit", form.AttrOr("action", ""))
|
||||||
@@ -70,7 +71,9 @@ func testPullDiffAssertPage(t *testing.T, prDiffURL string, reviewBtnDisabled bo
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Ensure the review button is enabled for full PR reviews
|
// Ensure the review button is enabled for full PR reviews
|
||||||
assert.Equal(t, reviewBtnDisabled, doc.Find(".js-btn-review").HasClass("disabled"))
|
btn := doc.Find(".review-box-button")
|
||||||
|
require.Equal(t, 1, btn.Length())
|
||||||
|
assert.Equal(t, reviewBtnDisabled, btn.Is("[disabled]"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLongLineDiffRendering(t *testing.T) {
|
func TestLongLineDiffRendering(t *testing.T) {
|
||||||
|
|||||||
@@ -131,20 +131,21 @@
|
|||||||
max-width: calc(100vw - 70px); /* leave enough space on left, and align the page content */
|
max-width: calc(100vw - 70px); /* leave enough space on left, and align the page content */
|
||||||
}
|
}
|
||||||
|
|
||||||
#review-box {
|
.ui.button.review-box-button {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
padding-right: 0.5em !important; /* override .repository .diff-detail-box .ui.button */
|
||||||
}
|
}
|
||||||
|
|
||||||
#review-box .review-comments-counter {
|
.review-box-button .review-comments-counter {
|
||||||
background-color: var(--color-primary-light-4);
|
background-color: var(--color-primary-light-4);
|
||||||
color: var(--color-primary-contrast);
|
color: var(--color-primary-contrast);
|
||||||
}
|
}
|
||||||
|
|
||||||
#review-box:hover .review-comments-counter {
|
.review-box-button:hover .review-comments-counter {
|
||||||
background-color: var(--color-primary-light-5);
|
background-color: var(--color-primary-light-5);
|
||||||
}
|
}
|
||||||
|
|
||||||
#review-box .review-comments-counter[data-pending-comment-number="0"] {
|
.review-box-button .review-comments-counter[data-pending-comment-number="0"] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ function initRepoDiffConversationForm() {
|
|||||||
|
|
||||||
// the default behavior is to add a pending review, so if no submitter, it also means "pending_review"
|
// the default behavior is to add a pending review, so if no submitter, it also means "pending_review"
|
||||||
if (!submitter || submitter?.matches('button[name="pending_review"]')) {
|
if (!submitter || submitter?.matches('button[name="pending_review"]')) {
|
||||||
const reviewBox = document.querySelector('#review-box')!;
|
const reviewBox = document.querySelector('.review-box-button')!;
|
||||||
const counter = reviewBox?.querySelector('.review-comments-counter');
|
const counter = reviewBox?.querySelector('.review-comments-counter');
|
||||||
if (!counter) return;
|
if (!counter) return;
|
||||||
const num = parseInt(counter.getAttribute('data-pending-comment-number')!) + 1 || 1;
|
const num = parseInt(counter.getAttribute('data-pending-comment-number')!) + 1 || 1;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import {tryShortcutQuoteReply} from './repo-issue-edit.ts';
|
import {tryQuoteSelectedReply} from './repo-issue-edit.ts';
|
||||||
|
|
||||||
test('tryShortcutQuoteReply', () => {
|
test('tryQuoteSelectedReply', () => {
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="comment-container">
|
<div class="comment-container">
|
||||||
<button class="quote-reply">Quote reply</button>
|
<button data-shortcut-handler="quoteSelectedReply">Quote reply</button>
|
||||||
<div class="render-content markup">Comment text</div>
|
<div class="render-content markup">Comment text</div>
|
||||||
<div class="raw-content">Raw text</div>
|
<div class="raw-content">Raw text</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -20,24 +20,24 @@ test('tryShortcutQuoteReply', () => {
|
|||||||
button.addEventListener('click', onClick);
|
button.addEventListener('click', onClick);
|
||||||
|
|
||||||
selection.removeAllRanges();
|
selection.removeAllRanges();
|
||||||
expect(tryShortcutQuoteReply()).toBe(false);
|
expect(tryQuoteSelectedReply()).toBe(false);
|
||||||
|
|
||||||
selection.selectAllChildren(container.querySelector('.raw-content')!);
|
selection.selectAllChildren(container.querySelector('.raw-content')!);
|
||||||
expect(tryShortcutQuoteReply()).toBe(false);
|
expect(tryQuoteSelectedReply()).toBe(false);
|
||||||
|
|
||||||
const range = document.createRange();
|
const range = document.createRange();
|
||||||
range.selectNodeContents(container.querySelector('.render-content')!.firstChild!);
|
range.selectNodeContents(container.querySelector('.render-content')!.firstChild!);
|
||||||
selection.removeAllRanges();
|
selection.removeAllRanges();
|
||||||
selection.addRange(range);
|
selection.addRange(range);
|
||||||
selection.collapseToStart();
|
selection.collapseToStart();
|
||||||
expect(tryShortcutQuoteReply()).toBe(false);
|
expect(tryQuoteSelectedReply()).toBe(false);
|
||||||
expect(onClick).not.toHaveBeenCalled();
|
expect(onClick).not.toHaveBeenCalled();
|
||||||
|
|
||||||
selection.removeAllRanges();
|
selection.removeAllRanges();
|
||||||
selection.addRange(range);
|
selection.addRange(range);
|
||||||
expect(tryShortcutQuoteReply()).toBe(true);
|
expect(tryQuoteSelectedReply()).toBe(true);
|
||||||
expect(onClick).toHaveBeenCalledTimes(1);
|
expect(onClick).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
button.remove();
|
button.remove();
|
||||||
expect(tryShortcutQuoteReply()).toBe(false);
|
expect(tryQuoteSelectedReply()).toBe(false);
|
||||||
});
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import {handleReply} from './repo-issue.ts';
|
import {handleCommentReply} from './repo-issue.ts';
|
||||||
import {getComboMarkdownEditor, initComboMarkdownEditor, ComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
|
import {getComboMarkdownEditor, initComboMarkdownEditor, ComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
|
||||||
import {POST} from '../modules/fetch.ts';
|
import {POST} from '../modules/fetch.ts';
|
||||||
import {showErrorToast} from '../modules/toast.ts';
|
import {showErrorToast} from '../modules/toast.ts';
|
||||||
@@ -8,12 +8,9 @@ import {triggerUploadStateChanged} from './comp/EditorUpload.ts';
|
|||||||
import {convertHtmlToMarkdown} from '../markup/html2markdown.ts';
|
import {convertHtmlToMarkdown} from '../markup/html2markdown.ts';
|
||||||
import {applyAreYouSure} from '../modules/are-you-sure.ts';
|
import {applyAreYouSure} from '../modules/are-you-sure.ts';
|
||||||
import {registerShortcutHandler} from '../modules/shortcut.ts';
|
import {registerShortcutHandler} from '../modules/shortcut.ts';
|
||||||
|
import {registerGlobalEventFunc} from '../modules/observer.ts';
|
||||||
|
|
||||||
async function tryOnEditContent(e: Event) {
|
async function onConvContentEdit(clickTarget: HTMLElement) {
|
||||||
const clickTarget = (e.target as HTMLElement).closest('.edit-content');
|
|
||||||
if (!clickTarget) return;
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
const commentContent = clickTarget.closest('.comment-header')!.nextElementSibling!;
|
const commentContent = clickTarget.closest('.comment-header')!.nextElementSibling!;
|
||||||
const editContentZone = commentContent.querySelector('.edit-content-zone')!;
|
const editContentZone = commentContent.querySelector('.edit-content-zone')!;
|
||||||
let renderContent = commentContent.querySelector('.render-content')!;
|
let renderContent = commentContent.querySelector('.render-content')!;
|
||||||
@@ -121,11 +118,7 @@ function extractSelectedMarkdown(container: HTMLElement) {
|
|||||||
return convertHtmlToMarkdown(el);
|
return convertHtmlToMarkdown(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function tryOnQuoteReply(e: Event) {
|
async function onConvContentQuoteReply(clickTarget: HTMLElement) {
|
||||||
const clickTarget = (e.target as HTMLElement).closest('.quote-reply');
|
|
||||||
if (!clickTarget) return;
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
const contentToQuoteId = clickTarget.getAttribute('data-target');
|
const contentToQuoteId = clickTarget.getAttribute('data-target');
|
||||||
const targetRawToQuote = document.querySelector<HTMLElement>(`#${contentToQuoteId}.raw-content`)!;
|
const targetRawToQuote = document.querySelector<HTMLElement>(`#${contentToQuoteId}.raw-content`)!;
|
||||||
const targetMarkupToQuote = targetRawToQuote.parentElement!.querySelector<HTMLElement>('.render-content.markup')!;
|
const targetMarkupToQuote = targetRawToQuote.parentElement!.querySelector<HTMLElement>('.render-content.markup')!;
|
||||||
@@ -134,9 +127,9 @@ async function tryOnQuoteReply(e: Event) {
|
|||||||
const quotedContent = `${contentToQuote.replace(/^/mg, '> ')}\n\n`;
|
const quotedContent = `${contentToQuote.replace(/^/mg, '> ')}\n\n`;
|
||||||
|
|
||||||
let editor;
|
let editor;
|
||||||
if (clickTarget.classList.contains('quote-reply-diff')) {
|
const btnReply = clickTarget.closest('.comment-code-cloud')?.querySelector<HTMLElement>('button.comment-form-reply');
|
||||||
const replyBtn = clickTarget.closest('.comment-code-cloud')!.querySelector<HTMLElement>('button.comment-form-reply')!;
|
if (btnReply) {
|
||||||
editor = await handleReply(replyBtn);
|
editor = await handleCommentReply(btnReply);
|
||||||
} else {
|
} else {
|
||||||
// for normal issue/comment page
|
// for normal issue/comment page
|
||||||
editor = getComboMarkdownEditor(document.querySelector('#comment-form .combo-markdown-editor'))!;
|
editor = getComboMarkdownEditor(document.querySelector('#comment-form .combo-markdown-editor'))!;
|
||||||
@@ -151,7 +144,7 @@ async function tryOnQuoteReply(e: Event) {
|
|||||||
editor.moveCursorToEnd();
|
editor.moveCursorToEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tryShortcutQuoteReply(): boolean {
|
export function tryQuoteSelectedReply(): boolean {
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
if (!selection?.rangeCount || selection.isCollapsed) return false;
|
if (!selection?.rangeCount || selection.isCollapsed) return false;
|
||||||
const node = selection.getRangeAt(0).commonAncestorContainer;
|
const node = selection.getRangeAt(0).commonAncestorContainer;
|
||||||
@@ -160,7 +153,7 @@ export function tryShortcutQuoteReply(): boolean {
|
|||||||
const commentContainer = el?.closest<HTMLElement>('.render-content.markup')?.closest('.comment-container');
|
const commentContainer = el?.closest<HTMLElement>('.render-content.markup')?.closest('.comment-container');
|
||||||
if (!commentContainer) return false;
|
if (!commentContainer) return false;
|
||||||
|
|
||||||
const quoteReplyBtn = commentContainer.querySelector<HTMLElement>('.quote-reply')!;
|
const quoteReplyBtn = commentContainer.querySelector<HTMLElement>('[data-shortcut-handler="quoteSelectedReply"]')!;
|
||||||
if (!quoteReplyBtn) return false;
|
if (!quoteReplyBtn) return false;
|
||||||
|
|
||||||
quoteReplyBtn.click();
|
quoteReplyBtn.click();
|
||||||
@@ -168,15 +161,10 @@ export function tryShortcutQuoteReply(): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function initRepoIssueCommentEdit() {
|
export function initRepoIssueCommentEdit() {
|
||||||
document.addEventListener('click', (e) => {
|
registerGlobalEventFunc('click', 'onConvContentQuoteReply', onConvContentQuoteReply);
|
||||||
tryOnEditContent(e); // Edit issue or comment content
|
registerGlobalEventFunc('click', 'onConvContentEdit', onConvContentEdit);
|
||||||
tryOnQuoteReply(e); // Quote reply to the comment editor
|
|
||||||
});
|
|
||||||
// TODO: it is not list in the shortcut help modal at the moment, there are many details:
|
|
||||||
// 1. not every page supports such "r"
|
|
||||||
// 2. "r" only works when there is a ".quote-reply" menu item, but it is not for a single one
|
|
||||||
// To make the framework fully support such mechanism correctly, we might also need to consider about dynamically added elements.
|
|
||||||
// The shortcut "r" was chosen by https://github.com/go-gitea/gitea/issues/5796#issuecomment-5528657082
|
// The shortcut "r" was chosen by https://github.com/go-gitea/gitea/issues/5796#issuecomment-5528657082
|
||||||
// and GitLab: https://gitlab.com/gitlab-org/gitlab-foss/-/commit/ca5d0c82509cedb94f9bfa4a40e77706a58faafe
|
// The same to GitHub and GitLab: https://gitlab.com/gitlab-org/gitlab-foss/-/commit/ca5d0c82509cedb94f9bfa4a40e77706a58faafe
|
||||||
registerShortcutHandler('r', tryShortcutQuoteReply);
|
registerShortcutHandler('quoteSelectedReply', tryQuoteSelectedReply);
|
||||||
}
|
}
|
||||||
@@ -17,8 +17,9 @@ import {initRepoIssueSidebar} from './repo-issue-sidebar.ts';
|
|||||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||||
import {showFomanticModal} from '../modules/fomantic/modal.ts';
|
import {showFomanticModal} from '../modules/fomantic/modal.ts';
|
||||||
import {ignoreAreYouSure} from '../modules/are-you-sure.ts';
|
import {ignoreAreYouSure} from '../modules/are-you-sure.ts';
|
||||||
import {registerGlobalInitFunc} from '../modules/observer.ts';
|
import {registerGlobalEventFunc, registerGlobalInitFunc} from '../modules/observer.ts';
|
||||||
import type {FomanticApiResponse} from '../types.ts';
|
import type {FomanticApiResponse} from '../types.ts';
|
||||||
|
import {performFetchActionRequest} from '../modules/fetch-action.ts';
|
||||||
|
|
||||||
const {appSubUrl} = window.config;
|
const {appSubUrl} = window.config;
|
||||||
|
|
||||||
@@ -94,69 +95,57 @@ export function initRepoIssueFilterItemLabel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function initRepoIssueCommentDelete() {
|
export function initRepoIssueCommentDelete() {
|
||||||
// Delete comment
|
registerGlobalEventFunc('click', 'onConvContentDelete', async (deleteButton) => {
|
||||||
document.addEventListener('click', async (e) => {
|
if (!window.confirm(deleteButton.getAttribute('data-locale')!)) return;
|
||||||
if (!(e.target as HTMLElement).matches('.delete-comment')) return;
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const deleteButton = e.target as HTMLElement;
|
const response = await performFetchActionRequest(deleteButton, {method: 'POST', url: deleteButton.getAttribute('data-url')!});
|
||||||
if (window.confirm(deleteButton.getAttribute('data-locale')!)) {
|
if (!response) return;
|
||||||
try {
|
|
||||||
const response = await POST(deleteButton.getAttribute('data-url')!);
|
|
||||||
if (!response.ok) throw new Error('Failed to delete comment');
|
|
||||||
|
|
||||||
const conversationHolder = deleteButton.closest('.conversation-holder');
|
const conversationHolder = deleteButton.closest('.conversation-holder');
|
||||||
const parentTimelineItem = deleteButton.closest('.timeline-item');
|
const parentTimelineItem = deleteButton.closest('.timeline-item');
|
||||||
const parentTimelineGroup = deleteButton.closest('.timeline-item-group');
|
const parentTimelineGroup = deleteButton.closest('.timeline-item-group');
|
||||||
|
|
||||||
// Check if this was a pending comment.
|
// Check if this was a pending comment.
|
||||||
if (conversationHolder?.querySelector('.pending-label')) {
|
if (conversationHolder?.querySelector('.pending-label')) {
|
||||||
const counter = document.querySelector('#review-box .review-comments-counter')!;
|
const counter = document.querySelector('.review-box-button .review-comments-counter')!;
|
||||||
let num = parseInt(counter?.getAttribute('data-pending-comment-number') || '') - 1 || 0;
|
let num = parseInt(counter?.getAttribute('data-pending-comment-number') || '') - 1 || 0;
|
||||||
num = Math.max(num, 0);
|
num = Math.max(num, 0);
|
||||||
counter.setAttribute('data-pending-comment-number', String(num));
|
counter.setAttribute('data-pending-comment-number', String(num));
|
||||||
counter.textContent = String(num);
|
counter.textContent = String(num);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector(`#${deleteButton.getAttribute('data-comment-id')}`)?.remove();
|
||||||
|
|
||||||
|
if (conversationHolder && !conversationHolder.querySelector('.comment')) {
|
||||||
|
const path = conversationHolder.getAttribute('data-path');
|
||||||
|
const side = conversationHolder.getAttribute('data-side');
|
||||||
|
const idx = conversationHolder.getAttribute('data-idx');
|
||||||
|
const lineType = conversationHolder.closest('tr')?.getAttribute('data-line-type');
|
||||||
|
|
||||||
|
// the conversation holder could appear either on the "Conversation" page, or the "Files Changed" page
|
||||||
|
// on the Conversation page, there is no parent "tr", so no need to do anything for "add-code-comment"
|
||||||
|
if (lineType) {
|
||||||
|
if (lineType === 'same') {
|
||||||
|
document.querySelector(`[data-path="${CSS.escape(String(path))}"] .add-code-comment[data-idx="${CSS.escape(String(idx))}"]`)!.classList.remove('tw-invisible');
|
||||||
|
} else {
|
||||||
|
document.querySelector(`[data-path="${CSS.escape(String(path))}"] .add-code-comment[data-side="${CSS.escape(String(side))}"][data-idx="${CSS.escape(String(idx))}"]`)!.classList.remove('tw-invisible');
|
||||||
}
|
}
|
||||||
|
|
||||||
document.querySelector(`#${deleteButton.getAttribute('data-comment-id')}`)?.remove();
|
|
||||||
|
|
||||||
if (conversationHolder && !conversationHolder.querySelector('.comment')) {
|
|
||||||
const path = conversationHolder.getAttribute('data-path');
|
|
||||||
const side = conversationHolder.getAttribute('data-side');
|
|
||||||
const idx = conversationHolder.getAttribute('data-idx');
|
|
||||||
const lineType = conversationHolder.closest('tr')?.getAttribute('data-line-type');
|
|
||||||
|
|
||||||
// the conversation holder could appear either on the "Conversation" page, or the "Files Changed" page
|
|
||||||
// on the Conversation page, there is no parent "tr", so no need to do anything for "add-code-comment"
|
|
||||||
if (lineType) {
|
|
||||||
if (lineType === 'same') {
|
|
||||||
document.querySelector(`[data-path="${CSS.escape(String(path))}"] .add-code-comment[data-idx="${CSS.escape(String(idx))}"]`)!.classList.remove('tw-invisible');
|
|
||||||
} else {
|
|
||||||
document.querySelector(`[data-path="${CSS.escape(String(path))}"] .add-code-comment[data-side="${CSS.escape(String(side))}"][data-idx="${CSS.escape(String(idx))}"]`)!.classList.remove('tw-invisible');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
conversationHolder.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if there is no review content, move the time avatar upward to avoid overlapping the content below.
|
|
||||||
if (!parentTimelineGroup?.querySelector('.timeline-item.comment') && !parentTimelineItem?.querySelector('.conversation-holder')) {
|
|
||||||
const timelineAvatar = parentTimelineGroup?.querySelector('.timeline-avatar');
|
|
||||||
timelineAvatar?.classList.remove('timeline-avatar-offset');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
}
|
||||||
|
conversationHolder.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there is no review content, move the time avatar upward to avoid overlapping the content below.
|
||||||
|
if (!parentTimelineGroup?.querySelector('.timeline-item.comment') && !parentTimelineItem?.querySelector('.conversation-holder')) {
|
||||||
|
const timelineAvatar = parentTimelineGroup?.querySelector('.timeline-avatar');
|
||||||
|
timelineAvatar?.classList.remove('timeline-avatar-offset');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initRepoIssueCodeCommentCancel() {
|
export function initRepoIssueCodeCommentCancel() {
|
||||||
// Cancel inline code comment
|
registerGlobalEventFunc('click', 'onCancelCodeComment', (el) => {
|
||||||
document.addEventListener('click', (e) => {
|
const form = el.closest('form')!;
|
||||||
if (!(e.target as HTMLElement).matches('.cancel-code-comment')) return;
|
if (form.classList.contains('comment-form')) {
|
||||||
|
|
||||||
const form = (e.target as HTMLElement).closest('form')!;
|
|
||||||
if (form?.classList.contains('comment-form')) {
|
|
||||||
hideElem(form);
|
hideElem(form);
|
||||||
showElem(form.closest('.comment-code-cloud')!.querySelectorAll('button.comment-form-reply'));
|
showElem(form.closest('.comment-code-cloud')!.querySelectorAll('button.comment-form-reply'));
|
||||||
} else {
|
} else {
|
||||||
@@ -186,7 +175,7 @@ export function initRepoIssueComments() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleReply(el: HTMLElement) {
|
export async function handleCommentReply(el: HTMLElement) {
|
||||||
const form = el.closest('.comment-code-cloud')!.querySelector('.comment-form')!;
|
const form = el.closest('.comment-code-cloud')!.querySelector('.comment-form')!;
|
||||||
const textarea = form.querySelector('textarea');
|
const textarea = form.querySelector('textarea');
|
||||||
|
|
||||||
@@ -242,15 +231,13 @@ export function initRepoPullRequestReview() {
|
|||||||
|
|
||||||
addDelegatedEventListener(document, 'click', 'button.comment-form-reply', (el, e) => {
|
addDelegatedEventListener(document, 'click', 'button.comment-form-reply', (el, e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleReply(el);
|
handleCommentReply(el);
|
||||||
});
|
});
|
||||||
|
|
||||||
// The following part is only for diff views
|
registerGlobalInitFunc('initPullReviewBox', (el) => {
|
||||||
if (!document.querySelector('.repository.pull.diff')) return;
|
const elReviewBtn = el.querySelector('.review-box-button')!;
|
||||||
|
const elReviewPanel = el.querySelector('.review-box-panel.tippy-target');
|
||||||
const elReviewBtn = document.querySelector('.js-btn-review');
|
if (!elReviewPanel) return;
|
||||||
const elReviewPanel = document.querySelector('.review-box-panel.tippy-target');
|
|
||||||
if (elReviewBtn && elReviewPanel) {
|
|
||||||
const tippy = createTippy(elReviewBtn, {
|
const tippy = createTippy(elReviewBtn, {
|
||||||
content: elReviewPanel,
|
content: elReviewPanel,
|
||||||
theme: 'default',
|
theme: 'default',
|
||||||
@@ -260,8 +247,8 @@ export function initRepoPullRequestReview() {
|
|||||||
interactive: true,
|
interactive: true,
|
||||||
hideOnClick: true,
|
hideOnClick: true,
|
||||||
});
|
});
|
||||||
elReviewPanel.querySelector('.close')!.addEventListener('click', () => tippy.hide());
|
elReviewPanel.querySelector('.close-review-panel')!.addEventListener('click', () => tippy.hide());
|
||||||
}
|
});
|
||||||
|
|
||||||
addDelegatedEventListener(document, 'click', '.add-code-comment', async (el, e) => {
|
addDelegatedEventListener(document, 'click', '.add-code-comment', async (el, e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@@ -42,13 +42,13 @@ export function initRepository() {
|
|||||||
registerGlobalInitFunc('initRepoPullRequestUpdate', initRepoPullRequestUpdate);
|
registerGlobalInitFunc('initRepoPullRequestUpdate', initRepoPullRequestUpdate);
|
||||||
registerGlobalInitFunc('initRepoCitationPanel', initRepoCitationPanel);
|
registerGlobalInitFunc('initRepoCitationPanel', initRepoCitationPanel);
|
||||||
|
|
||||||
|
// TODO: need to refactor more legacy if-querySelector-based "init" code to "data-global-xxx" framework in the future.
|
||||||
const pageContent = document.querySelector('.page-content.repository');
|
const pageContent = document.querySelector('.page-content.repository');
|
||||||
if (!pageContent) return;
|
if (!pageContent) return;
|
||||||
|
|
||||||
initRepoBranchTagSelector();
|
initRepoBranchTagSelector();
|
||||||
initRepoCommentFormAndSidebar();
|
initRepoCommentFormAndSidebar();
|
||||||
|
|
||||||
// Labels
|
|
||||||
initCompLabelEdit('.page-content.repository.labels');
|
initCompLabelEdit('.page-content.repository.labels');
|
||||||
initRepoMilestone();
|
initRepoMilestone();
|
||||||
initRepoNew();
|
initRepoNew();
|
||||||
@@ -57,18 +57,17 @@ export function initRepository() {
|
|||||||
initRepoSettings();
|
initRepoSettings();
|
||||||
initRepoIssueWipNewTitle();
|
initRepoIssueWipNewTitle();
|
||||||
|
|
||||||
// Issues
|
initRepoIssueCommentEdit();
|
||||||
if (pageContent.matches('.page-content.repository.view.issue')) {
|
initRepoIssueCommentDelete();
|
||||||
initRepoIssueCommentEdit();
|
initRepoIssueCodeCommentCancel();
|
||||||
|
|
||||||
|
if (pageContent.matches('.page-content.repository.view.issue')) {
|
||||||
initRepoIssueTitleEdit();
|
initRepoIssueTitleEdit();
|
||||||
initRepoIssueWipToggle();
|
initRepoIssueWipToggle();
|
||||||
initRepoIssueComments();
|
initRepoIssueComments();
|
||||||
|
|
||||||
initRepoIssueReferenceIssue();
|
initRepoIssueReferenceIssue();
|
||||||
|
|
||||||
initRepoIssueCommentDelete();
|
|
||||||
initRepoIssueCodeCommentCancel();
|
|
||||||
initCompReactionSelector();
|
initCompReactionSelector();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {registerGlobalInitFunc} from './observer.ts';
|
import {registerGlobalInitFunc} from './observer.ts';
|
||||||
import {createElementFromHTML, hideElem, toggleElem} from '../utils/dom.ts';
|
import {createElementFromHTML, hideElem, toggleElem} from '../utils/dom.ts';
|
||||||
import {showFomanticModal} from './fomantic/modal.ts';
|
import {showFomanticModal} from './fomantic/modal.ts';
|
||||||
import {html} from '../utils/html.ts';
|
import {html, htmlRaw} from '../utils/html.ts';
|
||||||
|
|
||||||
type ShortcutHandler = (el: HTMLElement) => boolean;
|
type ShortcutHandler = (el: HTMLElement) => boolean;
|
||||||
|
|
||||||
@@ -87,6 +87,7 @@ function showShortcutHelp() {
|
|||||||
document.body.append(modal);
|
document.body.append(modal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const seenShortcuts = new Set<string>();
|
||||||
const list = modal.querySelector<HTMLElement>('.shortcut-list')!;
|
const list = modal.querySelector<HTMLElement>('.shortcut-list')!;
|
||||||
list.replaceChildren(createElementFromHTML(html`<tr><td><kbd>?</kbd></td><td>Keyboard Shortcuts</td></tr>`));
|
list.replaceChildren(createElementFromHTML(html`<tr><td><kbd>?</kbd></td><td>Keyboard Shortcuts</td></tr>`));
|
||||||
for (const elem of document.querySelectorAll<HTMLElement>('[data-shortcut-keys]')) {
|
for (const elem of document.querySelectorAll<HTMLElement>('[data-shortcut-keys]')) {
|
||||||
@@ -99,15 +100,24 @@ function showShortcutHelp() {
|
|||||||
target.getAttribute('placeholder') ||
|
target.getAttribute('placeholder') ||
|
||||||
target.getAttribute('title') ||
|
target.getAttribute('title') ||
|
||||||
getShortcutTextContent(target);
|
getShortcutTextContent(target);
|
||||||
list.append(createElementFromHTML(html`<tr><td><kbd>${elem.getAttribute('data-shortcut-keys')}</kbd></td><td>${label}</td></tr>`));
|
|
||||||
|
const shortcutKeys = elem.getAttribute('data-shortcut-keys')!;
|
||||||
|
const shortcutHandler = elem.getAttribute('data-shortcut-handler') ?? '';
|
||||||
|
const shortcutIdentifier = `${shortcutKeys}::${shortcutHandler}`;
|
||||||
|
if (seenShortcuts.has(shortcutIdentifier)) continue;
|
||||||
|
seenShortcuts.add(shortcutIdentifier);
|
||||||
|
let keysHtml = '';
|
||||||
|
for (const keys of shortcutKeys.split(' ')) {
|
||||||
|
keysHtml += `<kbd>${keys}</kbd> `;
|
||||||
|
}
|
||||||
|
list.append(createElementFromHTML(html`<tr><td>${htmlRaw(keysHtml)}</td><td>${label}</td></tr>`));
|
||||||
}
|
}
|
||||||
showFomanticModal(modal);
|
showFomanticModal(modal);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerShortcutHandler(fullKey: string, handler: ShortcutHandler) {
|
export function registerShortcutHandler(name: string, handler: ShortcutHandler) {
|
||||||
fullKey = fullKey.toLowerCase();
|
shortcutHandlers[name] ||= [];
|
||||||
shortcutHandlers[fullKey] ||= [];
|
shortcutHandlers[name].push(handler);
|
||||||
shortcutHandlers[fullKey].push(handler);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initGlobalShortcut() {
|
export function initGlobalShortcut() {
|
||||||
@@ -169,20 +179,22 @@ export function initGlobalShortcut() {
|
|||||||
shortcutPresses.push({fullKey, pressTime});
|
shortcutPresses.push({fullKey, pressTime});
|
||||||
|
|
||||||
const fullKeys = shortcutPresses.map((p) => p.fullKey).join(' ');
|
const fullKeys = shortcutPresses.map((p) => p.fullKey).join(' ');
|
||||||
for (const handler of shortcutHandlers[fullKeys.toLowerCase()] || []) {
|
|
||||||
if (handler(target)) {
|
|
||||||
e.preventDefault();
|
|
||||||
shortcutPresses.length = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// It's said that "spaces in aria-keyshortcuts separate alternative shortcuts", but we want to support multi-key shortcuts
|
// It's said that "spaces in aria-keyshortcuts separate alternative shortcuts", but we want to support multi-key shortcuts
|
||||||
// So here we use our own data attribute to store the full shortcut keys, and match it case-insensitively.
|
// So here we use our own data attribute to store the full shortcut keys, and match it case-insensitively.
|
||||||
const matchedElems = document.querySelectorAll<HTMLElement>(`[data-shortcut-keys="${CSS.escape(fullKeys)}" i]`);
|
const matchedElems = document.querySelectorAll<HTMLElement>(`[data-shortcut-keys="${CSS.escape(fullKeys)}" i]`);
|
||||||
if (!matchedElems.length) return;
|
if (!matchedElems.length) return;
|
||||||
// TODO: if there are multiple matches, maybe we could introduce some priority rules to determine which one to trigger
|
// TODO: if there are multiple matches, maybe we could introduce some priority rules to determine which one to trigger
|
||||||
const matchedElem = matchedElems[0];
|
const matchedElem = matchedElems[0];
|
||||||
|
const handlerName = matchedElem.getAttribute('data-shortcut-handler')!;
|
||||||
|
if (shortcutHandlers[handlerName]) {
|
||||||
|
for (const handler of shortcutHandlers[handlerName]) {
|
||||||
|
if (handler(target)) break;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
shortcutPresses.length = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
shortcutPresses.length = 0;
|
shortcutPresses.length = 0;
|
||||||
matchedElem.click();
|
matchedElem.click();
|
||||||
|
|||||||
Reference in new issue
Block a user