</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>
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)
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{
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;
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);
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;
}
margin-right: 10px;
}
-.issue-title .edit-zone {
- margin-top: 10px;
-}
-
.issue-state-label {
display: flex !important;
align-items: center !important;
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();
+ }
}
});
}
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;
}
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;
});
}
-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) {