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) {
|
||||
if (this.isLoadingData[at]) return;
|
||||
|
||||
this.isLoadingData[at] = true;
|
||||
const dataSource = this.dataSources[GfmAutoComplete.atTypeMap[at]];
|
||||
|
||||
if (this.cachedData[at]) {
|
||||
this.loadData($input, at, this.cachedData[at]);
|
||||
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
|
||||
|
@ -418,12 +421,14 @@ class GfmAutoComplete {
|
|||
GfmAutoComplete.glEmojiTag = glEmojiTag;
|
||||
})
|
||||
.catch(() => { this.isLoadingData[at] = false; });
|
||||
} else {
|
||||
AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true)
|
||||
} else if (dataSource) {
|
||||
AjaxCache.retrieve(dataSource, true)
|
||||
.then((data) => {
|
||||
this.loadData($input, at, data);
|
||||
})
|
||||
.catch(() => { this.isLoadingData[at] = false; });
|
||||
} else {
|
||||
this.isLoadingData[at] = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -99,10 +99,6 @@ export default {
|
|||
'js-note-target-reopen': !this.isOpen,
|
||||
};
|
||||
},
|
||||
supportQuickActions() {
|
||||
// Disable quick actions support for Epics
|
||||
return this.noteableType !== constants.EPIC_NOTEABLE_TYPE;
|
||||
},
|
||||
markdownDocsPath() {
|
||||
return this.getNotesData.markdownDocsPath;
|
||||
},
|
||||
|
@ -359,7 +355,7 @@ Please check your network connection and try again.`;
|
|||
name="note[note]"
|
||||
class="note-textarea js-vue-comment-form
|
||||
js-gfm-input js-autosize markdown-area js-vue-textarea"
|
||||
:data-supports-quick-actions="supportQuickActions"
|
||||
data-supports-quick-actions="true"
|
||||
aria-label="Description"
|
||||
v-model="note"
|
||||
ref="textarea"
|
||||
|
|
|
@ -258,4 +258,17 @@ module ApplicationHelper
|
|||
|
||||
_('You are on a read-only GitLab instance.')
|
||||
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
|
||||
|
|
|
@ -10,6 +10,14 @@ class Ability
|
|||
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
|
||||
# read the given snippet.
|
||||
def users_that_can_read_personal_snippet(users, snippet)
|
||||
|
|
|
@ -98,6 +98,10 @@ module Participable
|
|||
|
||||
participants.merge(ext.users)
|
||||
|
||||
filter_by_ability(participants)
|
||||
end
|
||||
|
||||
def filter_by_ability(participants)
|
||||
case self
|
||||
when PersonalSnippet
|
||||
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))
|
||||
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
|
||||
User
|
||||
.where(id: members_with_parents.select(:user_id))
|
||||
|
@ -253,6 +260,30 @@ class Group < Namespace
|
|||
.reorder(nil)
|
||||
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)
|
||||
return GroupMember::OWNER if user.admin?
|
||||
|
||||
|
|
|
@ -166,6 +166,13 @@ class Namespace < ActiveRecord::Base
|
|||
projects.with_shared_runners.any?
|
||||
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.
|
||||
def ancestors
|
||||
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
|
||||
class ParticipantsService < BaseService
|
||||
attr_reader :noteable
|
||||
include Users::ParticipableService
|
||||
|
||||
def execute(noteable)
|
||||
@noteable = noteable
|
||||
|
@ -10,36 +10,6 @@ module Projects
|
|||
participants.uniq
|
||||
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
|
||||
count = project.team.members.flatten.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?
|
||||
|
||||
- if project
|
||||
- datasources = autocomplete_data_sources(object, noteable_type)
|
||||
|
||||
- if object
|
||||
-# haml-lint:disable InlineJavaScript
|
||||
:javascript
|
||||
gl = window.gl || {};
|
||||
gl.GfmAutoComplete = gl.GfmAutoComplete || {};
|
||||
gl.GfmAutoComplete.dataSources = {
|
||||
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])}"
|
||||
};
|
||||
gl.GfmAutoComplete.dataSources = #{datasources.to_json};
|
||||
|
|
|
@ -151,4 +151,16 @@ describe ApplicationHelper do
|
|||
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
|
||||
|
|
|
@ -424,6 +424,95 @@ describe Group do
|
|||
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
|
||||
it 'returns the user IDs for which to refresh authorizations' do
|
||||
master = create(:user)
|
||||
|
|
|
@ -399,6 +399,21 @@ describe Namespace do
|
|||
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
|
||||
let(:group) { create(:group) }
|
||||
let(:nested_group) { create(:group, parent: group) }
|
||||
|
|
Loading…
Reference in a new issue