Make issue meta dropdown support Enter, confirm before reloading (#23014) (#23102)

Backport #23014

As the title. Label/assignee share the same code.

* Close #22607
* Close #20727

Also:

* partially fix for #21742, now the comment reaction and menu work with
keyboard.
* partially fix for #17705, in most cases the comment won't be lost.
* partially fix for #21539
* partially fix for #20347
* partially fix for #7329

### The `Enter` support

Before, if user presses Enter, the dropdown just disappears and nothing
happens or the window reloads.

After, Enter can be used to select/deselect labels, and press Esc to
hide the dropdown to update the labels (still no way to cancel ....
maybe you can do a Cmd+R or F5 to refresh the window to discard the
changes .....)


This is only a quick patch, the UX is still not perfect, but it's much
better than before.


### The `confirm` before reloading

And more fixes for the `reload` problem, the new behaviors:

* If nothing changes (just show/hide the dropdown), then the page won't
be reloaded.
* If there are draft comments, show a confirm dialog before reloading,
to avoid losing comments.

That's the best effect can be done at the moment, unless completely
refactor these dropdown related code.

Screenshot of the confirm dialog:

<details>


![image](https://user-images.githubusercontent.com/2114189/220538288-e2da8459-6a4e-43cb-8596-74057f8a03a2.png)

</details>

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Brecht Van Lommel <brecht@blender.org>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
Yarden Shoham 2023-02-24 11:40:36 +02:00 committed by GitHub
parent 4d3e2b23b8
commit 5ddf67a9c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 46 additions and 20 deletions

View file

@ -7,7 +7,7 @@
<div class="header">{{.ctx.locale.Tr "repo.pick_reaction"}}</div> <div class="header">{{.ctx.locale.Tr "repo.pick_reaction"}}</div>
<div class="divider"></div> <div class="divider"></div>
{{range $value := AllowedReactions}} {{range $value := AllowedReactions}}
<div class="item reaction tooltip" data-content="{{$value}}">{{ReactionToEmoji $value}}</div> <a class="item reaction tooltip" data-content="{{$value}}">{{ReactionToEmoji $value}}</a>
{{end}} {{end}}
</div> </div>
</div> </div>

View file

@ -10,16 +10,16 @@
{{else}} {{else}}
{{$referenceUrl = Printf "%s/files#%s" .ctx.Issue.Link .item.HashTag}} {{$referenceUrl = Printf "%s/files#%s" .ctx.Issue.Link .item.HashTag}}
{{end}} {{end}}
<div class="item context" data-clipboard-text-type="url" data-clipboard-text="{{AppSubUrl}}{{$referenceUrl}}">{{.ctx.locale.Tr "repo.issues.context.copy_link"}}</div> <a class="item context" data-clipboard-text-type="url" data-clipboard-text="{{AppSubUrl}}{{$referenceUrl}}">{{.ctx.locale.Tr "repo.issues.context.copy_link"}}</a>
<div class="item context quote-reply {{if .diff}}quote-reply-diff{{end}}" data-target="{{.item.HashTag}}-raw">{{.ctx.locale.Tr "repo.issues.context.quote_reply"}}</div> <a class="item context quote-reply {{if .diff}}quote-reply-diff{{end}}" data-target="{{.item.HashTag}}-raw">{{.ctx.locale.Tr "repo.issues.context.quote_reply"}}</a>
{{if not .ctx.UnitIssuesGlobalDisabled}} {{if not .ctx.UnitIssuesGlobalDisabled}}
<div class="item context 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="{{$referenceUrl}}">{{.ctx.locale.Tr "repo.issues.context.reference_issue"}}</div> <a class="item context 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="{{$referenceUrl}}">{{.ctx.locale.Tr "repo.issues.context.reference_issue"}}</a>
{{end}} {{end}}
{{if or .ctx.Permission.IsAdmin .IsCommentPoster .ctx.HasIssuesOrPullsWritePermission}} {{if or .ctx.Permission.IsAdmin .IsCommentPoster .ctx.HasIssuesOrPullsWritePermission}}
<div class="divider"></div> <div class="divider"></div>
<div class="item context edit-content">{{.ctx.locale.Tr "repo.issues.context.edit"}}</div> <a class="item context edit-content">{{.ctx.locale.Tr "repo.issues.context.edit"}}</a>
{{if .delete}} {{if .delete}}
<div class="item context delete-comment" data-comment-id={{.item.HashTag}} data-url="{{.ctx.RepoLink}}/comments/{{.item.ID}}/delete" data-locale="{{.ctx.locale.Tr "repo.issues.delete_comment_confirm"}}">{{.ctx.locale.Tr "repo.issues.context.delete"}}</div> <a class="item context delete-comment" data-comment-id={{.item.HashTag}} data-url="{{.ctx.RepoLink}}/comments/{{.item.ID}}/delete" data-locale="{{.ctx.locale.Tr "repo.issues.delete_comment_confirm"}}">{{.ctx.locale.Tr "repo.issues.context.delete"}}</a>
{{end}} {{end}}
{{end}} {{end}}
</div> </div>

View file

@ -121,7 +121,7 @@
<input type="text" placeholder="{{.locale.Tr "repo.issues.filter_labels"}}"> <input type="text" placeholder="{{.locale.Tr "repo.issues.filter_labels"}}">
</div> </div>
{{end}} {{end}}
<div class="no-select item">{{.locale.Tr "repo.issues.new.clear_labels"}}</div> <a class="no-select item" href="#">{{.locale.Tr "repo.issues.new.clear_labels"}}</a>
{{if or .Labels .OrgLabels}} {{if or .Labels .OrgLabels}}
{{$previousExclusiveScope := "_no_scope"}} {{$previousExclusiveScope := "_no_scope"}}
{{range .Labels}} {{range .Labels}}

View file

@ -81,7 +81,8 @@ function attachOneDropdownAria($dropdown) {
$dropdown.on('keydown', (e) => { $dropdown.on('keydown', (e) => {
// here it must use keydown event before dropdown's keyup handler, otherwise there is no Enter event in our keyup handler // here it must use keydown event before dropdown's keyup handler, otherwise there is no Enter event in our keyup handler
if (e.key === 'Enter') { if (e.key === 'Enter') {
const $item = $dropdown.dropdown('get item', $dropdown.dropdown('get value')); let $item = $dropdown.dropdown('get item', $dropdown.dropdown('get value'));
if (!$item) $item = $menu.find('> .item.selected'); // when dropdown filters items by input, there is no "value", so query the "selected" item
// if the selected item is clickable, then trigger the click event. in the future there could be a special CSS class for it. // if the selected item is clickable, then trigger the click event. in the future there could be a special CSS class for it.
if ($item && $item.is('a')) $item[0].click(); if ($item && $item.is('a')) $item[0].click();
} }

View file

@ -29,6 +29,26 @@ import {hideElem, showElem} from '../utils/dom.js';
const {csrfToken} = window.config; const {csrfToken} = window.config;
// if there are draft comments (more than 20 chars), confirm before reloading, to avoid losing comments
function reloadConfirmDraftComment() {
const commentTextareas = [
document.querySelector('.edit-content-zone:not(.gt-hidden) textarea'),
document.querySelector('.edit_area'),
];
for (const textarea of commentTextareas) {
// Most users won't feel too sad if they lose a comment with 10 or 20 chars, they can re-type these in seconds.
// But if they have typed more (like 50) chars and the comment is lost, they will be very unhappy.
if (textarea && textarea.value.trim().length > 20) {
textarea.parentElement.scrollIntoView();
if (!window.confirm('Page will be reloaded, but there are draft comments. Continuing to reload will discard the comments. Continue?')) {
return;
}
break;
}
}
window.location.reload();
}
export function initRepoCommentForm() { export function initRepoCommentForm() {
const $commentForm = $('.comment.form'); const $commentForm = $('.comment.form');
if ($commentForm.length === 0) { if ($commentForm.length === 0) {
@ -86,12 +106,15 @@ export function initRepoCommentForm() {
let hasUpdateAction = $listMenu.data('action') === 'update'; let hasUpdateAction = $listMenu.data('action') === 'update';
const items = {}; const items = {};
$(`.${selector}`).dropdown('setting', 'onHide', () => { $(`.${selector}`).dropdown({
'action': 'nothing', // do not hide the menu if user presses Enter
fullTextSearch: 'exact',
async onHide() {
hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var
if (hasUpdateAction) { if (hasUpdateAction) {
// TODO: Add batch functionality and make this 1 network request. // TODO: Add batch functionality and make this 1 network request.
(async function() { const itemEntries = Object.entries(items);
for (const [elementId, item] of Object.entries(items)) { for (const [elementId, item] of itemEntries) {
await updateIssuesMeta( await updateIssuesMeta(
item['update-url'], item['update-url'],
item.action, item.action,
@ -99,9 +122,11 @@ export function initRepoCommentForm() {
elementId, elementId,
); );
} }
window.location.reload(); if (itemEntries.length) {
})(); reloadConfirmDraftComment();
} }
}
},
}); });
$listMenu.find('.item:not(.no-select)').on('click', function (e) { $listMenu.find('.item:not(.no-select)').on('click', function (e) {
@ -196,7 +221,7 @@ export function initRepoCommentForm() {
'clear', 'clear',
$listMenu.data('issue-id'), $listMenu.data('issue-id'),
'', '',
).then(() => window.location.reload()); ).then(reloadConfirmDraftComment);
} }
$(this).parent().find('.item').each(function () { $(this).parent().find('.item').each(function () {
@ -239,7 +264,7 @@ export function initRepoCommentForm() {
'', '',
$menu.data('issue-id'), $menu.data('issue-id'),
$(this).data('id'), $(this).data('id'),
).then(() => window.location.reload()); ).then(reloadConfirmDraftComment);
} }
let icon = ''; let icon = '';
@ -272,7 +297,7 @@ export function initRepoCommentForm() {
'', '',
$menu.data('issue-id'), $menu.data('issue-id'),
$(this).data('id'), $(this).data('id'),
).then(() => window.location.reload()); ).then(reloadConfirmDraftComment);
} }
$list.find('.selected').html(''); $list.find('.selected').html('');