Commit 0558a809 authored by Sean McGivern's avatar Sean McGivern

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
......@@ -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,
......@@ -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;
......@@ -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,
},
});
}
......@@ -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 = {
......
......@@ -181,6 +181,7 @@ class GroupsController < Groups::ApplicationController
:avatar,
:description,
:emails_disabled,
:mentions_disabled,
:lfs_enabled,
:name,
:path,
......
......@@ -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
......
......@@ -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
......
---
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
......@@ -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"
......@@ -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"
......@@ -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"
......
......@@ -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,
......
......@@ -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)
......
......@@ -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 ""
......@@ -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 ""
......
/* 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';
......@@ -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(
......@@ -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`,
......
......@@ -19,16 +19,24 @@ 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)
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 no users' do
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
it 'returns an empty Array' do
......
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