Commit cd4cb29b authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 2b339d4e
Please view this file on the master branch, on stable branches it's out of date.
## 12.5.3
### Performance (1 change)
- Geo - Improve query performance to determine job artifacts to sync when selective sync is enabled. !19583
### Other (1 change)
- Geo - Does not schedule duplicated jobs while backfilling uploads, LFS objects and job artifacts. !20324
## 12.5.1
### Security (6 changes)
......
......@@ -16,13 +16,6 @@ entry.
- Flatten exception details in API and controller logs. !20434
## 12.5.2
### Security (1 change)
- Fix 500 error caused by invalid byte sequences in links.
## 12.5.1
### Security (11 changes)
......
......@@ -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 = {
......
......@@ -182,6 +182,10 @@ const bindEvents = () => {
text: s__('ProjectTemplates|Netlify/Hexo'),
icon: '.template-option .icon-netlify',
},
salesforcedx: {
text: s__('ProjectTemplates|SalesforceDX'),
icon: '.template-option svg.icon-gitlab',
},
serverless_framework: {
text: s__('ProjectTemplates|Serverless Framework/JS'),
icon: '.template-option .icon-serverless_framework',
......
......@@ -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
......
......@@ -25,3 +25,5 @@ module Issues
end
end
end
Issues::DuplicateService.prepend_if_ee('EE::Issues::DuplicateService')
......@@ -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: Relate issues when they are marked as duplicated
merge_request: 20161
author: minghuan lei
type: added
---
title: Allow groups to disable mentioning their members, if the group is mentioned
merge_request: 20184
author: Fabio Huser
type: added
---
title: Upgrade to Gitaly v1.75.0
merge_request: 21045
author:
type: changed
---
title: Add SalesforceDX project template
merge_request: 20831
author:
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,
......
......@@ -60,7 +60,7 @@ The following quick actions are applicable to descriptions, discussions and thre
| `/remove_epic` | ✓ | | | Remove from epic **(ULTIMATE)** |
| `/promote` | ✓ | | | Promote issue to epic **(ULTIMATE)** |
| `/confidential` | ✓ | | | Make confidential |
| `/duplicate <#issue>` | ✓ | | | Mark this issue as a duplicate of another issue |
| `/duplicate <#issue>` | ✓ | | | Mark this issue as a duplicate of another issue and relate them for **(STARTER)** |
| `/create_merge_request <branch name>` | ✓ | | | Create a new merge request starting from the current issue |
| `/relate #issue1 #issue2` | ✓ | | | Mark issues as related **(STARTER)** |
| `/move <path/to/project>` | ✓ | | | Move this issue to another 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)
......
......@@ -54,6 +54,7 @@ module Gitlab
ProjectTemplate.new('nfplainhtml', 'Netlify/Plain HTML', _('A plain HTML site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfplain-html', 'illustrations/logos/netlify.svg'),
ProjectTemplate.new('nfgitbook', 'Netlify/GitBook', _('A GitBook site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfgitbook', 'illustrations/logos/netlify.svg'),
ProjectTemplate.new('nfhexo', 'Netlify/Hexo', _('A Hexo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhexo', 'illustrations/logos/netlify.svg'),
ProjectTemplate.new('salesforcedx', 'SalesforceDX', _('A project boilerplate for Salesforce App development with Salesforce Developer tools.'), 'https://gitlab.com/gitlab-org/project-templates/salesforcedx'),
ProjectTemplate.new('serverless_framework', 'Serverless Framework/JS', _('A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages'), 'https://gitlab.com/gitlab-org/project-templates/serverless-framework', 'illustrations/logos/serverless_framework.svg')
].freeze
......
......@@ -29,14 +29,14 @@ module Gitlab
def event(category, action, label: nil, property: nil, value: nil, context: nil)
return unless enabled?
snowplow.track_struct_event(category, action, label, property, value, context, Time.now.to_i)
snowplow.track_struct_event(category, action, label, property, value, context, (Time.now.to_f * 1000).to_i)
end
def self_describing_event(schema_url, event_data_json, context: nil)
return unless enabled?
event_json = SnowplowTracker::SelfDescribingJson.new(schema_url, event_data_json)
snowplow.track_self_describing_event(event_json, context, Time.now.to_i)
snowplow.track_self_describing_event(event_json, context, (Time.now.to_f * 1000).to_i)
end
def snowplow_options(group)
......
......@@ -696,6 +696,9 @@ msgstr ""
msgid "A plain HTML site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
msgid "A project boilerplate for Salesforce App development with Salesforce Developer tools."
msgstr ""
msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
msgstr ""
......@@ -8860,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 ""
......@@ -8908,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 ""
......@@ -13721,6 +13730,9 @@ msgstr ""
msgid "ProjectTemplates|Ruby on Rails"
msgstr ""
msgid "ProjectTemplates|SalesforceDX"
msgstr ""
msgid "ProjectTemplates|Serverless Framework/JS"
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
......
......@@ -23,6 +23,7 @@ describe Gitlab::ProjectTemplate do
described_class.new('nfplainhtml', 'Netlify/Plain HTML', _('A plain HTML site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfplain-html'),
described_class.new('nfgitbook', 'Netlify/GitBook', _('A GitBook site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfgitbook'),
described_class.new('nfhexo', 'Netlify/Hexo', _('A Hexo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhexo'),
described_class.new('salesforcedx', 'SalesforceDX', _('A project boilerplate for Salesforce App development with Salesforce Developer tools.'), 'https://gitlab.com/gitlab-org/project-templates/salesforcedx'),
described_class.new('serverless_framework', 'Serverless Framework/JS', _('A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages'), 'https://gitlab.com/gitlab-org/project-templates/serverless-framework', 'illustrations/logos/serverless_framework.svg')
]
......
......@@ -97,7 +97,7 @@ describe Gitlab::Tracking do
'_property_',
'_value_',
nil,
timestamp.to_i
(timestamp.to_f * 1000).to_i
)
track_event
......@@ -130,7 +130,7 @@ describe Gitlab::Tracking do
expect(tracker).to receive(:track_self_describing_event).with(
'_event_json_',
nil,
timestamp.to_i
(timestamp.to_f * 1000).to_i
)
track_event
......
......@@ -689,8 +689,9 @@ describe Issues::UpdateService, :mailer do
context 'valid canonical_issue_id' do
it 'calls the duplicate service with both issues' do
expect_any_instance_of(Issues::DuplicateService)
.to receive(:execute).with(issue, canonical_issue)
expect_next_instance_of(Issues::DuplicateService) do |service|
expect(service).to receive(:execute).with(issue, canonical_issue)
end
update_issue(canonical_issue_id: canonical_issue.id)
end
......
# frozen_string_literal: true
shared_examples 'duplicate quick action' do
context 'mark issue as duplicate' do
let(:original_issue) { create(:issue, project: project) }
context 'when the current user can update issues' do
it 'does not create a note, and marks the issue as a duplicate' do
add_note("/duplicate ##{original_issue.to_reference}")
expect(page).not_to have_content "/duplicate #{original_issue.to_reference}"
expect(page).to have_content "marked this issue as a duplicate of #{original_issue.to_reference}"
expect(issue.reload).to be_closed
end
end
context 'when the current user cannot update the issue' do
let(:guest) { create(:user) }
before do
project.add_guest(guest)
gitlab_sign_out
sign_in(guest)
visit project_issue_path(project, issue)
end
it 'does not create a note, and does not mark the issue as a duplicate' do
add_note("/duplicate ##{original_issue.to_reference}")
expect(page).not_to have_content "marked this issue as a duplicate of #{original_issue.to_reference}"
expect(issue.reload).to be_open
end
end
end
end
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