diff options
author | TimedIn <git@timedin.net> | 2024-10-08 10:39:52 +0200 |
---|---|---|
committer | Otto Richter <git@otto.splvs.net> | 2024-10-08 18:36:37 +0200 |
commit | 2feb3d03d73a89c0d52a174c92915ca3930f08a7 (patch) | |
tree | 0d181ba80fcfb105dc2eaf0e914e252a500fbbd4 | |
parent | Merge pull request 'Update dependency vue to v3.5.11 (forgejo)' (#5458) from ... (diff) | |
download | forgejo-2feb3d03d73a89c0d52a174c92915ca3930f08a7.tar.xz forgejo-2feb3d03d73a89c0d52a174c92915ca3930f08a7.zip |
feat: "assign to me" button on PRs and issues
includes:
Tests for assignees on issues
Move assignees selector of new Issue to assignees.tmpl
-rw-r--r-- | options/locale/locale_en-US.ini | 1 | ||||
-rw-r--r-- | services/forms/repo_form.go | 2 | ||||
-rw-r--r-- | templates/repo/issue/new_form.tmpl | 37 | ||||
-rw-r--r-- | templates/repo/issue/view_content/sidebar.tmpl | 2 | ||||
-rw-r--r-- | templates/repo/issue/view_content/sidebar/assignees.tmpl | 35 | ||||
-rw-r--r-- | tests/e2e/issue-sidebar.test.e2e.js | 85 | ||||
-rw-r--r-- | web_src/js/features/repo-issue.js | 54 | ||||
-rw-r--r-- | web_src/js/features/repo-legacy.js | 22 |
8 files changed, 171 insertions, 67 deletions
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index a4e653b6ba..708bd49142 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -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 diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index c3d9c3edc9..654106c47a 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -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 diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl index fe10c1f9b9..c2cf4ee7a7 100644 --- a/templates/repo/issue/new_form.tmpl +++ b/templates/repo/issue/new_form.tmpl @@ -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"> diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index ba15539841..623023bd51 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -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}} diff --git a/templates/repo/issue/view_content/sidebar/assignees.tmpl b/templates/repo/issue/view_content/sidebar/assignees.tmpl index e51bda95de..8e5043c076 100644 --- a/templates/repo/issue/view_content/sidebar/assignees.tmpl +++ b/templates/repo/issue/view_content/sidebar/assignees.tmpl @@ -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> + <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> + {{end}} + {{end}} + </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}} + {{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> - </div> + {{end}} {{end}} </div> </div> diff --git a/tests/e2e/issue-sidebar.test.e2e.js b/tests/e2e/issue-sidebar.test.e2e.js index f9e8380fd4..0409eab9e1 100644 --- a/tests/e2e/issue-sidebar.test.e2e.js +++ b/tests/e2e/issue-sidebar.test.e2e.js @@ -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); diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js index feba449c16..c4bd70b71d 100644 --- a/web_src/js/features/repo-issue.js +++ b/web_src/js/features/repo-issue.js @@ -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 diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js index b25fc28dea..cb92fcedb5 100644 --- a/web_src/js/features/repo-legacy.js +++ b/web_src/js/features/repo-legacy.js @@ -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'); |