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>tags/v1.22.0
@@ -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> | |||
@@ -63,14 +70,14 @@ | |||
{{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> |
@@ -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) |
@@ -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{ |
@@ -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; |
@@ -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(); | |||
} | |||
} | |||
}); | |||
} |
@@ -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; | |||
} |
@@ -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) { |