mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-22 12:54:53 -05:00
Merge pull request '[v9.0/forgejo] feat: "Assign to me" button on PR and Issues #5215' (#5524) from bp-v9.0/forgejo-2feb3d0 into v9.0/forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5524 Reviewed-by: Otto <otto@codeberg.org>
This commit is contained in:
commit
d8c8fa9bae
7 changed files with 172 additions and 70 deletions
|
@ -1507,6 +1507,7 @@ issues.new.closed_milestone = Closed milestones
|
|||
issues.new.assignees = Assignees
|
||||
issues.new.clear_assignees = Clear assignees
|
||||
issues.new.no_assignees = No assignees
|
||||
issues.new.assign_to_me = Assign to me
|
||||
issues.new.no_reviewers = No reviewers
|
||||
issues.edit.already_changed = Unable to save changes to the issue. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes
|
||||
issues.choose.get_started = Get started
|
||||
|
|
|
@ -140,42 +140,7 @@
|
|||
</div>
|
||||
{{end}}
|
||||
<div class="divider"></div>
|
||||
<input id="assignee_ids" name="assignee_ids" type="hidden" value="{{.assignee_ids}}">
|
||||
<div class="ui {{if not .HasIssuesOrPullsWritePermission}}disabled{{end}} floating jump select-assignees dropdown">
|
||||
<span class="text flex-text-block">
|
||||
<strong>{{ctx.Locale.Tr "repo.issues.new.assignees"}}</strong>
|
||||
{{if .HasIssuesOrPullsWritePermission}}
|
||||
{{svg "octicon-gear" 16 "tw-ml-1"}}
|
||||
{{end}}
|
||||
</span>
|
||||
<div class="filter menu" data-id="#assignee_ids">
|
||||
<div class="ui icon search input">
|
||||
<i class="icon">{{svg "octicon-search" 16}}</i>
|
||||
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_assignees"}}">
|
||||
</div>
|
||||
<div class="no-select item">{{ctx.Locale.Tr "repo.issues.new.clear_assignees"}}</div>
|
||||
{{range .Assignees}}
|
||||
<a class="item muted" href="#" data-id="{{.ID}}" data-id-selector="#assignee_{{.ID}}">
|
||||
<span class="octicon-check tw-invisible">{{svg "octicon-check"}}</span>
|
||||
<span class="text">
|
||||
{{ctx.AvatarUtils.Avatar . 28 "tw-mr-2"}}{{template "repo/search_name" .}}
|
||||
</span>
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui assignees list">
|
||||
<span class="no-select item {{if .HasSelectedLabel}}tw-hidden{{end}}">
|
||||
{{ctx.Locale.Tr "repo.issues.new.no_assignees"}}
|
||||
</span>
|
||||
<div class="selected">
|
||||
{{range .Assignees}}
|
||||
<a class="item tw-p-1 muted tw-hidden" id="assignee_{{.ID}}" href="{{$.RepoLink}}/issues?assignee={{.ID}}">
|
||||
{{ctx.AvatarUtils.Avatar . 28 "tw-mr-2 tw-align-middle"}}{{.GetDisplayName}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{template "repo/issue/view_content/sidebar/assignees" dict "isExistingIssue" false "." .}}
|
||||
{{if and .PageIsComparePull (not (eq .HeadRepo.FullName .BaseCompareRepo.FullName)) .CanWriteToHeadRepo}}
|
||||
<div class="divider"></div>
|
||||
<div class="inline field">
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
{{template "repo/issue/view_content/sidebar/projects" .}}
|
||||
<div class="divider"></div>
|
||||
|
||||
{{template "repo/issue/view_content/sidebar/assignees" .}}
|
||||
{{template "repo/issue/view_content/sidebar/assignees" dict "isExistingIssue" true "." .}}
|
||||
<div class="divider"></div>
|
||||
|
||||
{{if .Participants}}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<input id="assignee_id" name="assignee_id" type="hidden" value="{{.assignee_id}}">
|
||||
<div class="ui {{if or (not .HasIssuesOrPullsWritePermission) .Repository.IsArchived}}disabled{{end}} floating jump select-assignees-modify dropdown">
|
||||
<input id="assignee_ids" name="assignee_ids" type="hidden" value="{{.assignee_ids}}">
|
||||
<div class="ui {{if or (not .HasIssuesOrPullsWritePermission) .Repository.IsArchived}}disabled{{end}} floating jump select-assignees{{if .isExistingIssue}}-modify{{end}} dropdown">
|
||||
<a class="text muted flex-text-block">
|
||||
<strong>{{ctx.Locale.Tr "repo.issues.new.assignees"}}</strong>
|
||||
{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
|
||||
{{svg "octicon-gear" 16 "tw-ml-1"}}
|
||||
{{end}}
|
||||
</a>
|
||||
<div class="filter menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/assignee">
|
||||
<div class="filter menu" {{if .isExistingIssue}} data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/assignee" {{else}} data-id="#assignee_ids" {{end}}>
|
||||
<div class="ui icon search input">
|
||||
<i class="icon">{{svg "octicon-search" 16}}</i>
|
||||
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_assignees"}}">
|
||||
|
@ -31,15 +31,32 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="ui assignees list">
|
||||
<span class="no-select item {{if .Issue.Assignees}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_assignees"}}</span>
|
||||
<span class="no-select item {{if .Issue.Assignees}}tw-hidden{{end}}">
|
||||
{{ctx.Locale.Tr "repo.issues.new.no_assignees"}}
|
||||
{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
|
||||
{{with index .Assignees 0}}
|
||||
–
|
||||
<a class="select-assign-me" href="#" data-id="{{.ID}}" data-id-selector="#assignee_{{.ID}}" {{if $.isExistingIssue}} data-action="update" {{end}} data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/assignee" role="option">
|
||||
{{ctx.Locale.Tr "repo.issues.new.assign_to_me"}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</span>
|
||||
<div class="selected">
|
||||
{{if .isExistingIssue}}
|
||||
{{range .Issue.Assignees}}
|
||||
<div class="item">
|
||||
<a class="muted sidebar-item-link" href="{{$.RepoLink}}/{{if $.Issue.IsPull}}pulls{{else}}issues{{end}}?assignee={{.ID}}">
|
||||
{{ctx.AvatarUtils.Avatar . 28 "tw-mr-2"}}
|
||||
{{.GetDisplayName}}
|
||||
{{ctx.AvatarUtils.Avatar . 28 "tw-mr-2"}}{{.GetDisplayName}}
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
{{range .Assignees}}
|
||||
<a class="item tw-p-1 muted tw-hidden" id="assignee_{{.ID}}" href="{{$.RepoLink}}/{{if $.Issue.IsPull}}pulls{{else}}issues{{end}}?assignee={{.ID}}">
|
||||
{{ctx.AvatarUtils.Avatar . 28 "tw-mr-2"}}{{.GetDisplayName}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -96,6 +96,89 @@ test('Issue: Labels', async ({browser}, workerInfo) => {
|
|||
await expect(labelList.filter({hasText: 'label1'})).toBeVisible();
|
||||
});
|
||||
|
||||
test('Issue: Assignees', async ({browser}, workerInfo) => {
|
||||
test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636');
|
||||
const page = await login({browser}, workerInfo);
|
||||
// select label list in sidebar only
|
||||
const assigneesList = page.locator('.issue-content-right .assignees.list .selected .item a');
|
||||
|
||||
const response = await page.goto('/org3/repo3/issues/1');
|
||||
await expect(response?.status()).toBe(200);
|
||||
// preconditions
|
||||
await expect(assigneesList.filter({hasText: 'user2'})).toBeVisible();
|
||||
await expect(assigneesList.filter({hasText: 'user4'})).toBeHidden();
|
||||
await expect(page.locator('.ui.assignees.list .item.no-select')).toBeHidden();
|
||||
|
||||
// Clear all assignees
|
||||
await page.locator('.select-assignees-modify.dropdown').click();
|
||||
await page.locator('.select-assignees-modify.dropdown .no-select.item').click();
|
||||
await expect(assigneesList.filter({hasText: 'user2'})).toBeHidden();
|
||||
await expect(assigneesList.filter({hasText: 'user4'})).toBeHidden();
|
||||
await expect(page.locator('.ui.assignees.list .item.no-select')).toBeVisible();
|
||||
await expect(page.locator('.select-assign-me')).toBeVisible();
|
||||
|
||||
// Assign other user (with searchbox)
|
||||
await page.locator('.select-assignees-modify.dropdown').click();
|
||||
await page.type('.select-assignees-modify .menu .search input', 'user4');
|
||||
await expect(page.locator('.select-assignees-modify .menu .item').filter({hasText: 'user2'})).toBeHidden();
|
||||
await expect(page.locator('.select-assignees-modify .menu .item').filter({hasText: 'user4'})).toBeVisible();
|
||||
await page.locator('.select-assignees-modify .menu .item').filter({hasText: 'user4'}).click();
|
||||
await page.locator('.select-assignees-modify.dropdown').click();
|
||||
await expect(assigneesList.filter({hasText: 'user4'})).toBeVisible();
|
||||
|
||||
// remove user4
|
||||
await page.locator('.select-assignees-modify.dropdown').click();
|
||||
await page.locator('.select-assignees-modify .menu .item').filter({hasText: 'user4'}).click();
|
||||
await page.locator('.select-assignees-modify.dropdown').click();
|
||||
await expect(page.locator('.ui.assignees.list .item.no-select')).toBeVisible();
|
||||
await expect(assigneesList.filter({hasText: 'user4'})).toBeHidden();
|
||||
|
||||
// Test assign me
|
||||
await page.locator('.ui.assignees .select-assign-me').click();
|
||||
await expect(assigneesList.filter({hasText: 'user2'})).toBeVisible();
|
||||
await expect(page.locator('.ui.assignees.list .item.no-select')).toBeHidden();
|
||||
});
|
||||
|
||||
test('New Issue: Assignees', async ({browser}, workerInfo) => {
|
||||
test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636');
|
||||
const page = await login({browser}, workerInfo);
|
||||
// select label list in sidebar only
|
||||
const assigneesList = page.locator('.issue-content-right .assignees.list .selected .item');
|
||||
|
||||
const response = await page.goto('/org3/repo3/issues/new');
|
||||
await expect(response?.status()).toBe(200);
|
||||
// preconditions
|
||||
await expect(page.locator('.ui.assignees.list .item.no-select')).toBeVisible();
|
||||
await expect(assigneesList.filter({hasText: 'user2'})).toBeHidden();
|
||||
await expect(assigneesList.filter({hasText: 'user4'})).toBeHidden();
|
||||
|
||||
// Assign other user (with searchbox)
|
||||
await page.locator('.select-assignees.dropdown').click();
|
||||
await page.type('.select-assignees .menu .search input', 'user4');
|
||||
await expect(page.locator('.select-assignees .menu .item').filter({hasText: 'user2'})).toBeHidden();
|
||||
await expect(page.locator('.select-assignees .menu .item').filter({hasText: 'user4'})).toBeVisible();
|
||||
await page.locator('.select-assignees .menu .item').filter({hasText: 'user4'}).click();
|
||||
await page.locator('.select-assignees.dropdown').click();
|
||||
await expect(assigneesList.filter({hasText: 'user4'})).toBeVisible();
|
||||
|
||||
// remove user4
|
||||
await page.locator('.select-assignees.dropdown').click();
|
||||
await page.locator('.select-assignees .menu .item').filter({hasText: 'user4'}).click();
|
||||
await page.locator('.select-assignees.dropdown').click();
|
||||
await expect(page.locator('.ui.assignees.list .item.no-select')).toBeVisible();
|
||||
await expect(assigneesList.filter({hasText: 'user4'})).toBeHidden();
|
||||
|
||||
// Test assign me
|
||||
await page.locator('.ui.assignees .select-assign-me').click();
|
||||
await expect(assigneesList.filter({hasText: 'user2'})).toBeVisible();
|
||||
await expect(page.locator('.ui.assignees.list .item.no-select')).toBeHidden();
|
||||
|
||||
await page.locator('.select-assignees.dropdown').click();
|
||||
await page.fill('.select-assignees .menu .search input', '');
|
||||
await page.locator('.select-assignees.dropdown .no-select.item').click();
|
||||
await expect(page.locator('.select-assign-me')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Issue: Milestone', async ({browser}, workerInfo) => {
|
||||
test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636');
|
||||
const page = await login({browser}, workerInfo);
|
||||
|
|
|
@ -12,6 +12,26 @@ import {emojiHTML} from './emoji.js';
|
|||
|
||||
const {appSubUrl} = window.config;
|
||||
|
||||
// if there are draft comments, confirm before reloading, to avoid losing comments
|
||||
export function reloadConfirmDraftComment() {
|
||||
const commentTextareas = [
|
||||
document.querySelector('.edit-content-zone:not(.tw-hidden) textarea'),
|
||||
document.querySelector('#comment-form textarea'),
|
||||
];
|
||||
for (const textarea of commentTextareas) {
|
||||
// Most users won't feel too sad if they lose a comment with 10 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 > 10) {
|
||||
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 initRepoIssueTimeTracking() {
|
||||
$(document).on('click', '.issue-add-time', () => {
|
||||
$('.issue-start-time-modal').modal({
|
||||
|
@ -668,6 +688,40 @@ export function initRepoIssueBranchSelect() {
|
|||
});
|
||||
}
|
||||
|
||||
export function initRepoIssueAssignMe() {
|
||||
// Assign to me button
|
||||
document.querySelector('.ui.assignees.list .item.no-select .select-assign-me')
|
||||
?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const selectMe = e.target;
|
||||
const noSelect = selectMe.parentElement;
|
||||
const selectorList = document.querySelector('.ui.select-assignees .menu');
|
||||
|
||||
if (selectMe.getAttribute('data-action') === 'update') {
|
||||
(async () => {
|
||||
await updateIssuesMeta(
|
||||
selectMe.getAttribute('data-update-url'),
|
||||
selectMe.getAttribute('data-action'),
|
||||
selectMe.getAttribute('data-issue-id'),
|
||||
selectMe.getAttribute('data-id'),
|
||||
);
|
||||
reloadConfirmDraftComment();
|
||||
})();
|
||||
} else {
|
||||
for (const item of selectorList.querySelectorAll('.item')) {
|
||||
if (item.getAttribute('data-id') === selectMe.getAttribute('data-id')) {
|
||||
item.classList.add('checked');
|
||||
item.querySelector('.octicon-check').classList.remove('tw-invisible');
|
||||
}
|
||||
}
|
||||
document.querySelector(selectMe.getAttribute('data-id-selector')).classList.remove('tw-hidden');
|
||||
noSelect.classList.add('tw-hidden');
|
||||
document.querySelector(selectorList.getAttribute('data-id')).value = selectMe.getAttribute('data-id');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function initSingleCommentEditor($commentForm) {
|
||||
// pages:
|
||||
// * normal new issue/pr page, no status-button
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
initRepoIssueComments, initRepoIssueDependencyDelete, initRepoIssueReferenceIssue,
|
||||
initRepoIssueTitleEdit, initRepoIssueWipToggle,
|
||||
initRepoPullRequestUpdate, updateIssuesMeta, handleReply, initIssueTemplateCommentEditors, initSingleCommentEditor,
|
||||
initRepoIssueAssignMe, reloadConfirmDraftComment,
|
||||
} from './repo-issue.js';
|
||||
import {initUnicodeEscapeButton} from './repo-unicode-escape.js';
|
||||
import {svg} from '../svg.js';
|
||||
|
@ -29,26 +30,6 @@ import {POST, GET} from '../modules/fetch.js';
|
|||
|
||||
const {csrfToken} = window.config;
|
||||
|
||||
// if there are draft comments, confirm before reloading, to avoid losing comments
|
||||
function reloadConfirmDraftComment() {
|
||||
const commentTextareas = [
|
||||
document.querySelector('.edit-content-zone:not(.tw-hidden) textarea'),
|
||||
document.querySelector('#comment-form textarea'),
|
||||
];
|
||||
for (const textarea of commentTextareas) {
|
||||
// Most users won't feel too sad if they lose a comment with 10 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 > 10) {
|
||||
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() {
|
||||
const $commentForm = $('.comment.form');
|
||||
if (!$commentForm.length) return;
|
||||
|
@ -243,6 +224,7 @@ export function initRepoCommentForm() {
|
|||
// Init labels and assignees
|
||||
initListSubmits('select-label', 'labels');
|
||||
initListSubmits('select-assignees', 'assignees');
|
||||
initRepoIssueAssignMe();
|
||||
initListSubmits('select-assignees-modify', 'assignees');
|
||||
initListSubmits('select-reviewers-modify', 'assignees');
|
||||
|
||||
|
@ -274,7 +256,7 @@ export function initRepoCommentForm() {
|
|||
icon = svg('octicon-milestone', 18, 'tw-mr-2');
|
||||
} else if (input_id === '#project_id') {
|
||||
icon = svg('octicon-project', 18, 'tw-mr-2');
|
||||
} else if (input_id === '#assignee_id') {
|
||||
} else if (input_id === '#assignee_ids') {
|
||||
icon = `<img class="ui avatar image tw-mr-2" alt="avatar" src=${$(this).data('avatar')}>`;
|
||||
}
|
||||
|
||||
|
@ -314,7 +296,7 @@ export function initRepoCommentForm() {
|
|||
// Milestone, Assignee, Project
|
||||
selectItem('.select-project', '#project_id');
|
||||
selectItem('.select-milestone', '#milestone_id');
|
||||
selectItem('.select-assignee', '#assignee_id');
|
||||
selectItem('.select-assignee', '#assignee_ids');
|
||||
}
|
||||
|
||||
async function onEditContent(event) {
|
||||
|
|
Loading…
Reference in a new issue