mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-17 04:45:53 +02: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="flex-text-block tw-justify-end">
|
||||
{{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="single_review" value="true">
|
||||
{{else}}
|
||||
{{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}}
|
||||
<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="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="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">{{ctx.Locale.Tr "repo.diff.comment.add_single_comment"}}</button>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{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}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{range .comments}}
|
||||
{{range $comment := .comments}}
|
||||
|
||||
{{$createdStr:= DateUtils.TimeSince .CreatedUnix}}
|
||||
<div class="comment" id="{{.HashTag}}">
|
||||
@@ -53,7 +53,7 @@
|
||||
{{if not $.root.Repository.IsArchived}}
|
||||
{{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/comments/%d/reactions" $.root.RepoLink .ID)}}
|
||||
{{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 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}}>
|
||||
<button type="button" class="ui tiny primary button tw-pr-1 js-btn-review {{if not $.IsShowingAllCommits}}disabled{{end}}"
|
||||
{{if not $.IsShowingAllCommits}}data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.review_only_possible_for_full_diff"}}"{{end}}
|
||||
{{if $.Repository.IsArchived}}disabled{{end}}
|
||||
>
|
||||
{{$disabledReviewTooltip := "" -}}
|
||||
{{if $.Repository.IsArchived -}}
|
||||
{{$disabledReviewTooltip = ctx.Locale.Tr "repo.archive.pull.nocomment" -}}
|
||||
{{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"}}
|
||||
<span class="ui small label review-comments-counter" data-pending-comment-number="{{.PendingCodeCommentNumber}}">{{.PendingCodeCommentNumber}}</span>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
@@ -15,7 +19,7 @@
|
||||
<input type="hidden" name="commit_id" value="{{.AfterCommitID}}">
|
||||
<div class="field flex-text-block">
|
||||
<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 class="field">
|
||||
{{template "shared/combomarkdowneditor" (dict
|
||||
@@ -55,3 +59,4 @@
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
@@ -41,7 +41,7 @@
|
||||
{{if not $.Repository.IsArchived}}
|
||||
{{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index)}}
|
||||
{{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 class="ui attached segment comment-body" role="article">
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
{{if not $.Repository.IsArchived}}
|
||||
{{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
|
||||
{{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 class="ui attached segment comment-body" role="article">
|
||||
@@ -424,7 +424,7 @@
|
||||
{{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}}
|
||||
{{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/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}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,44 +1,45 @@
|
||||
<div class="item action ui dropdown jump pointing top right context-dropdown">
|
||||
<a class="context-menu muted">
|
||||
{{svg "octicon-kebab-horizontal"}}
|
||||
</a>
|
||||
{{$type := $.ConvContentType -}}
|
||||
{{$item := $.ConvContentItem -}}
|
||||
{{$page := $.ConvContentPage -}}
|
||||
|
||||
{{$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">
|
||||
{{$issueHTMLURL := ctx.RootData.Issue.HTMLURL ctx}}
|
||||
{{$referenceHTMLURL := ""}}
|
||||
{{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>
|
||||
<div class="item js-aria-clickable" data-clipboard-text="{{$referenceHTMLURL}}">{{ctx.Locale.Tr "copy_link"}}</div>
|
||||
<div class="item js-aria-clickable" data-clipboard-target="#{{$item.HashTag}}-raw">{{ctx.Locale.Tr "repo.issues.context.copy_source"}}</div>
|
||||
|
||||
{{if ctx.RootData.IsSigned}}
|
||||
{{$needDivider := false}}
|
||||
{{if not ctx.RootData.Repository.IsArchived}}
|
||||
{{$needDivider = true}}
|
||||
<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>
|
||||
<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>
|
||||
{{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}}
|
||||
{{if or ctx.RootData.Permission.IsAdmin .IsCommentPoster ctx.RootData.HasIssuesOrPullsWritePermission}}
|
||||
{{if $canEditContent}}
|
||||
<div class="divider"></div>
|
||||
<div class="item context js-aria-clickable edit-content">{{ctx.Locale.Tr "repo.issues.context.edit"}}</div>
|
||||
{{if .delete}}
|
||||
<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="onConvContentEdit">{{ctx.Locale.Tr "repo.issues.context.edit"}}</div>
|
||||
{{if $supportDelete}}
|
||||
<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}}
|
||||
{{$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 $needDivider}}
|
||||
<div class="divider"></div>
|
||||
{{end}}
|
||||
<div class="divider"></div>
|
||||
{{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}}
|
||||
{{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}}
|
||||
|
||||
@@ -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)
|
||||
The variables in "ctx.Data" are different in each case, making this template fragile, hard to read and maintain. */}}
|
||||
{{if len .comments}}
|
||||
{{$comment := index .comments 0}}
|
||||
{{$invalid := $comment.Invalidated}}
|
||||
{{$resolved := $comment.IsResolved}}
|
||||
{{$resolveDoer := $comment.ResolveDoer}}
|
||||
{{$hasReview := and $comment.Review}}
|
||||
{{$isReviewPending := and $hasReview (eq $comment.Review.Type 0)}}
|
||||
{{$firstComment := index .comments 0}}
|
||||
{{$invalid := $firstComment.Invalidated}}
|
||||
{{$resolved := $firstComment.IsResolved}}
|
||||
{{$resolveDoer := $firstComment.ResolveDoer}}
|
||||
{{$hasReview := and $firstComment.Review}}
|
||||
{{$isReviewPending := and $hasReview (eq $firstComment.Review.Type 0)}}
|
||||
<div class="ui segments conversation-holder">
|
||||
<div class="ui segment collapsible-comment-box tw-py-2 flex-left-right">
|
||||
<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}}
|
||||
<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"}}
|
||||
@@ -22,7 +22,7 @@ The variables in "ctx.Data" are different in each case, making this template fra
|
||||
</div>
|
||||
<div class="tw-flex tw-items-center">
|
||||
{{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"}}
|
||||
{{if $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"}}
|
||||
{{end}}
|
||||
</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"}}
|
||||
{{if $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}}
|
||||
</div>
|
||||
</div>
|
||||
{{$diff := (CommentMustAsDiff ctx $comment)}}
|
||||
{{$diff := (CommentMustAsDiff ctx $firstComment)}}
|
||||
{{if $diff}}
|
||||
{{$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="file-body file-code code-view code-diff code-diff-unified unicode-escaped">
|
||||
<table>
|
||||
@@ -56,9 +56,9 @@ The variables in "ctx.Data" are different in each case, making this template fra
|
||||
</div>
|
||||
</div>
|
||||
{{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">
|
||||
{{range .comments}}
|
||||
{{range $comment := .comments}}
|
||||
{{$createdSubStr:= DateUtils.TimeSince .CreatedUnix}}
|
||||
<div class="comment code-comment" id="{{.HashTag}}">
|
||||
<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}}
|
||||
{{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/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}}
|
||||
</div>
|
||||
</div>
|
||||
@@ -125,7 +125,7 @@ The variables in "ctx.Data" are different in each case, making this template fra
|
||||
</div>
|
||||
<div class="flex-text-block">
|
||||
{{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}}
|
||||
{{ctx.Locale.Tr "repo.issues.review.un_resolve_conversation"}}
|
||||
{{else}}
|
||||
@@ -140,7 +140,7 @@ The variables in "ctx.Data" are different in each case, making this template fra
|
||||
{{end}}
|
||||
</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>
|
||||
{{else}}
|
||||
|
||||
@@ -36,7 +36,7 @@ test('pr review flow', async ({page, request}) => {
|
||||
request,
|
||||
poster,
|
||||
repoName,
|
||||
[{path: 'added.txt', content: 'new content\n'}],
|
||||
[{path: 'added.txt', content: 'new content\nsecond line\n'}],
|
||||
{branch: 'main', newBranch: 'feat', headers: posterHeaders},
|
||||
),
|
||||
]);
|
||||
@@ -80,10 +80,10 @@ test('pr review flow', async ({page, request}) => {
|
||||
|
||||
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"]');
|
||||
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
|
||||
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 loginUser(page, reviewer);
|
||||
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');
|
||||
await panel.locator('textarea[name="content"]').fill(`First approval from ${reviewer}`);
|
||||
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);
|
||||
|
||||
|
||||
@@ -40,8 +40,9 @@ func testPullDiffSingleHeadCommitReviewFormAction(t *testing.T) {
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
doc := NewHTMLParser(t, resp.Body)
|
||||
|
||||
btn := doc.Find(".js-btn-review")
|
||||
assert.True(t, btn.Length() == 1 && !btn.HasClass("disabled"))
|
||||
btn := doc.Find(".review-box-button")
|
||||
require.Equal(t, 1, btn.Length())
|
||||
assert.False(t, btn.Is("[disabled]"))
|
||||
form := doc.Find(".review-box-panel form")
|
||||
assert.Equal(t, 1, form.Length())
|
||||
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
|
||||
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) {
|
||||
|
||||
@@ -131,20 +131,21 @@
|
||||
max-width: calc(100vw - 70px); /* leave enough space on left, and align the page content */
|
||||
}
|
||||
|
||||
#review-box {
|
||||
.ui.button.review-box-button {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
#review-box .review-comments-counter[data-pending-comment-number="0"] {
|
||||
.review-box-button .review-comments-counter[data-pending-comment-number="0"] {
|
||||
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"
|
||||
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');
|
||||
if (!counter) return;
|
||||
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');
|
||||
container.innerHTML = `
|
||||
<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="raw-content">Raw text</div>
|
||||
</div>
|
||||
@@ -20,24 +20,24 @@ test('tryShortcutQuoteReply', () => {
|
||||
button.addEventListener('click', onClick);
|
||||
|
||||
selection.removeAllRanges();
|
||||
expect(tryShortcutQuoteReply()).toBe(false);
|
||||
expect(tryQuoteSelectedReply()).toBe(false);
|
||||
|
||||
selection.selectAllChildren(container.querySelector('.raw-content')!);
|
||||
expect(tryShortcutQuoteReply()).toBe(false);
|
||||
expect(tryQuoteSelectedReply()).toBe(false);
|
||||
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(container.querySelector('.render-content')!.firstChild!);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
selection.collapseToStart();
|
||||
expect(tryShortcutQuoteReply()).toBe(false);
|
||||
expect(tryQuoteSelectedReply()).toBe(false);
|
||||
expect(onClick).not.toHaveBeenCalled();
|
||||
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
expect(tryShortcutQuoteReply()).toBe(true);
|
||||
expect(tryQuoteSelectedReply()).toBe(true);
|
||||
expect(onClick).toHaveBeenCalledTimes(1);
|
||||
|
||||
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 {POST} from '../modules/fetch.ts';
|
||||
import {showErrorToast} from '../modules/toast.ts';
|
||||
@@ -8,12 +8,9 @@ import {triggerUploadStateChanged} from './comp/EditorUpload.ts';
|
||||
import {convertHtmlToMarkdown} from '../markup/html2markdown.ts';
|
||||
import {applyAreYouSure} from '../modules/are-you-sure.ts';
|
||||
import {registerShortcutHandler} from '../modules/shortcut.ts';
|
||||
import {registerGlobalEventFunc} from '../modules/observer.ts';
|
||||
|
||||
async function tryOnEditContent(e: Event) {
|
||||
const clickTarget = (e.target as HTMLElement).closest('.edit-content');
|
||||
if (!clickTarget) return;
|
||||
|
||||
e.preventDefault();
|
||||
async function onConvContentEdit(clickTarget: HTMLElement) {
|
||||
const commentContent = clickTarget.closest('.comment-header')!.nextElementSibling!;
|
||||
const editContentZone = commentContent.querySelector('.edit-content-zone')!;
|
||||
let renderContent = commentContent.querySelector('.render-content')!;
|
||||
@@ -121,11 +118,7 @@ function extractSelectedMarkdown(container: HTMLElement) {
|
||||
return convertHtmlToMarkdown(el);
|
||||
}
|
||||
|
||||
async function tryOnQuoteReply(e: Event) {
|
||||
const clickTarget = (e.target as HTMLElement).closest('.quote-reply');
|
||||
if (!clickTarget) return;
|
||||
|
||||
e.preventDefault();
|
||||
async function onConvContentQuoteReply(clickTarget: HTMLElement) {
|
||||
const contentToQuoteId = clickTarget.getAttribute('data-target');
|
||||
const targetRawToQuote = document.querySelector<HTMLElement>(`#${contentToQuoteId}.raw-content`)!;
|
||||
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`;
|
||||
|
||||
let editor;
|
||||
if (clickTarget.classList.contains('quote-reply-diff')) {
|
||||
const replyBtn = clickTarget.closest('.comment-code-cloud')!.querySelector<HTMLElement>('button.comment-form-reply')!;
|
||||
editor = await handleReply(replyBtn);
|
||||
const btnReply = clickTarget.closest('.comment-code-cloud')?.querySelector<HTMLElement>('button.comment-form-reply');
|
||||
if (btnReply) {
|
||||
editor = await handleCommentReply(btnReply);
|
||||
} else {
|
||||
// for normal issue/comment page
|
||||
editor = getComboMarkdownEditor(document.querySelector('#comment-form .combo-markdown-editor'))!;
|
||||
@@ -151,7 +144,7 @@ async function tryOnQuoteReply(e: Event) {
|
||||
editor.moveCursorToEnd();
|
||||
}
|
||||
|
||||
export function tryShortcutQuoteReply(): boolean {
|
||||
export function tryQuoteSelectedReply(): boolean {
|
||||
const selection = window.getSelection();
|
||||
if (!selection?.rangeCount || selection.isCollapsed) return false;
|
||||
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');
|
||||
if (!commentContainer) return false;
|
||||
|
||||
const quoteReplyBtn = commentContainer.querySelector<HTMLElement>('.quote-reply')!;
|
||||
const quoteReplyBtn = commentContainer.querySelector<HTMLElement>('[data-shortcut-handler="quoteSelectedReply"]')!;
|
||||
if (!quoteReplyBtn) return false;
|
||||
|
||||
quoteReplyBtn.click();
|
||||
@@ -168,15 +161,10 @@ export function tryShortcutQuoteReply(): boolean {
|
||||
}
|
||||
|
||||
export function initRepoIssueCommentEdit() {
|
||||
document.addEventListener('click', (e) => {
|
||||
tryOnEditContent(e); // Edit issue or comment content
|
||||
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.
|
||||
registerGlobalEventFunc('click', 'onConvContentQuoteReply', onConvContentQuoteReply);
|
||||
registerGlobalEventFunc('click', 'onConvContentEdit', onConvContentEdit);
|
||||
|
||||
// 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
|
||||
registerShortcutHandler('r', tryShortcutQuoteReply);
|
||||
// The same to GitHub and GitLab: https://gitlab.com/gitlab-org/gitlab-foss/-/commit/ca5d0c82509cedb94f9bfa4a40e77706a58faafe
|
||||
registerShortcutHandler('quoteSelectedReply', tryQuoteSelectedReply);
|
||||
}
|
||||
@@ -17,8 +17,9 @@ import {initRepoIssueSidebar} from './repo-issue-sidebar.ts';
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
import {showFomanticModal} from '../modules/fomantic/modal.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 {performFetchActionRequest} from '../modules/fetch-action.ts';
|
||||
|
||||
const {appSubUrl} = window.config;
|
||||
|
||||
@@ -94,69 +95,57 @@ export function initRepoIssueFilterItemLabel() {
|
||||
}
|
||||
|
||||
export function initRepoIssueCommentDelete() {
|
||||
// Delete comment
|
||||
document.addEventListener('click', async (e) => {
|
||||
if (!(e.target as HTMLElement).matches('.delete-comment')) return;
|
||||
e.preventDefault();
|
||||
registerGlobalEventFunc('click', 'onConvContentDelete', async (deleteButton) => {
|
||||
if (!window.confirm(deleteButton.getAttribute('data-locale')!)) return;
|
||||
|
||||
const deleteButton = e.target as HTMLElement;
|
||||
if (window.confirm(deleteButton.getAttribute('data-locale')!)) {
|
||||
try {
|
||||
const response = await POST(deleteButton.getAttribute('data-url')!);
|
||||
if (!response.ok) throw new Error('Failed to delete comment');
|
||||
const response = await performFetchActionRequest(deleteButton, {method: 'POST', url: deleteButton.getAttribute('data-url')!});
|
||||
if (!response) return;
|
||||
|
||||
const conversationHolder = deleteButton.closest('.conversation-holder');
|
||||
const parentTimelineItem = deleteButton.closest('.timeline-item');
|
||||
const parentTimelineGroup = deleteButton.closest('.timeline-item-group');
|
||||
const conversationHolder = deleteButton.closest('.conversation-holder');
|
||||
const parentTimelineItem = deleteButton.closest('.timeline-item');
|
||||
const parentTimelineGroup = deleteButton.closest('.timeline-item-group');
|
||||
|
||||
// Check if this was a pending comment.
|
||||
if (conversationHolder?.querySelector('.pending-label')) {
|
||||
const counter = document.querySelector('#review-box .review-comments-counter')!;
|
||||
let num = parseInt(counter?.getAttribute('data-pending-comment-number') || '') - 1 || 0;
|
||||
num = Math.max(num, 0);
|
||||
counter.setAttribute('data-pending-comment-number', String(num));
|
||||
counter.textContent = String(num);
|
||||
// Check if this was a pending comment.
|
||||
if (conversationHolder?.querySelector('.pending-label')) {
|
||||
const counter = document.querySelector('.review-box-button .review-comments-counter')!;
|
||||
let num = parseInt(counter?.getAttribute('data-pending-comment-number') || '') - 1 || 0;
|
||||
num = Math.max(num, 0);
|
||||
counter.setAttribute('data-pending-comment-number', 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() {
|
||||
// Cancel inline code comment
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!(e.target as HTMLElement).matches('.cancel-code-comment')) return;
|
||||
|
||||
const form = (e.target as HTMLElement).closest('form')!;
|
||||
if (form?.classList.contains('comment-form')) {
|
||||
registerGlobalEventFunc('click', 'onCancelCodeComment', (el) => {
|
||||
const form = el.closest('form')!;
|
||||
if (form.classList.contains('comment-form')) {
|
||||
hideElem(form);
|
||||
showElem(form.closest('.comment-code-cloud')!.querySelectorAll('button.comment-form-reply'));
|
||||
} 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 textarea = form.querySelector('textarea');
|
||||
|
||||
@@ -242,15 +231,13 @@ export function initRepoPullRequestReview() {
|
||||
|
||||
addDelegatedEventListener(document, 'click', 'button.comment-form-reply', (el, e) => {
|
||||
e.preventDefault();
|
||||
handleReply(el);
|
||||
handleCommentReply(el);
|
||||
});
|
||||
|
||||
// The following part is only for diff views
|
||||
if (!document.querySelector('.repository.pull.diff')) return;
|
||||
|
||||
const elReviewBtn = document.querySelector('.js-btn-review');
|
||||
const elReviewPanel = document.querySelector('.review-box-panel.tippy-target');
|
||||
if (elReviewBtn && elReviewPanel) {
|
||||
registerGlobalInitFunc('initPullReviewBox', (el) => {
|
||||
const elReviewBtn = el.querySelector('.review-box-button')!;
|
||||
const elReviewPanel = el.querySelector('.review-box-panel.tippy-target');
|
||||
if (!elReviewPanel) return;
|
||||
const tippy = createTippy(elReviewBtn, {
|
||||
content: elReviewPanel,
|
||||
theme: 'default',
|
||||
@@ -260,8 +247,8 @@ export function initRepoPullRequestReview() {
|
||||
interactive: 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) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -42,13 +42,13 @@ export function initRepository() {
|
||||
registerGlobalInitFunc('initRepoPullRequestUpdate', initRepoPullRequestUpdate);
|
||||
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');
|
||||
if (!pageContent) return;
|
||||
|
||||
initRepoBranchTagSelector();
|
||||
initRepoCommentFormAndSidebar();
|
||||
|
||||
// Labels
|
||||
initCompLabelEdit('.page-content.repository.labels');
|
||||
initRepoMilestone();
|
||||
initRepoNew();
|
||||
@@ -57,18 +57,17 @@ export function initRepository() {
|
||||
initRepoSettings();
|
||||
initRepoIssueWipNewTitle();
|
||||
|
||||
// Issues
|
||||
if (pageContent.matches('.page-content.repository.view.issue')) {
|
||||
initRepoIssueCommentEdit();
|
||||
initRepoIssueCommentEdit();
|
||||
initRepoIssueCommentDelete();
|
||||
initRepoIssueCodeCommentCancel();
|
||||
|
||||
if (pageContent.matches('.page-content.repository.view.issue')) {
|
||||
initRepoIssueTitleEdit();
|
||||
initRepoIssueWipToggle();
|
||||
initRepoIssueComments();
|
||||
|
||||
initRepoIssueReferenceIssue();
|
||||
|
||||
initRepoIssueCommentDelete();
|
||||
initRepoIssueCodeCommentCancel();
|
||||
initCompReactionSelector();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {registerGlobalInitFunc} from './observer.ts';
|
||||
import {createElementFromHTML, hideElem, toggleElem} from '../utils/dom.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;
|
||||
|
||||
@@ -87,6 +87,7 @@ function showShortcutHelp() {
|
||||
document.body.append(modal);
|
||||
}
|
||||
|
||||
const seenShortcuts = new Set<string>();
|
||||
const list = modal.querySelector<HTMLElement>('.shortcut-list')!;
|
||||
list.replaceChildren(createElementFromHTML(html`<tr><td><kbd>?</kbd></td><td>Keyboard Shortcuts</td></tr>`));
|
||||
for (const elem of document.querySelectorAll<HTMLElement>('[data-shortcut-keys]')) {
|
||||
@@ -99,15 +100,24 @@ function showShortcutHelp() {
|
||||
target.getAttribute('placeholder') ||
|
||||
target.getAttribute('title') ||
|
||||
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);
|
||||
}
|
||||
|
||||
export function registerShortcutHandler(fullKey: string, handler: ShortcutHandler) {
|
||||
fullKey = fullKey.toLowerCase();
|
||||
shortcutHandlers[fullKey] ||= [];
|
||||
shortcutHandlers[fullKey].push(handler);
|
||||
export function registerShortcutHandler(name: string, handler: ShortcutHandler) {
|
||||
shortcutHandlers[name] ||= [];
|
||||
shortcutHandlers[name].push(handler);
|
||||
}
|
||||
|
||||
export function initGlobalShortcut() {
|
||||
@@ -169,20 +179,22 @@ export function initGlobalShortcut() {
|
||||
shortcutPresses.push({fullKey, pressTime});
|
||||
|
||||
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
|
||||
// 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]`);
|
||||
if (!matchedElems.length) return;
|
||||
// TODO: if there are multiple matches, maybe we could introduce some priority rules to determine which one to trigger
|
||||
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();
|
||||
shortcutPresses.length = 0;
|
||||
matchedElem.click();
|
||||
|
||||
Reference in new issue
Block a user