Merge branch '4084-epics-username-autocomplete-ce' into 'master'

Backport CE changes from "autocomplete usernames in Epic comments/description"

See merge request gitlab-org/gitlab-ce!18605
This commit is contained in:
Douwe Maan 2018-05-07 08:26:44 +00:00
commit 67c9f822dd
13 changed files with 234 additions and 48 deletions

View file

@ -408,7 +408,10 @@ class GfmAutoComplete {
fetchData($input, at) { fetchData($input, at) {
if (this.isLoadingData[at]) return; if (this.isLoadingData[at]) return;
this.isLoadingData[at] = true; this.isLoadingData[at] = true;
const dataSource = this.dataSources[GfmAutoComplete.atTypeMap[at]];
if (this.cachedData[at]) { if (this.cachedData[at]) {
this.loadData($input, at, this.cachedData[at]); this.loadData($input, at, this.cachedData[at]);
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') { } else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
@ -418,12 +421,14 @@ class GfmAutoComplete {
GfmAutoComplete.glEmojiTag = glEmojiTag; GfmAutoComplete.glEmojiTag = glEmojiTag;
}) })
.catch(() => { this.isLoadingData[at] = false; }); .catch(() => { this.isLoadingData[at] = false; });
} else { } else if (dataSource) {
AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true) AjaxCache.retrieve(dataSource, true)
.then((data) => { .then((data) => {
this.loadData($input, at, data); this.loadData($input, at, data);
}) })
.catch(() => { this.isLoadingData[at] = false; }); .catch(() => { this.isLoadingData[at] = false; });
} else {
this.isLoadingData[at] = false;
} }
} }

View file

