Skip to content
Snippets Groups Projects
Commit 0558a809 authored by Sean McGivern's avatar Sean McGivern
Browse files

Merge branch 'feat/group-mentionings-prevention' into 'master'

Implement disable group mentions for members

Closes #21301

See merge request gitlab-org/gitlab!20184
parents 32d804d3 9fb99aef
No related branches found
No related tags found
No related merge requests found
Loading
Loading
@@ -3,11 +3,44 @@ import 'at.js';
import _ from 'underscore';
import glRegexp from './lib/utils/regexp';
import AjaxCache from './lib/utils/ajax_cache';
import { spriteIcon } from './lib/utils/common_utils';
 
function sanitize(str) {
return str.replace(/<(?:.|\n)*?>/gm, '');
}
 
export function membersBeforeSave(members) {
return _.map(members, member => {
const GROUP_TYPE = 'Group';
let title = '';
if (member.username == null) {
return member;
}
title = member.name;
if (member.count && !member.mentionsDisabled) {
title += ` (${member.count})`;
}
const autoCompleteAvatar = member.avatar_url || member.username.charAt(0).toUpperCase();
const rectAvatarClass = member.type === GROUP_TYPE ? 'rect-avatar' : '';
const imgAvatar = `<img src="${member.avatar_url}" alt="${member.username}" class="avatar ${rectAvatarClass} avatar-inline center s26"/>`;
const txtAvatar = `<div class="avatar ${rectAvatarClass} center avatar-inline s26">${autoCompleteAvatar}</div>`;
const avatarIcon = member.mentionsDisabled
? spriteIcon('notifications-off', 's16 vertical-align-middle prepend-left-5')
: '';
return {
username: member.username,
avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar,
title: sanitize(title),
search: sanitize(`${member.username} ${member.name}`),
icon: avatarIcon,
};
});
}
export const defaultAutocompleteConfig = {
emojis: true,
members: true,
Loading
Loading
@@ -167,12 +200,13 @@ class GfmAutoComplete {
alias: 'users',
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
const { avatarTag, username, title } = value;
const { avatarTag, username, title, icon } = value;
if (username != null) {
tmpl = GfmAutoComplete.Members.templateFunction({
avatarTag,
username,
title,
icon,
});
}
return tmpl;
Loading
Loading
@@ -185,33 +219,7 @@ class GfmAutoComplete {
data: GfmAutoComplete.defaultLoadingData,
callbacks: {
...this.getDefaultCallbacks(),
beforeSave(members) {
return $.map(members, m => {
let title = '';
if (m.username == null) {
return m;
}
title = m.name;
if (m.count) {
title += ` (${m.count})`;
}
const GROUP_TYPE = 'Group';
const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase();
const rectAvatarClass = m.type === GROUP_TYPE ? 'rect-avatar' : '';
const imgAvatar = `<img src="${m.avatar_url}" alt="${m.username}" class="avatar ${rectAvatarClass} avatar-inline center s26"/>`;
const txtAvatar = `<div class="avatar ${rectAvatarClass} center avatar-inline s26">${autoCompleteAvatar}</div>`;
return {
username: m.username,
avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar,
title: sanitize(title),
search: sanitize(`${m.username} ${m.name}`),
};
});
},
beforeSave: membersBeforeSave,
},
});
}
Loading
Loading
@@ -624,8 +632,8 @@ GfmAutoComplete.Emoji = {
};
// Team Members
GfmAutoComplete.Members = {
templateFunction({ avatarTag, username, title }) {
return `<li>${avatarTag} ${username} <small>${_.escape(title)}</small></li>`;
templateFunction({ avatarTag, username, title, icon }) {
return `<li>${avatarTag} ${username} <small>${_.escape(title)}</small> ${icon}</li>`;
},
};
GfmAutoComplete.Labels = {
Loading
Loading
Loading
Loading
@@ -181,6 +181,7 @@ class GroupsController < Groups::ApplicationController
:avatar,
:description,
:emails_disabled,
:mentions_disabled,
:lfs_enabled,
:name,
:path,
Loading
Loading
Loading
Loading
@@ -55,7 +55,8 @@ module Users
username: group.full_path,
name: group.full_name,
avatar_url: group.avatar_url,
count: group_counts.fetch(group.id, 0)
count: group_counts.fetch(group.id, 0),
mentionsDisabled: group.mentions_disabled
}
end
end
Loading
Loading
Loading
Loading
@@ -23,6 +23,13 @@
%span.d-block= s_('GroupSettings|Disable email notifications')
%span.text-muted= s_('GroupSettings|This setting will override user notification preferences for all members of the group, subgroups, and projects.')
 
.form-group.append-bottom-default
.form-check
= f.check_box :mentions_disabled, checked: @group.mentions_disabled?, class: 'form-check-input'
= f.label :mentions_disabled, class: 'form-check-label' do
%span.d-block= s_('GroupSettings|Disable group mentions')
%span.text-muted= s_('GroupSettings|This setting will prevent group members from being notified if the group is mentioned.')
= render_if_exists 'groups/settings/ip_restriction', f: f, group: @group
= render_if_exists 'groups/settings/allowed_email_domain', f: f, group: @group
= render 'groups/settings/lfs', f: f
Loading
Loading
---
title: Allow groups to disable mentioning their members, if the group is mentioned
merge_request: 20184
author: Fabio Huser
type: added
# frozen_string_literal: true
class AddMentionsDisabledToNamespaces < ActiveRecord::Migration[5.2]
DOWNTIME = false
def change
add_column :namespaces, :mentions_disabled, :boolean
end
end
Loading
Loading
@@ -349,6 +349,7 @@ ActiveRecord::Schema.define(version: 2019_11_25_140458) do
t.boolean "sourcegraph_enabled", default: false, null: false
t.string "sourcegraph_url", limit: 255
t.boolean "sourcegraph_public_only", default: true, null: false
t.bigint "snippet_size_limit", default: 52428800, null: false
t.text "encrypted_akismet_api_key"
t.string "encrypted_akismet_api_key_iv", limit: 255
t.text "encrypted_elasticsearch_aws_secret_access_key"
Loading
Loading
@@ -361,7 +362,6 @@ ActiveRecord::Schema.define(version: 2019_11_25_140458) do
t.string "encrypted_slack_app_secret_iv", limit: 255
t.text "encrypted_slack_app_verification_token"
t.string "encrypted_slack_app_verification_token_iv", limit: 255
t.bigint "snippet_size_limit", default: 52428800, null: false
t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id"
t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id"
t.index ["instance_administration_project_id"], name: "index_applicationsettings_on_instance_administration_project_id"
Loading
Loading
@@ -2603,6 +2603,7 @@ ActiveRecord::Schema.define(version: 2019_11_25_140458) do
t.boolean "emails_disabled"
t.integer "max_pages_size"
t.integer "max_artifacts_size"
t.boolean "mentions_disabled"
t.index ["created_at"], name: "index_namespaces_on_created_at"
t.index ["custom_project_templates_group_id", "type"], name: "index_namespaces_on_custom_project_templates_group_id_and_type", where: "(custom_project_templates_group_id IS NOT NULL)"
t.index ["file_template_project_id"], name: "index_namespaces_on_file_template_project_id"
Loading
Loading
Loading
Loading
@@ -431,6 +431,23 @@ To enable this feature:
1. Expand the **Permissions, LFS, 2FA** section, and select **Disable email notifications**.
1. Click **Save changes**.
 
#### Disabling group mentions
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/21301) in GitLab 12.6.
You can prevent users from being added to a conversation and getting notified when
anyone mentions a group in which those users are members.
Groups with disabled mentions are visualized accordingly in the autocompletion dropdown.
This is particularly helpful for groups with a large number of users.
To enable this feature:
1. Navigate to the group's **Settings > General** page.
1. Expand the **Permissions, LFS, 2FA** section, and select **Disable group mentions**.
1. Click **Save changes**.
### Advanced settings
 
