Commit b61147ee authored by Robert Speicher's avatar Robert Speicher

Merge branch '5606-issues-and-mr-autocomplete-in-epics' into 'master'

Add autocomplete for issues and MRs in epics

Closes #5606

See merge request gitlab-org/gitlab-ee!8936
parents e68f6665 abf448fb
......@@ -228,13 +228,13 @@ class GfmAutoComplete {
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
if (value.title != null) {
tmpl = GfmAutoComplete.Issues.templateFunction(value.id, value.title);
tmpl = GfmAutoComplete.Issues.templateFunction(value);
}
return tmpl;
},
data: GfmAutoComplete.defaultLoadingData,
// eslint-disable-next-line no-template-curly-in-string
insertTpl: '${atwho-at}${id}',
insertTpl: GfmAutoComplete.Issues.insertTemplateFunction,
skipSpecialCharacterTest: true,
callbacks: {
...this.getDefaultCallbacks(),
beforeSave(issues) {
......@@ -245,6 +245,7 @@ class GfmAutoComplete {
return {
id: i.iid,
title: sanitize(i.title),
reference: i.reference,
search: `${i.iid} ${i.title}`,
};
});
......@@ -294,13 +295,13 @@ class GfmAutoComplete {
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
if (value.title != null) {
tmpl = GfmAutoComplete.Issues.templateFunction(value.id, value.title);
tmpl = GfmAutoComplete.Issues.templateFunction(value);
}
return tmpl;
},
data: GfmAutoComplete.defaultLoadingData,
// eslint-disable-next-line no-template-curly-in-string
insertTpl: '${atwho-at}${id}',
insertTpl: GfmAutoComplete.Issues.insertTemplateFunction,
skipSpecialCharacterTest: true,
callbacks: {
...this.getDefaultCallbacks(),
beforeSave(merges) {
......@@ -311,6 +312,7 @@ class GfmAutoComplete {
return {
id: m.iid,
title: sanitize(m.title),
reference: m.reference,
search: `${m.iid} ${m.title}`,
};
});
......@@ -404,7 +406,7 @@ class GfmAutoComplete {
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
if (value.title != null) {
tmpl = GfmAutoComplete.Issues.templateFunction(value.id, value.title);
tmpl = GfmAutoComplete.Issues.templateFunction(value);
}
return tmpl;
},
......@@ -603,8 +605,12 @@ GfmAutoComplete.Labels = {
};
// Issues, MergeRequests and Snippets
GfmAutoComplete.Issues = {
templateFunction(id, title) {
return `<li><small>${id}</small> ${_.escape(title)}</li>`;
insertTemplateFunction(value) {
// eslint-disable-next-line no-template-curly-in-string
return value.reference || '${atwho-at}${id}';
},
templateFunction({ id, title, reference }) {
return `<li><small>${reference || id}</small> ${_.escape(title)}</li>`;
},
};
// Milestones
......
......@@ -9,7 +9,7 @@ const setupAutoCompleteEpics = ($input, defaultCallbacks) => {
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
if (value.title != null) {
tmpl = GfmAutoComplete.Issues.templateFunction(value.id, value.title);
tmpl = GfmAutoComplete.Issues.templateFunction(value);
}
return tmpl;
},
......
......@@ -7,6 +7,14 @@ class Groups::AutocompleteSourcesController < Groups::ApplicationController
render json: ::Groups::ParticipantsService.new(@group, current_user).execute(target)
end
def issues
render json: issuable_serializer.represent(@autocomplete_service.issues, parent_group: @group)
end
def merge_requests
render json: issuable_serializer.represent(@autocomplete_service.merge_requests, parent_group: @group)
end
def labels
render json: @autocomplete_service.labels_as_hash(target)
end
......@@ -29,6 +37,10 @@ class Groups::AutocompleteSourcesController < Groups::ApplicationController
@autocomplete_service = ::Groups::AutocompleteService.new(@group, current_user)
end
def issuable_serializer
GroupIssuableAutocompleteSerializer.new
end
# rubocop: disable CodeReuse/ActiveRecord
def target
case params[:type]&.downcase
......
......@@ -75,6 +75,8 @@ module EE
{
members: members_group_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
labels: labels_group_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
issues: issues_group_autocomplete_sources_path(object),
mergeRequests: merge_requests_group_autocomplete_sources_path(object),
epics: epics_group_autocomplete_sources_path(object),
commands: commands_group_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
milestones: milestones_group_autocomplete_sources_path(object)
......
# frozen_string_literal: true
class GroupIssuableAutocompleteEntity < Grape::Entity
expose :iid
expose :title
expose :reference do |issuable, options|
issuable.to_reference(options[:parent_group])
end
end
# frozen_string_literal: true
class GroupIssuableAutocompleteSerializer < BaseSerializer
entity GroupIssuableAutocompleteEntity
end
......@@ -3,12 +3,31 @@
module Groups
class AutocompleteService < Groups::BaseService
include LabelsAsHash
# rubocop: disable CodeReuse/ActiveRecord
def issues
IssuesFinder.new(current_user, group_id: group.id, include_subgroups: true, state: 'opened')
.execute
.preload(project: :namespace)
.select(:iid, :title, :project_id)
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def merge_requests
MergeRequestsFinder.new(current_user, group_id: group.id, include_subgroups: true, state: 'opened')
.execute
.preload(target_project: :namespace)
.select(:iid, :title, :target_project_id)
end
# rubocop: enable CodeReuse/ActiveRecord
def epics
# TODO: change to EpicsFinder once frontend supports epics from external groups.
# See https://gitlab.com/gitlab-org/gitlab-ee/issues/6837
DeclarativePolicy.user_scope do
if Ability.allowed?(current_user, :read_epic, group)
group.epics
group.epics.select(:iid, :title)
else
[]
end
......@@ -19,7 +38,7 @@ module Groups
group_ids =
ParentGroupsFinder.new(current_user, group).execute.map(&:id)
MilestonesFinder.new(group_ids: group_ids).execute
MilestonesFinder.new(group_ids: group_ids).execute.select(:iid, :title)
end
def labels_as_hash(target)
......
---
title: Autocomplete issues and MRs in epics
merge_request: 8936
author:
type: added
......@@ -41,6 +41,8 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
resources :autocomplete_sources, only: [] do
collection do
get 'members'
get 'issues'
get 'merge_requests'
get 'labels'
get 'epics'
get 'commands'
......
......@@ -25,7 +25,7 @@ describe Groups::AutocompleteSourcesController do
expect(json_response).to be_an(Array)
expect(json_response.first).to include(
'id' => epic.id, 'iid' => epic.iid, 'title' => epic.title
'iid' => epic.iid, 'title' => epic.title
)
end
end
......@@ -43,9 +43,8 @@ describe Groups::AutocompleteSourcesController do
expect(response).to have_gitlab_http_status(200)
expect(json_response.count).to eq(1)
expect(response).to match_response_schema('public_api/v4/milestones')
expect(json_response.first).to include(
'id' => group_milestone.id, 'iid' => group_milestone.iid, 'title' => group_milestone.title
'iid' => group_milestone.iid, 'title' => group_milestone.title
)
end
end
......
......@@ -15,6 +15,32 @@ describe 'GFM autocomplete', :js do
wait_for_requests
end
context 'issuables' do
let(:project) { create(:project, :repository, namespace: group) }
context 'issues' do
it 'shows issues of group' do
issue_1 = create(:issue, project: project)
issue_2 = create(:issue, project: project)
type(find('#note-body'), '#')
expect_resources(shown: [issue_1, issue_2])
end
end
context 'merge requests' do
it 'shows merge requests of group' do
mr_1 = create(:merge_request, source_project: project)
mr_2 = create(:merge_request, source_project: project, source_branch: 'other-branch')
type(find('#note-body'), '!')
expect_resources(shown: [mr_1, mr_2])
end
end
end
context 'epics' do
let!(:epic2) { create(:epic, group: group, title: 'make tea') }
......
......@@ -62,7 +62,7 @@ describe ApplicationHelper do
let(:noteable_type) { Epic }
it 'returns paths for autocomplete_sources_controller' do
expect_autocomplete_data_sources(object, noteable_type, [:members, :labels, :epics, :commands, :milestones])
expect_autocomplete_data_sources(object, noteable_type, [:members, :issues, :mergeRequests, :labels, :epics, :commands, :milestones])
end
end
......
# frozen_string_literal: true
require 'spec_helper'
describe GroupIssuableAutocompleteEntity do
let(:group) { build_stubbed(:group) }
let(:project) { build_stubbed(:project, group: group) }
let(:issue) { build_stubbed(:issue, project: project) }
subject { described_class.new(issue, parent_group: group).as_json }
describe '#represent' do
it 'includes the iid, title, and reference' do
expect(subject).to include(:iid, :title, :reference)
end
end
end
......@@ -54,6 +54,32 @@ describe Groups::AutocompleteService do
end
end
describe '#issues', :nested_groups do
let(:project) { create(:project, group: group) }
let(:sub_group_project) { create(:project, group: sub_group) }
let!(:project_issue) { create(:issue, project: project) }
let!(:sub_group_project_issue) { create(:issue, project: sub_group_project) }
it 'returns issues in group and subgroups' do
expect(subject.issues.map(&:iid)).to contain_exactly(project_issue.iid, sub_group_project_issue.iid)
expect(subject.issues.map(&:title)).to contain_exactly(project_issue.title, sub_group_project_issue.title)
end
end
describe '#merge_requests', :nested_groups do
let(:project) { create(:project, :repository, group: group) }
let(:sub_group_project) { create(:project, :repository, group: sub_group) }
let!(:project_mr) { create(:merge_request, source_project: project) }
let!(:sub_group_project_mr) { create(:merge_request, source_project: sub_group_project) }
it 'returns merge requests in group and subgroups' do
expect(subject.merge_requests.map(&:iid)).to contain_exactly(project_mr.iid, sub_group_project_mr.iid)
expect(subject.merge_requests.map(&:title)).to contain_exactly(project_mr.title, sub_group_project_mr.title)
end
end
describe '#epics' do
it 'returns nothing if not allowed' do
allow(Ability).to receive(:allowed?).with(user, :read_epic, group).and_return(false)
......@@ -64,7 +90,7 @@ describe Groups::AutocompleteService do
it 'returns epics from group' do
allow(Ability).to receive(:allowed?).with(user, :read_epic, group).and_return(true)
expect(subject.epics).to contain_exactly(epic)
expect(subject.epics.map(&:iid)).to contain_exactly(epic.iid)
end
end
......@@ -92,28 +118,35 @@ describe Groups::AutocompleteService do
end
context 'when group is public' do
it 'returns milestones from groups', :nested_groups do
group = create(:group, :public)
subgroup = create(:group, :public, parent: group)
group_milestone = create(:milestone, group: group)
subgroup_milestone = create(:milestone, group: subgroup)
subgroup.add_guest(user)
group.add_guest(user)
let(:public_group) { create(:group, :public) }
let(:public_subgroup) { create(:group, :public, parent: public_group) }
before do
public_subgroup.add_guest(user)
public_group.add_guest(user)
group_milestone.update(group: public_group)
subgroup_milestone.update(group: public_subgroup)
end
subject = described_class.new(subgroup, user)
it 'returns milestones from groups and subgroups', :nested_groups do
subject = described_class.new(public_subgroup, user)
expect(subject.milestones).to match_array([group_milestone, subgroup_milestone])
expect(subject.milestones.map(&:iid)).to contain_exactly(group_milestone.iid, subgroup_milestone.iid)
expect(subject.milestones.map(&:title)).to contain_exactly(group_milestone.title, subgroup_milestone.title)
end
end
it 'returns milestones from group' do
expect(subject.milestones).to include(group_milestone)
expect(subject.milestones.map(&:iid)).to contain_exactly(group_milestone.iid)
expect(subject.milestones.map(&:title)).to contain_exactly(group_milestone.title)
end
it 'returns milestones from groups and subgroups', :nested_groups do
milestones = described_class.new(sub_group, user).milestones
expect(milestones).to include(group_milestone, subgroup_milestone)
expect(milestones.map(&:iid)).to contain_exactly(group_milestone.iid, subgroup_milestone.iid)
expect(milestones.map(&:title)).to contain_exactly(group_milestone.title, subgroup_milestone.title)
end
it 'returns only milestones that user can read', :nested_groups do
......@@ -122,7 +155,8 @@ describe Groups::AutocompleteService do
milestones = described_class.new(sub_group, user).milestones
expect(milestones).to match_array([subgroup_milestone])
expect(milestones.map(&:iid)).to contain_exactly(subgroup_milestone.iid)
expect(milestones.map(&:title)).to contain_exactly(subgroup_milestone.title)
end
end
end
......@@ -205,4 +205,40 @@ describe('GfmAutoComplete', function() {
expect(GfmAutoComplete.isLoading({ title: 'Foo' })).toBe(false);
});
});
describe('Issues.insertTemplateFunction', function() {
it('should return default template', function() {
expect(GfmAutoComplete.Issues.insertTemplateFunction({ id: 5, title: 'Some Issue' })).toBe(
'${atwho-at}${id}', // eslint-disable-line no-template-curly-in-string
);
});
it('should return reference when reference is set', function() {
expect(
GfmAutoComplete.Issues.insertTemplateFunction({
id: 5,
title: 'Some Issue',
reference: 'grp/proj#5',
}),
).toBe('grp/proj#5');
});
});
describe('Issues.templateFunction', function() {
it('should return html with id and title', function() {
expect(GfmAutoComplete.Issues.templateFunction({ id: 5, title: 'Some Issue' })).toBe(
'<li><small>5</small> Some Issue</li>',
);
});
it('should replace id with reference if reference is set', function() {
expect(
GfmAutoComplete.Issues.templateFunction({
id: 5,
title: 'Some Issue',
reference: 'grp/proj#5',
}),
).toBe('<li><small>grp/proj#5</small> Some Issue</li>');
});
});
});
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