mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-27 06:03:51 +03:00
feat: "assign to me" button on PRs and issues
includes: Tests for assignees on issues Move assignees selector of new Issue to assignees.tmpl
This commit is contained in:
parent
b5b9cc1c38
commit
2feb3d03d7
8 changed files with 172 additions and 68 deletions
|
@ -1509,6 +1509,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
|
||||
|
|
|
@ -317,7 +317,7 @@ type WebhookForm struct {
|
|||
type CreateIssueForm struct {
|
||||
Title string `binding:"Required;MaxSize(255)"`
|
||||
LabelIDs string `form:"label_ids"`
|
||||
AssigneeIDs string `form:"assignee_ids"`
|
||||
AssigneeIDs string `form:"assignee_id"`
|
||||
Ref string `form:"ref"`
|
||||
MilestoneID int64
|
||||
ProjectID int64
|
||||
|
|
|
@ -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">
|
||||
<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_id" {{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>
|
||||
<div class="selected">
|
||||
{{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}}
|
||||
<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="item 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>
|
||||
</div>
|
||||
{{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}}
|
||||
</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>
|
||||
|
|
|
@ -103,6 +103,91 @@ 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 page.waitForLoadState('networkidle');
|
||||
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();
|
||||
|
||||
// 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 page.waitForLoadState('networkidle');
|
||||
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 page.waitForLoadState('networkidle');
|
||||
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 page.waitForLoadState('networkidle');
|
||||
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();
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
|
|
Loading…
Reference in a new issue