- **Projects**: View all projects within that group, add members to each project,
Loading
Loading
Loading
Loading
@@ -97,7 +97,9 @@ module Banzai
def find_users_for_groups(ids)
return [] if ids.empty?
 
User.joins(:group_members).where(members: { source_id: ids }).to_a
User.joins(:group_members).where(members: {
source_id: Namespace.where(id: ids).where('mentions_disabled IS NOT TRUE').select(:id)
}).to_a
end
 
def find_users_for_projects(ids)
Loading
Loading
Loading
Loading
@@ -8863,6 +8863,9 @@ msgstr ""
msgid "GroupSettings|Disable email notifications"
msgstr ""
 
msgid "GroupSettings|Disable group mentions"
msgstr ""
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
 
Loading
Loading
@@ -8911,6 +8914,9 @@ msgstr ""
msgid "GroupSettings|This setting will override user notification preferences for all members of the group, subgroups, and projects."
msgstr ""
 
msgid "GroupSettings|This setting will prevent group members from being notified if the group is mentioned."
msgstr ""
msgid "GroupSettings|Transfer group"
msgstr ""
 
Loading
Loading
/* eslint no-param-reassign: "off" */
 
import $ from 'jquery';
import { membersBeforeSave } from '~/gfm_auto_complete';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
 
