Fix issue/PR title edit (#30858) (#30865)

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>
This commit is contained in:
Giteabot 2024-05-05 21:53:12 +08:00 committed by GitHub
parent 054602977a
commit 60fa2a5960
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 141 additions and 139 deletions

View File

@ -4,29 +4,36 @@
</div> </div>
{{end}} {{end}}
<div class="issue-title-header"> <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"> <h1 class="gt-word-break">
<span id="issue-title">{{RenderIssueTitle $.Context .Issue.Title ($.Repository.ComposeMetas ctx) | RenderCodeBlock}} <span class="index">#{{.Issue.Index}}</span> {{RenderIssueTitle $.Context .Issue.Title ($.Repository.ComposeMetas ctx) | RenderCodeBlock}}
</span> <span class="index">#{{.Issue.Index}}</span>
<div id="edit-title-input" class="ui input tw-flex-1 tw-hidden">
<input value="{{.Issue.Title}}" maxlength="255" autocomplete="off">
</div>
</h1> </h1>
<div class="issue-title-buttons"> <div class="issue-title-buttons">
{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}} {{if $canEditIssueTitle}}
<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> <button id="issue-title-edit-show" class="ui small basic button">{{ctx.Locale.Tr "repo.issues.edit"}}</button>
{{end}} {{end}}
{{if not .Issue.IsPull}} {{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}} {{end}}
</div> </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> </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"> <div class="issue-title-meta">
{{if .HasMerged}} {{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> <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>
@ -63,14 +70,14 @@
{{end}} {{end}}
{{else}} {{else}}
{{if .Issue.OriginalAuthor}} {{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}} {{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> <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}} {{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref}}
</span> </span>
{{end}} {{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 floating filter dropdown">
<div class="ui basic small button tw-mr-0"> <div class="ui basic small button tw-mr-0">
<span class="text">{{ctx.Locale.Tr "repo.pulls.compare_compare"}}: {{$.HeadTarget}}</span> <span class="text">{{ctx.Locale.Tr "repo.pulls.compare_compare"}}: {{$.HeadTarget}}</span>

View File

@ -144,7 +144,7 @@ func testNewIssue(t *testing.T, session *TestSession, user, repo, title, content
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc = NewHTMLParser(t, resp.Body) 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) assert.Contains(t, val, title)
val = htmlDoc.doc.Find(".comment .render-content p").First().Text() val = htmlDoc.doc.Find(".comment .render-content p").First().Text()
assert.Equal(t, content, val) assert.Equal(t, content, val)

View File

@ -125,7 +125,7 @@ func TestPullCreate_TitleEscape(t *testing.T) {
req := NewRequest(t, "GET", url) req := NewRequest(t, "GET", url)
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body) 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") assert.True(t, exists, "The template has changed")
req = NewRequestWithValues(t, "POST", editTestTitleURL, map[string]string{ req = NewRequestWithValues(t, "POST", editTestTitleURL, map[string]string{

View File

@ -575,34 +575,7 @@ td .commit-summary {
display: inline-block; display: inline-block;
} }
.issue-title-header {
width: 100%;
padding-bottom: 4px;
margin-bottom: 1rem;
}
.issue-title-meta {
display: flex;
align-items: center;
}
.repository.view.issue .issue-title-buttons,
.repository.view.issue .edit-buttons {
display: flex;
}
@media (max-width: 767.98px) { @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 { .comment.form .issue-content-left .avatar {
display: none; display: none;
} }
@ -617,15 +590,37 @@ td .commit-summary {
} }
} }
/* issue title & meta & edit */
.issue-title-header {
width: 100%;
padding-bottom: 4px;
margin-bottom: 1rem;
}
.issue-title-meta {
display: flex;
align-items: center;
}
.repository.view.issue .issue-title-buttons {
display: flex;
gap: 0.5em;
}
.repository.view.issue .issue-title-buttons > .ui.button {
margin: 0;
height: 35px;
}
.repository.view.issue .issue-title { .repository.view.issue .issue-title {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5em;
margin-bottom: 8px; margin-bottom: 8px;
min-height: 40px; /* avoid layout shift on edit */
} }
.repository.view.issue .issue-title h1 { .repository.view.issue .issue-title h1 {
display: flex;
align-items: center;
flex: 1; flex: 1;
width: 100%; width: 100%;
font-weight: var(--font-weight-normal); font-weight: var(--font-weight-normal);
@ -633,14 +628,24 @@ td .commit-summary {
line-height: 40px; line-height: 40px;
margin: 0; margin: 0;
padding-right: 0.25rem; padding-right: 0.25rem;
min-height: 41px; /* avoid layout shift on edit */
} }
.repository.view.issue .issue-title h1 .ui.input { @media (max-width: 767.98px) {
font-size: 0.5em; .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; font-size: 1.5em;
padding: 2px .5rem; padding: 2px .5rem;
} }
@ -653,10 +658,6 @@ td .commit-summary {
margin-right: 10px; margin-right: 10px;
} }
.issue-title .edit-zone {
margin-top: 10px;
}
.issue-state-label { .issue-state-label {
display: flex !important; display: flex !important;
align-items: center !important; align-items: center !important;

View File

@ -47,10 +47,18 @@ export function initFootLanguageMenu() {
export function initGlobalEnterQuickSubmit() { export function initGlobalEnterQuickSubmit() {
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
const isQuickSubmitEnter = ((e.ctrlKey && !e.altKey) || e.metaKey) && (e.key === 'Enter'); if (e.key !== 'Enter') return;
if (isQuickSubmitEnter && e.target.matches('textarea')) { const hasCtrlOrMeta = ((e.ctrlKey || e.metaKey) && !e.altKey);
e.preventDefault(); if (hasCtrlOrMeta && e.target.matches('textarea')) {
handleGlobalEnterQuickSubmit(e.target); 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();
}
} }
}); });
} }

View File

@ -3,16 +3,17 @@ export function handleGlobalEnterQuickSubmit(target) {
if (form) { if (form) {
if (!form.checkValidity()) { if (!form.checkValidity()) {
form.reportValidity(); 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}));
} }
return 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;
} }
form = target.closest('.ui.form'); form = target.closest('.ui.form');
if (form) { if (form) {
form.querySelector('.ui.primary.button')?.click(); form.querySelector('.ui.primary.button')?.click();
return true;
} }
return false;
} }

