Commit 4c30e09e authored by Steve Mokris's avatar Steve Mokris

Link new issue to original via checkbox

When using the issue header action to create a new issue,
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68226 added the
text "Related to #<parent-issue>." to the new issue's description.

This improves the UX of that change by:

- Renaming the "New issue" menu item to "New related issue" to clarify
  that it will create a link.
- Replacing the added description text with a new checkbox widget, to
  avoid obliterating the issue template. This checkbox widget is only
  shown when the new `add_related_issue` URL query parameter is present.

Changelog: changed
parent 739c74e6
......@@ -128,7 +128,7 @@ export default {
});
},
newIssueTypeText() {
return sprintf(__('New %{issueType}'), { issueType: this.issueType });
return sprintf(__('New related %{issueType}'), { issueType: this.issueType });
},
showToggleIssueStateButton() {
const canClose = !this.isClosed && this.canUpdateIssue;
......
......@@ -107,6 +107,8 @@ class Projects::IssuesController < Projects::ApplicationController
@issue = @noteable = service.execute
@add_related_issue = add_related_issue
@merge_request_to_resolve_discussions_of = service.merge_request_to_resolve_discussions_of
if params[:discussion_to_resolve]
......@@ -123,6 +125,7 @@ class Projects::IssuesController < Projects::ApplicationController
def create
create_params = issue_params.merge(
add_related_issue: add_related_issue,
merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of],
discussion_to_resolve: params[:discussion_to_resolve]
)
......@@ -378,6 +381,11 @@ class Projects::IssuesController < Projects::ApplicationController
action_name == 'service_desk'
end
def add_related_issue
add_related_issue = project.issues.find_by_iid(params[:add_related_issue])
add_related_issue if Ability.allowed?(current_user, :read_issue, add_related_issue)
end
# Overridden in EE
def create_vulnerability_issue_feedback(issue); end
end
......
......@@ -169,7 +169,7 @@ module IssuesHelper
end
def issue_header_actions_data(project, issuable, current_user)
new_issuable_params = { issue: { description: _('Related to #%{issue_id}.') % { issue_id: issuable.iid } + "\n\n" } }
new_issuable_params = { issue: {}, add_related_issue: issuable.iid }
if issuable.incident?
new_issuable_params[:issuable_template] = 'incident'
new_issuable_params[:issue][:issue_type] = 'incident'
......
......@@ -23,6 +23,7 @@ module Issues
handle_move_between_ids(@issue)
@add_related_issue ||= params.delete(:add_related_issue)
filter_resolve_discussion_params
create(@issue, skip_system_notes: skip_system_notes)
......@@ -52,6 +53,7 @@ module Issues
# Add new items to Issues::AfterCreateService if they can be performed in Sidekiq
def after_create(issue)
user_agent_detail_service.create
handle_add_related_issue(issue)
resolve_discussions_with_issue(issue)
create_escalation_status(issue)
......@@ -91,6 +93,12 @@ module Issues
def user_agent_detail_service
UserAgentDetailService.new(spammable: @issue, spam_params: spam_params)
end
def handle_add_related_issue(issue)
return unless @add_related_issue
IssueLinks::CreateService.new(issue, issue.author, { target_issuable: @add_related_issue }).execute
end
end
end
......
......@@ -4,6 +4,16 @@
- has_due_date = issuable.has_attribute?(:due_date)
- form = local_assigns.fetch(:form)
- if @add_related_issue
.form-group.row
.offset-sm-2.col-sm-10
.form-check
= check_box_tag :add_related_issue, @add_related_issue.iid, true, class: 'form-check-input'
= label_tag :add_related_issue, class: 'form-check-label' do
- add_related_issue_link = link_to "\##{@add_related_issue.iid}", issue_path(@add_related_issue), class: ['has-tooltip'], title: @add_related_issue.title
#{_('Relate to %{issuable_type} %{add_related_issue_link}').html_safe % { issuable_type: @add_related_issue.issue_type, add_related_issue_link: add_related_issue_link }}
%p.text-muted= _('Adds this %{issuable_type} as related to the %{issuable_type} it was created from') % { issuable_type: @add_related_issue.issue_type }
- if issuable.respond_to?(:confidential) && can?(current_user, :set_confidentiality, issuable)
.form-group.row
.offset-sm-2.col-sm-10
......
......@@ -19,7 +19,7 @@ You can create an issue in many ways in GitLab:
- [From a project](#from-a-project)
- [From a group](#from-a-group)
- [From another issue](#from-another-issue)
- [From another issue or incident](#from-another-issue-or-incident)
- [From an issue board](#from-an-issue-board)
- [By sending an email](#by-sending-an-email)
- [Using a URL with prefilled values](#using-a-url-with-prefilled-values)
......@@ -70,9 +70,10 @@ The newly created issue opens.
The project you selected most recently becomes the default for your next visit.
This can save you a lot of time and clicks, if you mostly create issues for the same project.
### From another issue
### From another issue or incident
> New issue becoming linked to the issue of origin [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68226) in GitLab 14.3.
> - New issue becoming linked to the issue of origin [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68226) in GitLab 14.3.
> - **Relate to…** checkbox [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/198494) in GitLab 14.9.
You can create a new issue from an existing one. The two issues can then be marked as related.
......@@ -83,10 +84,10 @@ Prerequisites:
To create an issue from another issue:
1. In an existing issue, select the vertical ellipsis (**{ellipsis_v}**).
1. Select **New issue**.
1. Select **New related issue**.
1. Complete the [fields](#fields-in-the-new-issue-form).
The new issue's description is prefilled with `Related to #123`, where `123` is the ID of the
issue of origin. If you keep this mention in the description, the two issues become
The new issue form has a **Relate to issue #123** checkbox, where `123` is the ID of the
issue of origin. If you keep this checkbox checked, the two issues become
[linked](related_issues.md).
1. Select **Create issue**.
......@@ -160,7 +161,8 @@ To regenerate the email address:
### Using a URL with prefilled values
> Ability to use both `issuable_template` and `issue[description]` in the same URL [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80554) in GitLab 14.9.
> - Ability to use both `issuable_template` and `issue[description]` in the same URL [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80554) in GitLab 14.9.
> - Ability to specify `add_related_issue` [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/198494) in GitLab 14.9.
To link directly to the new issue page with prefilled fields, use query
string parameters in a URL. You can embed a URL in an external
......@@ -173,6 +175,7 @@ HTML page to create issues with certain fields prefilled.
| Description template | `issuable_template` | Must be [URL-encoded](../../../api/index.md#namespaced-path-encoding). |
| Description | `issue[description]` | Must be [URL-encoded](../../../api/index.md#namespaced-path-encoding). If used in combination with `issuable_template` or a [default issue template](../description_templates.md#set-a-default-template-for-merge-requests-and-issues), the `issue[description]` value is appended to the template. |
| Confidential | `issue[confidential]` | If `true`, the issue is marked as confidential. |
| Relate to… | `add_related_issue` | A numeric issue ID. If present, the issue form shows a [**Relate to…** checkbox](#from-another-issue-or-incident) to optionally link the new issue to the specified existing issue. |
Adapt these examples to form your new issue URL with prefilled fields.
To create an issue in the GitLab project:
......
......@@ -2394,6 +2394,9 @@ msgstr ""
msgid "Adds email participant(s)."
msgstr ""
msgid "Adds this %{issuable_type} as related to the %{issuable_type} it was created from"
msgstr ""
msgid "Adjust how frequently the GitLab UI polls for updates."
msgstr ""
......@@ -24277,9 +24280,6 @@ msgstr ""
msgid "New"
msgstr ""
msgid "New %{issueType}"
msgstr ""
msgid "New %{type} in %{project}"
msgstr ""
......@@ -24447,6 +24447,9 @@ msgstr ""
msgid "New public deploy key"
msgstr ""
msgid "New related %{issueType}"
msgstr ""
msgid "New release"
msgstr ""
......@@ -30220,6 +30223,9 @@ msgstr ""
msgid "Rejected (closed)"
msgstr ""
msgid "Relate to %{issuable_type} %{add_related_issue_link}"
msgstr ""
msgid "Related feature flags"
msgstr ""
......@@ -30229,9 +30235,6 @@ msgstr ""
msgid "Related merge requests"
msgstr ""
msgid "Related to #%{issue_id}."
msgstr ""
msgid "Relates to"
msgstr ""
......
......@@ -26,7 +26,7 @@ RSpec.describe "User views incident" do
it 'shows the merge request and incident actions', :js, :aggregate_failures do
click_button 'Incident actions'
expect(page).to have_link('New incident', href: new_project_issue_path(project, { issuable_template: 'incident', issue: { issue_type: 'incident', description: "Related to \##{incident.iid}.\n\n" } }))
expect(page).to have_link('New related incident', href: new_project_issue_path(project, { issuable_template: 'incident', issue: { issue_type: 'incident' }, add_related_issue: incident.iid }))
expect(page).to have_button('Create merge request')
expect(page).to have_button('Close incident')
end
......
......@@ -8,16 +8,19 @@ RSpec.describe 'New/edit issue', :js do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let_it_be(:user2) { create(:user) }
let_it_be(:guest) { create(:user) }
let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:label) { create(:label, project: project) }
let_it_be(:label2) { create(:label, project: project) }
let_it_be(:issue) { create(:issue, project: project, assignees: [user], milestone: milestone) }
let_it_be(:confidential_issue) { create(:issue, project: project, assignees: [user], milestone: milestone, confidential: true) }
let(:current_user) { user }
before_all do
project.add_maintainer(user)
project.add_maintainer(user2)
project.add_guest(guest)
end
before do
......@@ -357,6 +360,61 @@ RSpec.describe 'New/edit issue', :js do
end
end
describe 'new issue from related issue' do
it 'does not offer to link the new issue to any other issues if the URL parameter is absent' do
visit new_project_issue_path(project)
expect(page).not_to have_selector '#add_related_issue'
expect(page).not_to have_text "Relate to"
end
context 'guest' do
let(:current_user) { guest }
it 'does not offer to link the new issue to an issue that the user does not have access to' do
visit new_project_issue_path(project, { add_related_issue: confidential_issue.iid })
expect(page).not_to have_selector '#add_related_issue'
expect(page).not_to have_text "Relate to"
end
end
it 'links the new issue and the issue of origin' do
visit new_project_issue_path(project, { add_related_issue: issue.iid })
expect(page).to have_selector '#add_related_issue'
expect(page).to have_text "Relate to issue \##{issue.iid}"
expect(page).to have_text 'Adds this issue as related to the issue it was created from'
fill_in 'issue_title', with: 'title'
click_button 'Create issue'
page.within '#related-issues' do
expect(page).to have_text "\##{issue.iid}"
end
end
it 'links the new incident and the incident of origin' do
incident = create(:incident, project: project)
visit new_project_issue_path(project, { add_related_issue: incident.iid })
expect(page).to have_selector '#add_related_issue'
expect(page).to have_text "Relate to incident \##{incident.iid}"
expect(page).to have_text 'Adds this incident as related to the incident it was created from'
fill_in 'issue_title', with: 'title'
click_button 'Create issue'
page.within '#related-issues' do
expect(page).to have_text "\##{incident.iid}"
end
end
it 'does not link the new issue to any other issues if the checkbox is not checked' do
visit new_project_issue_path(project, { add_related_issue: issue.iid })
expect(page).to have_selector '#add_related_issue'
expect(page).to have_text "Relate to issue \##{issue.iid}"
uncheck "Relate to issue \##{issue.iid}"
fill_in 'issue_title', with: 'title'
click_button 'Create issue'
page.within '#related-issues' do
expect(page).not_to have_text "\##{issue.iid}"
end
end
end
describe 'edit issue' do
before do
visit edit_project_issue_path(project, issue)
......
......@@ -25,8 +25,8 @@ RSpec.describe 'issue header', :js do
click_button 'Issue actions'
end
it 'shows the "New issue", "Report abuse", and "Delete issue" items', :aggregate_failures do
expect(page).to have_link 'New issue'
it 'shows the "New related issue", "Report abuse", and "Delete issue" items', :aggregate_failures do
expect(page).to have_link 'New related issue'
expect(page).to have_link 'Report abuse'
expect(page).to have_button 'Delete issue'
expect(page).not_to have_link 'Submit as spam'
......@@ -114,8 +114,8 @@ RSpec.describe 'issue header', :js do
click_button 'Issue actions'
end
it 'only shows the "New issue" and "Report abuse" items', :aggregate_failures do
expect(page).to have_link 'New issue'
it 'only shows the "New related issue" and "Report abuse" items', :aggregate_failures do
expect(page).to have_link 'New related issue'
expect(page).to have_link 'Report abuse'
expect(page).not_to have_link 'Submit as spam'
expect(page).not_to have_button 'Delete issue'
......
......@@ -25,7 +25,7 @@ RSpec.describe "User views issue" do
it 'shows the merge request and issue actions', :js, :aggregate_failures do
click_button 'Issue actions'
expect(page).to have_link('New issue', href: new_project_issue_path(project, { issue: { description: "Related to \##{issue.iid}.\n\n" } }))
expect(page).to have_link('New related issue', href: new_project_issue_path(project, { add_related_issue: issue.iid }))
expect(page).to have_button('Create merge request')
expect(page).to have_button('Close issue')
end
......
......@@ -166,19 +166,19 @@ describe('HeaderActions component', () => {
${'desktop dropdown'} | ${false} | ${findDesktopDropdownItems}
`('$description', ({ isCloseIssueItemVisible, findDropdownItems }) => {
describe.each`
description | itemText | isItemVisible | canUpdateIssue | canCreateIssue | isIssueAuthor | canReportSpam | canPromoteToEpic | canDestroyIssue
${`when user can update ${issueType}`} | ${`Close ${issueType}`} | ${isCloseIssueItemVisible} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${`when user cannot update ${issueType}`} | ${`Close ${issueType}`} | ${false} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true}
${`when user can create ${issueType}`} | ${`New ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${`when user cannot create ${issueType}`} | ${`New ${issueType}`} | ${false} | ${true} | ${false} | ${true} | ${true} | ${true} | ${true}
${'when user can promote to epic'} | ${'Promote to epic'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${'when user cannot promote to epic'} | ${'Promote to epic'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${false} | ${true}
${'when user can report abuse'} | ${'Report abuse'} | ${true} | ${true} | ${true} | ${false} | ${true} | ${true} | ${true}
${'when user cannot report abuse'} | ${'Report abuse'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${'when user can submit as spam'} | ${'Submit as spam'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${'when user cannot submit as spam'} | ${'Submit as spam'} | ${false} | ${true} | ${true} | ${true} | ${false} | ${true} | ${true}
${`when user can delete ${issueType}`} | ${`Delete ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${`when user cannot delete ${issueType}`} | ${`Delete ${issueType}`} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true} | ${false}
description | itemText | isItemVisible | canUpdateIssue | canCreateIssue | isIssueAuthor | canReportSpam | canPromoteToEpic | canDestroyIssue
${`when user can update ${issueType}`} | ${`Close ${issueType}`} | ${isCloseIssueItemVisible} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${`when user cannot update ${issueType}`} | ${`Close ${issueType}`} | ${false} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true}
${`when user can create ${issueType}`} | ${`New related ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${`when user cannot create ${issueType}`} | ${`New related ${issueType}`} | ${false} | ${true} | ${false} | ${true} | ${true} | ${true} | ${true}
${'when user can promote to epic'} | ${'Promote to epic'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${'when user cannot promote to epic'} | ${'Promote to epic'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${false} | ${true}
${'when user can report abuse'} | ${'Report abuse'} | ${true} | ${true} | ${true} | ${false} | ${true} | ${true} | ${true}
${'when user cannot report abuse'} | ${'Report abuse'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${'when user can submit as spam'} | ${'Submit as spam'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${'when user cannot submit as spam'} | ${'Submit as spam'} | ${false} | ${true} | ${true} | ${true} | ${false} | ${true} | ${true}
${`when user can delete ${issueType}`} | ${`Delete ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${`when user cannot delete ${issueType}`} | ${`Delete ${issueType}`} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true} | ${false}
`(
'$description',
({
......
......@@ -265,7 +265,7 @@ RSpec.describe IssuesHelper do
is_issue_author: 'false',
issue_path: issue_path(issue),
issue_type: 'issue',
new_issue_path: new_project_issue_path(project, { issue: { description: "Related to \##{issue.iid}.\n\n" } }),
new_issue_path: new_project_issue_path(project, { add_related_issue: issue.iid }),
project_path: project.full_path,
report_abuse_path: new_abuse_report_path(user_id: issue.author.id, ref_url: issue_url(issue)),
submit_as_spam_path: mark_as_spam_project_issue_path(project, issue)
......
......@@ -526,6 +526,31 @@ RSpec.describe Issues::CreateService do
end
end
context 'add related issue' do
let_it_be(:related_issue) { create(:issue, project: project) }
let(:opts) do
{ title: 'A new issue', add_related_issue: related_issue }
end
it 'ignores related issue if not accessible' do
expect { issue }.not_to change { IssueLink.count }
expect(issue).to be_persisted
end
context 'when user has access to the related issue' do
before do
project.add_developer(user)
end
it 'adds a link to the issue' do
expect { issue }.to change { IssueLink.count }.by(1)
expect(issue).to be_persisted
expect(issue.related_issues(user)).to eq([related_issue])
end
end
end
context 'checking spam' do
let(:params) do
{
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment