]> source.dussan.org Git - gitea.git/commitdiff
Fix issue/PR title edit (#30858) (#30865)
authorGiteabot <teabot@gitea.io>
Sun, 5 May 2024 13:53:12 +0000 (21:53 +0800)
committerGitHub <noreply@github.com>
Sun, 5 May 2024 13:53:12 +0000 (13:53 +0000)
Backport #30858 by wxiaoguang

1. "enter" doesn't work (I think it is the last enter support for #14843)
2. if a branch name contains something like `&`, then the branch selector doesn't update

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
templates/repo/issue/view_title.tmpl
tests/integration/issue_test.go
tests/integration/pull_create_test.go
web_src/css/repo.css
web_src/js/features/common-global.js
web_src/js/features/comp/QuickSubmit.js
web_src/js/features/repo-issue.js

index fccf8cca91412bce0568d5fb15fcdfcd39a169d8..4415ad79f519af00a4718952f33e161449e47669 100644 (file)
@@ -4,29 +4,36 @@
        </div>
 {{end}}
 <div class="issue-title-header">
-       <div class="issue-title" id="issue-title-wrapper">
+       {{$canEditIssueTitle := and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}
+       <div class="issue-title" id="issue-title-display">
                <h1 class="gt-word-break">
-                       <span id="issue-title">{{RenderIssueTitle $.Context .Issue.Title ($.Repository.ComposeMetas ctx) | RenderCodeBlock}} <span class="index">#{{.Issue.Index}}</span>
-</span>
-                       <div id="edit-title-input" class="ui input tw-flex-1 tw-hidden">
-                               <input value="{{.Issue.Title}}" maxlength="255" autocomplete="off">
-                       </div>
+                       {{RenderIssueTitle $.Context .Issue.Title ($.Repository.ComposeMetas ctx) | RenderCodeBlock}}
+                       <span class="index">#{{.Issue.Index}}</span>
                </h1>
                <div class="issue-title-buttons">
-                       {{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}
-                               <button id="edit-title" class="ui small basic button edit-button not-in-edit{{if .Issue.IsPull}} tw-mr-0{{end}}">{{ctx.Locale.Tr "repo.issues.edit"}}</button>
+                       {{if $canEditIssueTitle}}
+                       <button id="issue-title-edit-show" class="ui small basic button">{{ctx.Locale.Tr "repo.issues.edit"}}</button>
                        {{end}}
                        {{if not .Issue.IsPull}}
-                               <a role="button" class="ui small primary button new-issue-button tw-mr-0" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>
+                       <a role="button" class="ui small primary button" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>
                        {{end}}
                </div>
-               {{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}
-                       <div class="edit-buttons">
-                               <button id="cancel-edit-title" class="ui small basic button in-edit tw-hidden">{{ctx.Locale.Tr "repo.issues.cancel"}}</button>
-                               <button id="save-edit-title" class="ui small primary button in-edit tw-hidden tw-mr-0" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/title" {{if .Issue.IsPull}}data-target-update-url="{{$.RepoLink}}/pull/{{.Issue.Index}}/target_branch"{{end}}>{{ctx.Locale.Tr "repo.issues.save"}}</button>
-                       </div>
-               {{end}}
        </div>
+       {{if $canEditIssueTitle}}
+       <div class="ui form issue-title tw-hidden" id="issue-title-editor">
+               <div class="ui input tw-flex-1">
+                       <input value="{{.Issue.Title}}" data-old-title="{{.Issue.Title}}" maxlength="255" autocomplete="off">
+               </div>
+               <div class="issue-title-buttons">
+                       <button class="ui small basic cancel button">{{ctx.Locale.Tr "repo.issues.cancel"}}</button>
+                       <button class="ui small primary button"
+                                                       data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/title"
+                                                       {{if .Issue.IsPull}}data-target-update-url="{{$.RepoLink}}/pull/{{.Issue.Index}}/target_branch"{{end}}>
+                               {{ctx.Locale.Tr "repo.issues.save"}}
+                       </button>
+               </div>
+       </div>
+       {{end}}
        <div class="issue-title-meta">
                {{if .HasMerged}}
                        <div class="ui purple label issue-state-label">{{svg "octicon-git-merge" 16 "tw-mr-1"}} {{if eq .Issue.PullRequest.Status 3}}{{ctx.Locale.Tr "repo.pulls.manually_merged"}}{{else}}{{ctx.Locale.Tr "repo.pulls.merged"}}{{end}}</div>
                                        {{end}}
                                {{else}}
                                        {{if .Issue.OriginalAuthor}}
-                                               <span id="pull-desc" class="pull-desc">{{.Issue.OriginalAuthor}} {{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref}}</span>
+                                               <span id="pull-desc-display" class="pull-desc">{{.Issue.OriginalAuthor}} {{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref}}</span>
                                        {{else}}
-                                               <span id="pull-desc" class="pull-desc">
+                                               <span id="pull-desc-display" class="pull-desc">
                                                        <a {{if gt .Issue.Poster.ID 0}}href="{{.Issue.Poster.HomeLink}}"{{end}}>{{.Issue.Poster.GetDisplayName}}</a>
                                                        {{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref}}
                                                </span>
                                        {{end}}
-                                       <span id="pull-desc-edit" class="tw-hidden flex-text-block">
+                                       <span id="pull-desc-editor" class="tw-hidden flex-text-block">
                                                <div class="ui floating filter dropdown">
                                                        <div class="ui basic small button tw-mr-0">
                                                                <span class="text">{{ctx.Locale.Tr "repo.pulls.compare_compare"}}: {{$.HeadTarget}}</span>
index b7952b0879a1b62c7ab5374c06075c1b437a7ec6..d74516d110a61670505271f30faacc55a532237a 100644 (file)
@@ -144,7 +144,7 @@ func testNewIssue(t *testing.T, session *TestSession, user, repo, title, content
        resp = session.MakeRequest(t, req, http.StatusOK)
 
        htmlDoc = NewHTMLParser(t, resp.Body)
-       val := htmlDoc.doc.Find("#issue-title").Text()
+       val := htmlDoc.doc.Find("#issue-title-display").Text()
        assert.Contains(t, val, title)
        val = htmlDoc.doc.Find(".comment .render-content p").First().Text()
        assert.Equal(t, content, val)
index 609bd73fd514cb14e73b25642624c9384d210da2..7add8e1db65164ad6be8b55e95537c408f2d41d1 100644 (file)
@@ -125,7 +125,7 @@ func TestPullCreate_TitleEscape(t *testing.T) {
                req := NewRequest(t, "GET", url)
                resp = session.MakeRequest(t, req, http.StatusOK)
                htmlDoc := NewHTMLParser(t, resp.Body)
-               editTestTitleURL, exists := htmlDoc.doc.Find("#save-edit-title").First().Attr("data-update-url")
+               editTestTitleURL, exists := htmlDoc.doc.Find(".issue-title-buttons button[data-update-url]").First().Attr("data-update-url")
                assert.True(t, exists, "The template has changed")
 
                req = NewRequestWithValues(t, "POST", editTestTitleURL, map[string]string{
index 408b62ad3c2b0d9864eca386b89168f896ac1a12..7695b632b4f6eda7c71d6febf4e6b749384c3c10 100644 (file)
@@ -575,6 +575,22 @@ td .commit-summary {
   display: inline-block;
 }
 
+@media (max-width: 767.98px) {
+  .comment.form .issue-content-left .avatar {
+    display: none;
+  }
+  .comment.form .issue-content-left .content {
+    margin-left: 0 !important;
+  }
+  .comment.form .issue-content-left .content::before,
+  .comment.form .issue-content-left .content::after,
+  .comment.form .content .form::before,
+  .comment.form .content .form::after {
+    display: none;
+  }
+}
+
+/* issue title & meta & edit */
 .issue-title-header {
   width: 100%;
   padding-bottom: 4px;
@@ -586,46 +602,25 @@ td .commit-summary {
   align-items: center;
 }
 
-.repository.view.issue .issue-title-buttons,
-.repository.view.issue .edit-buttons {
+.repository.view.issue .issue-title-buttons {
   display: flex;
+  gap: 0.5em;
 }
 
-@media (max-width: 767.98px) {
-  .repository.view.issue .issue-title {
-    flex-direction: column;
-  }
-  .repository.view.issue .issue-title-buttons,
-  .repository.view.issue .edit-buttons {
-    width: 100%;
-    justify-content: space-between;
-  }
-  .repository.view.issue .edit-buttons {
-    margin-top: .5rem;
-  }
-  .comment.form .issue-content-left .avatar {
-    display: none;
-  }
-  .comment.form .issue-content-left .content {
-    margin-left: 0 !important;
-  }
-  .comment.form .issue-content-left .content::before,
-  .comment.form .issue-content-left .content::after,
-  .comment.form .content .form::before,
-  .comment.form .content .form::after {
-    display: none;
-  }
+.repository.view.issue .issue-title-buttons > .ui.button {
+  margin: 0;
+  height: 35px;
 }
 
 .repository.view.issue .issue-title {
   display: flex;
   align-items: center;
+  gap: 0.5em;
   margin-bottom: 8px;
+  min-height: 40px; /* avoid layout shift on edit */
 }
 
 .repository.view.issue .issue-title h1 {
-  display: flex;
-  align-items: center;
   flex: 1;
   width: 100%;
   font-weight: var(--font-weight-normal);
@@ -633,14 +628,24 @@ td .commit-summary {
   line-height: 40px;
   margin: 0;
   padding-right: 0.25rem;
-  min-height: 41px; /* avoid layout shift on edit */
 }
 
-.repository.view.issue .issue-title h1 .ui.input {
-  font-size: 0.5em;
+@media (max-width: 767.98px) {
+  .repository.view.issue .issue-title {
+    flex-direction: column;
+  }
+  .repository.view.issue .issue-title-buttons {
+    width: 100%;
+    justify-content: space-between;
+  }
 }
 
-.repository.view.issue .issue-title h1 .ui.input input {
+.repository.view.issue .issue-title .ui.input {
+  width: 100%;
+  height: 35px;
+}
+
+.repository.view.issue .issue-title .ui.input input {
   font-size: 1.5em;
   padding: 2px .5rem;
 }
@@ -653,10 +658,6 @@ td .commit-summary {
   margin-right: 10px;
 }
 
-.issue-title .edit-zone {
-  margin-top: 10px;
-}
-
 .issue-state-label {
   display: flex !important;
   align-items: center !important;
index a821e1b92194e98f5469de9404dd0ba07e973998..5b8673105d6b2eb89498737eba29981b1fecc546 100644 (file)
@@ -47,10 +47,18 @@ export function initFootLanguageMenu() {
 
 export function initGlobalEnterQuickSubmit() {
   document.addEventListener('keydown', (e) => {
-    const isQuickSubmitEnter = ((e.ctrlKey && !e.altKey) || e.metaKey) && (e.key === 'Enter');
-    if (isQuickSubmitEnter && e.target.matches('textarea')) {
-      e.preventDefault();
-      handleGlobalEnterQuickSubmit(e.target);
+    if (e.key !== 'Enter') return;
+    const hasCtrlOrMeta = ((e.ctrlKey || e.metaKey) && !e.altKey);
+    if (hasCtrlOrMeta && e.target.matches('textarea')) {
+      if (handleGlobalEnterQuickSubmit(e.target)) {
+        e.preventDefault();
+      }
+    } else if (e.target.matches('input') && !e.target.closest('form')) {
+      // input in a normal form could handle Enter key by default, so we only handle the input outside a form
+      // eslint-disable-next-line unicorn/no-lonely-if
+      if (handleGlobalEnterQuickSubmit(e.target)) {
+        e.preventDefault();
+      }
     }
   });
 }
index 6bd5f6644d8b4b43b054056912c1553234bf5c42..3ff29f4fac5c317a23fed47f8f67f25d6d7db49a 100644 (file)
@@ -3,16 +3,17 @@ export function handleGlobalEnterQuickSubmit(target) {
   if (form) {
     if (!form.checkValidity()) {
       form.reportValidity();
-      return;
+    } else {
+      // here use the event to trigger the submit event (instead of calling `submit()` method directly)
+      // otherwise the `areYouSure` handler won't be executed, then there will be an annoying "confirm to leave" dialog
+      form.dispatchEvent(new SubmitEvent('submit', {bubbles: true, cancelable: true}));
     }
-
-    // here use the event to trigger the submit event (instead of calling `submit()` method directly)
-    // otherwise the `areYouSure` handler won't be executed, then there will be an annoying "confirm to leave" dialog
-    form.dispatchEvent(new SubmitEvent('submit', {bubbles: true, cancelable: true}));
-    return;
+    return true;
   }
   form = target.closest('.ui.form');
   if (form) {
     form.querySelector('.ui.primary.button')?.click();
+    return true;
   }
+  return false;
 }
index c4e14c62c451b62db79b01870f011d2bca880ae1..39c364ca50f0c8045e4e1fda99a3a8e9b14fff27 100644 (file)
@@ -7,6 +7,7 @@ import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkd
 import {toAbsoluteUrl} from '../utils.js';
 import {initDropzone} from './common-global.js';
 import {POST, GET} from '../modules/fetch.js';
+import {showErrorToast} from '../modules/toast.js';
 
 const {appSubUrl} = window.config;
 
@@ -602,85 +603,69 @@ export function initRepoIssueWipToggle() {
   });
 }
 
-async function pullrequest_targetbranch_change(update_url) {
-  const targetBranch = $('#pull-target-branch').data('branch');
-  const $branchTarget = $('#branch_target');
-  if (targetBranch === $branchTarget.text()) {
-    window.location.reload();
-    return false;
-  }
-  try {
-    await POST(update_url, {data: new URLSearchParams({target_branch: targetBranch})});
-  } catch (error) {
-    console.error(error);
-  } finally {
-    window.location.reload();
-  }
-}
-
 export function initRepoIssueTitleEdit() {
-  // Edit issue title
-  const $issueTitle = $('#issue-title');
-  const $editInput = $('#edit-title-input input');
-
-  const editTitleToggle = function () {
-    toggleElem($issueTitle);
-    toggleElem('.not-in-edit');
-    toggleElem('#edit-title-input');
-    toggleElem('#pull-desc');
-    toggleElem('#pull-desc-edit');
-    toggleElem('.in-edit');
-    toggleElem('.new-issue-button');
-    document.getElementById('issue-title-wrapper')?.classList.toggle('edit-active');
-    $editInput[0].focus();
-    $editInput[0].select();
-    return false;
-  };
-
-  $('#edit-title').on('click', editTitleToggle);
-  $('#cancel-edit-title').on('click', editTitleToggle);
-  $('#save-edit-title').on('click', editTitleToggle).on('click', async function () {
-    const pullrequest_target_update_url = this.getAttribute('data-target-update-url');
-    if (!$editInput.val().length || $editInput.val() === $issueTitle.text()) {
-      $editInput.val($issueTitle.text());
-      await pullrequest_targetbranch_change(pullrequest_target_update_url);
-    } else {
-      try {
-        const params = new URLSearchParams();
-        params.append('title', $editInput.val());
-        const response = await POST(this.getAttribute('data-update-url'), {data: params});
-        const data = await response.json();
-        $editInput.val(data.title);
-        $issueTitle.text(data.title);
-        if (pullrequest_target_update_url) {
-          await pullrequest_targetbranch_change(pullrequest_target_update_url); // it will reload the window
-        } else {
-          window.location.reload();
+  const issueTitleDisplay = document.querySelector('#issue-title-display');
+  const issueTitleEditor = document.querySelector('#issue-title-editor');
+  if (!issueTitleEditor) return;
+
+  const issueTitleInput = issueTitleEditor.querySelector('input');
+  const oldTitle = issueTitleInput.getAttribute('data-old-title');
+  issueTitleDisplay.querySelector('#issue-title-edit-show').addEventListener('click', () => {
+    hideElem(issueTitleDisplay);
+    hideElem('#pull-desc-display');
+    showElem(issueTitleEditor);
+    showElem('#pull-desc-editor');
+    if (!issueTitleInput.value.trim()) {
+      issueTitleInput.value = oldTitle;
+    }
+    issueTitleInput.focus();
+  });
+  issueTitleEditor.querySelector('.ui.cancel.button').addEventListener('click', () => {
+    hideElem(issueTitleEditor);
+    hideElem('#pull-desc-editor');
+    showElem(issueTitleDisplay);
+    showElem('#pull-desc-display');
+  });
+  const editSaveButton = issueTitleEditor.querySelector('.ui.primary.button');
+  editSaveButton.addEventListener('click', async () => {
+    const prTargetUpdateUrl = editSaveButton.getAttribute('data-target-update-url');
+    const newTitle = issueTitleInput.value.trim();
+    try {
+      if (newTitle && newTitle !== oldTitle) {
+        const resp = await POST(editSaveButton.getAttribute('data-update-url'), {data: new URLSearchParams({title: newTitle})});
+        if (!resp.ok) {
+          throw new Error(`Failed to update issue title: ${resp.statusText}`);
         }
-      } catch (error) {
-        console.error(error);
       }
+      if (prTargetUpdateUrl) {
+        const newTargetBranch = document.querySelector('#pull-target-branch').getAttribute('data-branch');
+        const oldTargetBranch = document.querySelector('#branch_target').textContent;
+        if (newTargetBranch !== oldTargetBranch) {
+          const resp = await POST(prTargetUpdateUrl, {data: new URLSearchParams({target_branch: newTargetBranch})});
+          if (!resp.ok) {
+            throw new Error(`Failed to update PR target branch: ${resp.statusText}`);
+          }
+        }
+      }
+      window.location.reload();
+    } catch (error) {
+      console.error(error);
+      showErrorToast(error.message);
     }
-    return false;
   });
 }
 
 export function initRepoIssueBranchSelect() {
-  const changeBranchSelect = function () {
-    const $selectionTextField = $('#pull-target-branch');
-
-    const baseName = $selectionTextField.data('basename');
-    const branchNameNew = $(this).data('branch');
-    const branchNameOld = $selectionTextField.data('branch');
-
-    // Replace branch name to keep translation from HTML template
-    $selectionTextField.html($selectionTextField.html().replace(
-      `${baseName}:${branchNameOld}`,
-      `${baseName}:${branchNameNew}`,
-    ));
-    $selectionTextField.data('branch', branchNameNew); // update branch name in setting
-  };
-  $('#branch-select > .item').on('click', changeBranchSelect);
+  document.querySelector('#branch-select')?.addEventListener('click', (e) => {
+    const el = e.target.closest('.item[data-branch]');
+    if (!el) return;
+    const pullTargetBranch = document.querySelector('#pull-target-branch');
+    const baseName = pullTargetBranch.getAttribute('data-basename');
+    const branchNameNew = el.getAttribute('data-branch');
+    const branchNameOld = pullTargetBranch.getAttribute('data-branch');
+    pullTargetBranch.textContent = pullTargetBranch.textContent.replace(`${baseName}:${branchNameOld}`, `${baseName}:${branchNameNew}`);
+    pullTargetBranch.setAttribute('data-branch', branchNameNew);
+  });
 }
 
 export function initSingleCommentEditor($commentForm) {