import 'jquery.caret';
Loading
Loading
@@ -262,6 +263,79 @@ describe('GfmAutoComplete', () => {
});
});
 
describe('membersBeforeSave', () => {
const mockGroup = {
username: 'my-group',
name: 'My Group',
count: 2,
avatar_url: './group.jpg',
type: 'Group',
mentionsDisabled: false,
};
it('should return the original object when username is null', () => {
expect(membersBeforeSave([{ ...mockGroup, username: null }])).toEqual([
{ ...mockGroup, username: null },
]);
});
it('should set the text avatar if avatar_url is null', () => {
expect(membersBeforeSave([{ ...mockGroup, avatar_url: null }])).toEqual([
{
username: 'my-group',
avatarTag: '<div class="avatar rect-avatar center avatar-inline s26">M</div>',
title: 'My Group (2)',
search: 'my-group My Group',
icon: '',
},
]);
});
it('should set the image avatar if avatar_url is given', () => {
expect(membersBeforeSave([mockGroup])).toEqual([
{
username: 'my-group',
avatarTag:
'<img src="./group.jpg" alt="my-group" class="avatar rect-avatar avatar-inline center s26"/>',
title: 'My Group (2)',
search: 'my-group My Group',
icon: '',
},
]);
});
it('should set mentions disabled icon if mentionsDisabled is set', () => {
expect(membersBeforeSave([{ ...mockGroup, mentionsDisabled: true }])).toEqual([
{
username: 'my-group',
avatarTag:
'<img src="./group.jpg" alt="my-group" class="avatar rect-avatar avatar-inline center s26"/>',
title: 'My Group',
search: 'my-group My Group',
icon:
'<svg class="s16 vertical-align-middle prepend-left-5"><use xlink:href="undefined#notifications-off" /></svg>',
},
]);
});
it('should set the right image classes for User type members', () => {
expect(
membersBeforeSave([
{ username: 'my-user', name: 'My User', avatar_url: './users.jpg', type: 'User' },
]),
).toEqual([
{
username: 'my-user',
avatarTag:
'<img src="./users.jpg" alt="my-user" class="avatar avatar-inline center s26"/>',
title: 'My User',
search: 'my-user My User',
icon: '',
},
]);
});
});
describe('Issues.insertTemplateFunction', () => {
it('should return default template', () => {
expect(GfmAutoComplete.Issues.insertTemplateFunction({ id: 5, title: 'Some Issue' })).toBe(
Loading
Loading
@@ -298,6 +372,41 @@ describe('GfmAutoComplete', () => {
});
});
 
describe('Members.templateFunction', () => {
it('should return html with avatarTag and username', () => {
expect(
GfmAutoComplete.Members.templateFunction({
avatarTag: 'IMG',
username: 'my-group',
title: '',
icon: '',
}),
).toBe('<li>IMG my-group <small></small> </li>');
});
it('should add icon if icon is set', () => {
expect(
GfmAutoComplete.Members.templateFunction({
avatarTag: 'IMG',
username: 'my-group',
title: '',
icon: '<i class="icon"/>',
}),
).toBe('<li>IMG my-group <small></small> <i class="icon"/></li>');
});
it('should add escaped title if title is set', () => {
expect(
GfmAutoComplete.Members.templateFunction({
avatarTag: 'IMG',
username: 'my-group',
title: 'MyGroup+',
icon: '<i class="icon"/>',
}),
).toBe('<li>IMG my-group <small>MyGroup+</small> <i class="icon"/></li>');
});
});
describe('labels', () => {
const dataSources = {
labels: `${TEST_HOST}/autocomplete_sources/labels`,
Loading
Loading
Loading
Loading
@@ -19,15 +19,23 @@ describe Banzai::ReferenceParser::UserParser do
link['data-group'] = project.group.id.to_s
end
 
it 'returns the users of the group' do
create(:group_member, group: group, user: user)
expect(subject.referenced_by([link])).to eq([user])
end
it 'returns an empty Array when the group has no users' do
expect(subject.referenced_by([link])).to eq([])
end
context 'when group has members' do
let!(:group_member) { create(:group_member, group: group, user: user) }
it 'returns the users of the group' do
expect(subject.referenced_by([link])).to eq([user])
end
it 'returns an empty Array when the group has mentions disabled' do
group.update!(mentions_disabled: true)
expect(subject.referenced_by([link])).to eq([])
end
end
end
 
context 'using a non-existing group ID' do
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment