Commit 7aab0e5c authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Add autocomplete for issues and MRs in epics

References with namespace are inserted because this is at group scope
and iids are not unique
parent 8125a1a4
......@@ -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)
......
......@@ -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)
subject = described_class.new(subgroup, user)
group_milestone.update(group: public_group)
subgroup_milestone.update(group: public_subgroup)
end
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