View File

@ -7,6 +7,7 @@ import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkd
import {toAbsoluteUrl} from '../utils.js'; import {toAbsoluteUrl} from '../utils.js';
import {initDropzone} from './common-global.js'; import {initDropzone} from './common-global.js';
import {POST, GET} from '../modules/fetch.js'; import {POST, GET} from '../modules/fetch.js';
import {showErrorToast} from '../modules/toast.js';
const {appSubUrl} = window.config; 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() { export function initRepoIssueTitleEdit() {
// Edit issue title const issueTitleDisplay = document.querySelector('#issue-title-display');
const $issueTitle = $('#issue-title'); const issueTitleEditor = document.querySelector('#issue-title-editor');
const $editInput = $('#edit-title-input input'); if (!issueTitleEditor) return;
const editTitleToggle = function () { const issueTitleInput = issueTitleEditor.querySelector('input');
toggleElem($issueTitle); const oldTitle = issueTitleInput.getAttribute('data-old-title');
toggleElem('.not-in-edit'); issueTitleDisplay.querySelector('#issue-title-edit-show').addEventListener('click', () => {
toggleElem('#edit-title-input'); hideElem(issueTitleDisplay);
toggleElem('#pull-desc'); hideElem('#pull-desc-display');
toggleElem('#pull-desc-edit'); showElem(issueTitleEditor);
toggleElem('.in-edit'); showElem('#pull-desc-editor');
toggleElem('.new-issue-button'); if (!issueTitleInput.value.trim()) {
document.getElementById('issue-title-wrapper')?.classList.toggle('edit-active'); issueTitleInput.value = oldTitle;
$editInput[0].focus(); }
$editInput[0].select(); issueTitleInput.focus();
return false; });
}; issueTitleEditor.querySelector('.ui.cancel.button').addEventListener('click', () => {
hideElem(issueTitleEditor);
$('#edit-title').on('click', editTitleToggle); hideElem('#pull-desc-editor');
$('#cancel-edit-title').on('click', editTitleToggle); showElem(issueTitleDisplay);
$('#save-edit-title').on('click', editTitleToggle).on('click', async function () { showElem('#pull-desc-display');
const pullrequest_target_update_url = this.getAttribute('data-target-update-url'); });
if (!$editInput.val().length || $editInput.val() === $issueTitle.text()) { const editSaveButton = issueTitleEditor.querySelector('.ui.primary.button');
$editInput.val($issueTitle.text()); editSaveButton.addEventListener('click', async () => {
await pullrequest_targetbranch_change(pullrequest_target_update_url); const prTargetUpdateUrl = editSaveButton.getAttribute('data-target-update-url');
} else { const newTitle = issueTitleInput.value.trim();
try { try {
const params = new URLSearchParams(); if (newTitle && newTitle !== oldTitle) {
params.append('title', $editInput.val()); const resp = await POST(editSaveButton.getAttribute('data-update-url'), {data: new URLSearchParams({title: newTitle})});
const response = await POST(this.getAttribute('data-update-url'), {data: params}); if (!resp.ok) {
const data = await response.json(); throw new Error(`Failed to update issue title: ${resp.statusText}`);
$editInput.val(data.title); }
$issueTitle.text(data.title); }
if (pullrequest_target_update_url) { if (prTargetUpdateUrl) {
await pullrequest_targetbranch_change(pullrequest_target_update_url); // it will reload the window const newTargetBranch = document.querySelector('#pull-target-branch').getAttribute('data-branch');
} else { const oldTargetBranch = document.querySelector('#branch_target').textContent;
window.location.reload(); if (newTargetBranch !== oldTargetBranch) {
} const resp = await POST(prTargetUpdateUrl, {data: new URLSearchParams({target_branch: newTargetBranch})});
} catch (error) { if (!resp.ok) {
console.error(error); 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() { export function initRepoIssueBranchSelect() {
const changeBranchSelect = function () { document.querySelector('#branch-select')?.addEventListener('click', (e) => {
const $selectionTextField = $('#pull-target-branch'); const el = e.target.closest('.item[data-branch]');
if (!el) return;
const baseName = $selectionTextField.data('basename'); const pullTargetBranch = document.querySelector('#pull-target-branch');
const branchNameNew = $(this).data('branch'); const baseName = pullTargetBranch.getAttribute('data-basename');
const branchNameOld = $selectionTextField.data('branch'); const branchNameNew = el.getAttribute('data-branch');
const branchNameOld = pullTargetBranch.getAttribute('data-branch');
// Replace branch name to keep translation from HTML template pullTargetBranch.textContent = pullTargetBranch.textContent.replace(`${baseName}:${branchNameOld}`, `${baseName}:${branchNameNew}`);
$selectionTextField.html($selectionTextField.html().replace( pullTargetBranch.setAttribute('data-branch', branchNameNew);
`${baseName}:${branchNameOld}`, });
`${baseName}:${branchNameNew}`,
));
$selectionTextField.data('branch', branchNameNew); // update branch name in setting
};
$('#branch-select > .item').on('click', changeBranchSelect);
} }
export function initSingleCommentEditor($commentForm) { export function initSingleCommentEditor($commentForm) {