mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 16:40:24 +08:00 
			
		
		
		
	Make issue meta dropdown support Enter, confirm before reloading (#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>  </details> --------- Co-authored-by: Brecht Van Lommel <brecht@blender.org> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
		@@ -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>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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}}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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('');
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user