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:
commit
67c9f822dd
13 changed files with 234 additions and 48 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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?
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
41
app/services/concerns/users/participable_service.rb
Normal file
41
app/services/concerns/users/participable_service.rb
Normal 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
|
|
@ -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 }]
|
||||||
|
|
|
@ -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])}"
|
|
||||||
};
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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) }
|
||||||
|
|
Loading…
Reference in a new issue