@ -99,10 +99,6 @@ export default {
'js-note-target-reopen': !this.isOpen, 'js-note-target-reopen': !this.isOpen,
}; };
}, },
supportQuickActions() {
// Disable quick actions support for Epics
return this.noteableType !== constants.EPIC_NOTEABLE_TYPE;
},
markdownDocsPath() { markdownDocsPath() {
return this.getNotesData.markdownDocsPath; return this.getNotesData.markdownDocsPath;
}, },
@ -359,7 +355,7 @@ Please check your network connection and try again.`;
name="note[note]" name="note[note]"
class="note-textarea js-vue-comment-form class="note-textarea js-vue-comment-form
js-gfm-input js-autosize markdown-area js-vue-textarea" js-gfm-input js-autosize markdown-area js-vue-textarea"
:data-supports-quick-actions="supportQuickActions" data-supports-quick-actions="true"
aria-label="Description" aria-label="Description"
v-model="note" v-model="note"
ref="textarea" ref="textarea"

View file

@ -258,4 +258,17 @@ module ApplicationHelper
_('You are on a read-only GitLab instance.') _('You are on a read-only GitLab instance.')
end end
def autocomplete_data_sources(object, noteable_type)
return {} unless object && noteable_type
{
members: members_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
issues: issues_project_autocomplete_sources_path(object),
merge_requests: merge_requests_project_autocomplete_sources_path(object),
labels: labels_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
milestones: milestones_project_autocomplete_sources_path(object),
commands: commands_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id])
}
end
end end

View file

@ -10,6 +10,14 @@ class Ability
end end
end end
# Given a list of users and a group this method returns the users that can
# read the given group.
def users_that_can_read_group(users, group)
DeclarativePolicy.subject_scope do
users.select { |u| allowed?(u, :read_group, group) }
end
end
# Given a list of users and a snippet this method returns the users that can # Given a list of users and a snippet this method returns the users that can
# read the given snippet. # read the given snippet.
def users_that_can_read_personal_snippet(users, snippet) def users_that_can_read_personal_snippet(users, snippet)

View file

@ -98,6 +98,10 @@ module Participable
participants.merge(ext.users) participants.merge(ext.users)
filter_by_ability(participants)
end
def filter_by_ability(participants)
case self case self
when PersonalSnippet when PersonalSnippet
Ability.users_that_can_read_personal_snippet(participants.to_a, self) Ability.users_that_can_read_personal_snippet(participants.to_a, self)

View file

@ -241,6 +241,13 @@ class Group < Namespace
.where(source_id: self_and_descendants.reorder(nil).select(:id)) .where(source_id: self_and_descendants.reorder(nil).select(:id))
end end
# Returns all members that are part of the group, it's subgroups, and ancestor groups
def direct_and_indirect_members
GroupMember
.active_without_invites_and_requests
.where(source_id: self_and_hierarchy.reorder(nil).select(:id))
end
def users_with_parents def users_with_parents
User User
.where(id: members_with_parents.select(:user_id)) .where(id: members_with_parents.select(:user_id))
@ -253,6 +260,30 @@ class Group < Namespace
.reorder(nil) .reorder(nil)
end end
# Returns all users that are members of the group because:
# 1. They belong to the group
# 2. They belong to a project that belongs to the group
# 3. They belong to a sub-group or project in such sub-group
# 4. They belong to an ancestor group
def direct_and_indirect_users
union = Gitlab::SQL::Union.new([
User
.where(id: direct_and_indirect_members.select(:user_id))
.reorder(nil),
project_users_with_descendants
])
User.from("(#{union.to_sql}) #{User.table_name}")
end
# Returns all users that are members of projects
# belonging to the current group or sub-groups
def project_users_with_descendants
User
.joins(projects: :group)
.where(namespaces: { id: self_and_descendants.select(:id) })
end
def max_member_access_for_user(user) def max_member_access_for_user(user)
return GroupMember::OWNER if user.admin? return GroupMember::OWNER if user.admin?

View file

@ -166,6 +166,13 @@ class Namespace < ActiveRecord::Base
projects.with_shared_runners.any? projects.with_shared_runners.any?
end end
# Returns all ancestors, self, and descendants of the current namespace.
def self_and_hierarchy
Gitlab::GroupHierarchy
.new(self.class.where(id: id))
.all_groups
end
# Returns all the ancestors of the current namespaces. # Returns all the ancestors of the current namespaces.
def ancestors def ancestors
return self.class.none unless parent_id return self.class.none unless parent_id

View file

@ -0,0 +1,41 @@
module Users
module ParticipableService
extend ActiveSupport::Concern
included do
attr_reader :noteable
end
def noteable_owner
return [] unless noteable && noteable.author.present?
[as_hash(noteable.author)]
end
def participants_in_noteable
return [] unless noteable
users = noteable.participants(current_user)
sorted(users)
end
def sorted(users)
users.uniq.to_a.compact.sort_by(&:username).map do |user|
as_hash(user)
end
end
def groups
current_user.authorized_groups.sort_by(&:path).map do |group|
count = group.users.count
{ username: group.full_path, name: group.full_name, count: count, avatar_url: group.avatar_url }
end
end
private
def as_hash(user)
{ username: user.username, name: user.name, avatar_url: user.avatar_url }
end
end
end

View file

@ -1,6 +1,6 @@
module Projects module Projects
class ParticipantsService < BaseService class ParticipantsService < BaseService
attr_reader :noteable include Users::ParticipableService
def execute(noteable) def execute(noteable)
@noteable = noteable @noteable = noteable
@ -10,36 +10,6 @@ module Projects
participants.uniq participants.uniq
end end
def noteable_owner
return [] unless noteable && noteable.author.present?
[{
name: noteable.author.name,
username: noteable.author.username,
avatar_url: noteable.author.avatar_url
}]
end
def participants_in_noteable
return [] unless noteable
users = noteable.participants(current_user)
sorted(users)
end
def sorted(users)
users.uniq.to_a.compact.sort_by(&:username).map do |user|
{ username: user.username, name: user.name, avatar_url: user.avatar_url }
end
end
def groups
current_user.authorized_groups.sort_by(&:path).map do |group|
count = group.users.count
{ username: group.full_path, name: group.full_name, count: count, avatar_url: group.avatar_url }
end
end
def all_members def all_members
count = project.team.members.flatten.count count = project.team.members.flatten.count
[{ username: "all", name: "All Project and Group Members", count: count }] [{ username: "all", name: "All Project and Group Members", count: count }]

View file

@ -1,16 +1,11 @@
- project = @target_project || @project - object = @target_project || @project || @group
- noteable_type = @noteable.class if @noteable.present? - noteable_type = @noteable.class if @noteable.present?
- if project - datasources = autocomplete_data_sources(object, noteable_type)
- if object
-# haml-lint:disable InlineJavaScript -# haml-lint:disable InlineJavaScript
:javascript :javascript
gl = window.gl || {}; gl = window.gl || {};
gl.GfmAutoComplete = gl.GfmAutoComplete || {}; gl.GfmAutoComplete = gl.GfmAutoComplete || {};
gl.GfmAutoComplete.dataSources = { gl.GfmAutoComplete.dataSources = #{datasources.to_json};
members: "#{members_project_autocomplete_sources_path(project, type: noteable_type, type_id: params[:id])}",
issues: "#{issues_project_autocomplete_sources_path(project)}",
mergeRequests: "#{merge_requests_project_autocomplete_sources_path(project)}",
labels: "#{labels_project_autocomplete_sources_path(project, type: noteable_type, type_id: params[:id])}",
milestones: "#{milestones_project_autocomplete_sources_path(project)}",
commands: "#{commands_project_autocomplete_sources_path(project, type: noteable_type, type_id: params[:id])}"
};

View file

@ -151,4 +151,16 @@ describe ApplicationHelper do
end end
end end
end end
describe '#autocomplete_data_sources' do
let(:project) { create(:project) }
let(:noteable_type) { Issue }
it 'returns paths for autocomplete_sources_controller' do
sources = helper.autocomplete_data_sources(project, noteable_type)
expect(sources.keys).to match_array([:members, :issues, :merge_requests, :labels, :milestones, :commands])
sources.keys.each do |key|
expect(sources[key]).not_to be_nil
end
end
end
end end

View file

@ -424,6 +424,95 @@ describe Group do
end end
end end
describe '#direct_and_indirect_members', :nested_groups do
let!(:group) { create(:group, :nested) }
let!(:sub_group) { create(:group, parent: group) }
let!(:master) { group.parent.add_user(create(:user), GroupMember::MASTER) }
let!(:developer) { group.add_user(create(:user), GroupMember::DEVELOPER) }
let!(:other_developer) { group.add_user(create(:user), GroupMember::DEVELOPER) }
it 'returns parents members' do
expect(group.direct_and_indirect_members).to include(developer)
expect(group.direct_and_indirect_members).to include(master)
end
it 'returns descendant members' do
expect(group.direct_and_indirect_members).to include(other_developer)
end
end
describe '#users_with_descendants', :nested_groups do
let(:user_a) { create(:user) }
let(:user_b) { create(:user) }
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
let(:deep_nested_group) { create(:group, parent: nested_group) }
it 'returns member users on every nest level without duplication' do
group.add_developer(user_a)
nested_group.add_developer(user_b)
deep_nested_group.add_developer(user_a)
expect(group.users_with_descendants).to contain_exactly(user_a, user_b)
expect(nested_group.users_with_descendants).to contain_exactly(user_a, user_b)
expect(deep_nested_group.users_with_descendants).to contain_exactly(user_a)
end
end
describe '#direct_and_indirect_users', :nested_groups do
let(:user_a) { create(:user) }
let(:user_b) { create(:user) }
let(:user_c) { create(:user) }
let(:user_d) { create(:user) }
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
let(:deep_nested_group) { create(:group, parent: nested_group) }
let(:project) { create(:project, namespace: group) }
before do
group.add_developer(user_a)
group.add_developer(user_c)
nested_group.add_developer(user_b)
deep_nested_group.add_developer(user_a)
project.add_developer(user_d)
end
it 'returns member users on every nest level without duplication' do
expect(group.direct_and_indirect_users).to contain_exactly(user_a, user_b, user_c, user_d)
expect(nested_group.direct_and_indirect_users).to contain_exactly(user_a, user_b, user_c)
expect(deep_nested_group.direct_and_indirect_users).to contain_exactly(user_a, user_b, user_c)
end
it 'does not return members of projects belonging to ancestor groups' do
expect(nested_group.direct_and_indirect_users).not_to include(user_d)
end
end
describe '#project_users_with_descendants', :nested_groups do
let(:user_a) { create(:user) }
let(:user_b) { create(:user) }
let(:user_c) { create(:user) }
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
let(:deep_nested_group) { create(:group, parent: nested_group) }
let(:project_a) { create(:project, namespace: group) }
let(:project_b) { create(:project, namespace: nested_group) }
let(:project_c) { create(:project, namespace: deep_nested_group) }
it 'returns members of all projects in group and subgroups' do
project_a.add_developer(user_a)
project_b.add_developer(user_b)
project_c.add_developer(user_c)
expect(group.project_users_with_descendants).to contain_exactly(user_a, user_b, user_c)
expect(nested_group.project_users_with_descendants).to contain_exactly(user_b, user_c)
expect(deep_nested_group.project_users_with_descendants).to contain_exactly(user_c)
end
end
describe '#user_ids_for_project_authorizations' do describe '#user_ids_for_project_authorizations' do
it 'returns the user IDs for which to refresh authorizations' do it 'returns the user IDs for which to refresh authorizations' do
master = create(:user) master = create(:user)

View file

@ -399,6 +399,21 @@ describe Namespace do
end end
end end
describe '#self_and_hierarchy', :nested_groups do
let!(:group) { create(:group, path: 'git_lab') }
let!(:nested_group) { create(:group, parent: group) }
let!(:deep_nested_group) { create(:group, parent: nested_group) }
let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
let!(:another_group) { create(:group, path: 'gitllab') }
let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) }
it 'returns the correct tree' do
expect(group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
expect(nested_group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
expect(very_deep_nested_group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
end
end
describe '#ancestors', :nested_groups do describe '#ancestors', :nested_groups do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) } let(:nested_group) { create(:group, parent: group) }