Commit 0f78af76 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab-ce master

parents 2ef9c251 1068483f
......@@ -28,6 +28,11 @@ export default {
type: Object,
required: true,
},
canDisableEmails: {
type: Boolean,
required: false,
default: false,
},
canChangeVisibilityLevel: {
type: Boolean,
required: false,
......@@ -104,6 +109,7 @@ export default {
lfsEnabled: true,
requestAccessEnabled: true,
highlightChangesClass: false,
emailsDisabled: false,
};
return { ...defaults, ...this.currentSettings };
......@@ -341,5 +347,14 @@ export default {
/>
</project-setting-row>
</div>
<project-setting-row v-if="canDisableEmails" class="mb-3">
<label class="js-emails-disabled">
<input :value="emailsDisabled" type="hidden" name="project[emails_disabled]" />
<input v-model="emailsDisabled" type="checkbox" /> {{ __('Disable email notifications') }}
</label>
<span class="form-text text-muted">{{
__('This setting will override user notification preferences for all project members.')
}}</span>
</project-setting-row>
</div>
</template>
......@@ -31,6 +31,11 @@ module GroupsHelper
can?(current_user, :change_share_with_group_lock, group)
end
def can_disable_group_emails?(group)
Feature.enabled?(:emails_disabled, group, default_enabled: true) &&
can?(current_user, :set_emails_disabled, group) && !group.parent&.emails_disabled?
end
def group_issues_count(state:)
IssuesFinder
.new(current_user, group_id: @group.id, state: state, non_archived: true, include_subgroups: true)
......
......@@ -5,7 +5,7 @@ module NotificationsHelper
def notification_icon_class(level)
case level.to_sym
when :disabled
when :disabled, :owner_disabled
'microphone-slash'
when :participating
'volume-up'
......@@ -18,6 +18,16 @@ module NotificationsHelper
end
end
def notification_icon_level(notification_setting, emails_disabled = false)
if emails_disabled
'owner_disabled'
elsif notification_setting.global?
current_user.global_notification_setting.level
else
notification_setting.level
end
end
def notification_icon(level, text = nil)
icon("#{notification_icon_class(level)} fw", text: text)
end
......@@ -53,6 +63,8 @@ module NotificationsHelper
_('Use your global notification setting')
when :custom
_('You will only receive notifications for the events you choose')
when :owner_disabled
_('Notifications have been disabled by the project or group owner')
end
end
......
......@@ -157,6 +157,12 @@ module ProjectsHelper
end
end
def can_disable_emails?(project, current_user)
return false if project.group&.emails_disabled?
can?(current_user, :set_emails_disabled, project) && Feature.enabled?(:emails_disabled, project, default_enabled: true)
end
def last_push_event
current_user&.recent_push(@project)
end
......@@ -543,13 +549,15 @@ module ProjectsHelper
snippetsAccessLevel: feature.snippets_access_level,
pagesAccessLevel: feature.pages_access_level,
containerRegistryEnabled: !!project.container_registry_enabled,
lfsEnabled: !!project.lfs_enabled
lfsEnabled: !!project.lfs_enabled,
emailsDisabled: project.emails_disabled?
}
end
def project_permissions_panel_data(project)
{
currentSettings: project_permissions_settings(project),
canDisableEmails: can_disable_emails?(project, current_user),
canChangeVisibilityLevel: can_change_visibility_level?(project, current_user),
allowedVisibilityOptions: project_allowed_visibility_levels(project),
visibilityHelpPath: help_page_path('public_access/public_access'),
......
......@@ -98,6 +98,10 @@ class IssuableSidebarBasicEntity < Grape::Entity
autocomplete_projects_path(project_id: issuable.project.id)
end
expose :project_emails_disabled do |issuable|
issuable.project.emails_disabled?
end
private
def current_user
......
- can_create_subgroups = can?(current_user, :create_subgroup, @group)
- emails_disabled = @group.emails_disabled?
.group-home-panel
.row.mb-3
......@@ -21,7 +22,7 @@
.home-panel-buttons.col-md-12.col-lg-6.d-inline-flex.flex-wrap.justify-content-lg-end
- if current_user
.group-buttons
= render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn'
= render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn', emails_disabled: emails_disabled
- if can? current_user, :create_projects, @group
- new_project_label = _("New project")
- new_subgroup_label = _("New subgroup")
......
......@@ -11,12 +11,18 @@
.form-check
= f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group), class: 'form-check-input'
= f.label :share_with_group_lock, class: 'form-check-label' do
%span
%span.d-block
- group_link = link_to @group.name, group_path(@group)
= s_('GroupSettings|Prevent sharing a project within %{group} with other groups').html_safe % { group: group_link }
%br
%span.descr.text-muted= share_with_group_lock_help_text(@group)
.form-group.append-bottom-default
.form-check
= f.check_box :emails_disabled, checked: @group.emails_disabled?, disabled: !can_disable_group_emails?(@group), class: 'form-check-input'
= f.label :emails_disabled, class: 'form-check-label' do
%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.')
= 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
......
- emails_disabled = group.emails_disabled?
.gl-responsive-table-row.notification-list-item
.table-section.section-40
%span.notification.fa.fa-holder.append-right-5
- if setting.global?
= notification_icon(current_user.global_notification_setting.level)
- else
= notification_icon(setting.level)
= notification_icon(notification_icon_level(setting, emails_disabled))
%span.str-truncated
= link_to group.name, group_path(group)
.table-section.section-30.text-right
= render 'shared/notifications/button', notification_setting: setting
= render 'shared/notifications/button', notification_setting: setting, emails_disabled: emails_disabled
.table-section.section-30
= form_for @user.notification_settings.find { |ns| ns.source == group }, url: profile_notifications_group_path(group), method: :put, html: { class: 'update-notifications' } do |f|
......
- emails_disabled = project.emails_disabled?
%li.notification-list-item
%span.notification.fa.fa-holder.append-right-5
- if setting.global?
= notification_icon(current_user.global_notification_setting.level)
- else
= notification_icon(setting.level)
= notification_icon(notification_icon_level(setting, emails_disabled))
%span.str-truncated
= link_to_project(project)
.float-right
= render 'shared/notifications/button', notification_setting: setting
= render 'shared/notifications/button', notification_setting: setting, emails_disabled: emails_disabled
- empty_repo = @project.empty_repo?
- show_auto_devops_callout = show_auto_devops_callout?(@project)
- max_project_topic_length = 15
- emails_disabled = @project.emails_disabled?
.project-home-panel{ class: [("empty-project" if empty_repo), ("js-keep-hidden-on-navigation" if vue_file_list_enabled?)] }
.row.append-bottom-8
.home-panel-title-row.col-md-12.col-lg-6.d-flex
......@@ -41,7 +43,7 @@
.project-repo-buttons.col-md-12.col-lg-6.d-inline-flex.flex-wrap.justify-content-lg-end
- if current_user
.d-inline-flex
= render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn-xs'
= render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn-xs', emails_disabled: emails_disabled
.count-buttons.d-inline-flex
= render 'projects/buttons/star'
......
......@@ -137,7 +137,11 @@
.js-sidebar-participants-entry-point
- if signed_in
.js-sidebar-subscriptions-entry-point
- if issuable_sidebar[:project_emails_disabled]
.block.js-emails-disabled
= notification_description(:owner_disabled)
- else
.js-sidebar-subscriptions-entry-point
- project_ref = issuable_sidebar[:reference]
.block.project-reference
......
- btn_class = local_assigns.fetch(:btn_class, nil)
- btn_class = local_assigns.fetch(:btn_class, '')
- emails_disabled = local_assigns.fetch(:emails_disabled, false)
- if notification_setting
- if emails_disabled
- button_title = notification_description(:owner_disabled)
- aria_label = button_title
- btn_class << " disabled"
- else
- button_title = _("Notification setting")
- aria_label = _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }
.js-notification-dropdown.notification-dropdown.mr-md-2.home-panel-action-button.dropdown.inline
= form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f|
= hidden_setting_source_input(notification_setting)
......@@ -8,14 +17,14 @@
.js-notification-toggle-btns
%div{ class: ("btn-group" if notification_setting.custom?) }
- if notification_setting.custom?
%button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn.text-left#notifications-button{ type: "button", title: _("Notification setting"), class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
%button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn.text-left#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
%button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
= icon('caret-down')
.sr-only Toggle dropdown
- else
%button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: _("Notification setting"), class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
%button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
.float-left
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
......
- btn_class = local_assigns.fetch(:btn_class, nil)
- btn_class = local_assigns.fetch(:btn_class, '')
- emails_disabled = local_assigns.fetch(:emails_disabled, false)
- if notification_setting
- if emails_disabled
- button_title = notification_description(:owner_disabled)
- btn_class << " disabled"
- else
- button_title = _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }
.js-notification-dropdown.notification-dropdown.home-panel-action-button.prepend-top-default.append-right-8.dropdown.inline
= form_for notification_setting, remote: true, html: { class: "inline notification-form no-label" } do |f|
= hidden_setting_source_input(notification_setting)
......@@ -9,14 +16,14 @@
.js-notification-toggle-btns
%div{ class: ("btn-group" if notification_setting.custom?) }
- if notification_setting.custom?
%button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", placement: 'top', toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
%button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => button_title, data: { container: "body", placement: 'top', toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
= notification_setting_icon(notification_setting)
%span.js-notification-loading.fa.hidden
%button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" }, class: "#{btn_class}" }
= sprite_icon("arrow-down", css_class: "icon mr-0")
.sr-only Toggle dropdown
- else
%button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", placement: 'top', toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
%button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => button_title, data: { container: "body", placement: 'top', toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
= notification_setting_icon(notification_setting)
%span.js-notification-loading.fa.hidden
= sprite_icon("arrow-down", css_class: "icon")
......
---
title: UI for disabling group/project email notifications
merge_request: 30961
author: Dustin Spicuzza
type: added
......@@ -411,6 +411,17 @@ To enable this feature, navigate to the group settings page, expand the
Define project templates at a group level by setting a group as the template source.
[Learn more about group-level project templates](custom_project_templates.md).
#### Disabling email notifications
You can disable all email notifications related to the group, which also includes
it's subgroups and projects.
To enable this feature:
1. Navigate to the group's **Settings > General** page.
1. Expand the **Permissions, LFS, 2FA** section, and select **Disable email notifications**.
1. Click **Save changes**.
### Advanced settings
- **Projects**: View all projects within that group, add members to each project,
......
......@@ -126,6 +126,7 @@ The following table depicts the various user permission levels in a project.
| Transfer project to another namespace | | | | | ✓ |
| Remove project | | | | | ✓ |
| Delete issues | | | | | ✓ |
| Disable notification emails | | | | | ✓ |
| Force push to protected branches (*4*) | | | | | |
| Remove protected branches (*4*) | | | | | |
......@@ -220,6 +221,7 @@ group.
| Remove group | | | | | ✓ |
| Delete group epic **(ULTIMATE)** | | | | | ✓ |
| View group Audit Events | | | | | ✓ |
| Disable notification emails | | | | | ✓ |
- (1): Groups can be set to [allow either Owners or Owners and
Maintainers to create subgroups](group/subgroups/index.md#creating-a-subgroup)
......
......@@ -32,6 +32,12 @@ links will be missing from the sidebar UI.
You can still access them with direct links if you can access Merge Requests. This is deliberate, if you can see
Issues or Merge Requests, both of which use Labels and Milestones, then you shouldn't be denied access to Labels and Milestones pages.
#### Disabling email notifications
You can disable all email notifications related to the project by selecting the
**Disable email notifications** checkbox. Only the project owner is allowed to change
this setting.
### Issue settings
Add an [issue description template](../description_templates.md#description-templates) to your project, so that every new issue will start with a custom template.
......
......@@ -51,6 +51,10 @@ Organization like this is suitable for users that belong to different groups but
same need for being notified for every group they are member of.
These settings can be configured on group page under the name of the group. It will be the dropdown with the bell icon. They can also be configured on the user profile notifications dropdown.
The group owner can disable email notifications for a group, which also includes
it's subgroups and projects. If this is the case, you will not receive any corresponding notifications,
and the notification button will be disabled with an explanatory tooltip.
### Project Settings
![notification settings](img/notification_project_settings.png)
......@@ -60,6 +64,10 @@ other setting.
This is suitable for users that have different needs for notifications per project basis.
These settings can be configured on project page under the name of the project. It will be the dropdown with the bell icon. They can also be configured on the user profile notifications dropdown.
The project owner (or it's group owner) can disable email notifications for the project.
If this is the case, you will not receive any corresponding notifications, and the notification
button will be disabled with an explanatory tooltip.
## Notification events
Below is the table of events users can be notified of:
......
......@@ -4963,6 +4963,9 @@ msgstr ""
msgid "Disable"
msgstr ""
msgid "Disable email notifications"
msgstr ""
msgid "Disable for this project"
msgstr ""
......
......@@ -161,4 +161,27 @@ describe 'Group show page' do
expect(find('.group-row:nth-child(3) .namespace-title > a')).to have_content(project3.title)
end
end
context 'notification button', :js do
let(:maintainer) { create(:user) }
let!(:project) { create(:project, namespace: group) }
before do
group.add_maintainer(maintainer)
sign_in(maintainer)
end
it 'is enabled by default' do
visit path
expect(page).to have_selector('.notifications-btn:not(.disabled)', visible: true)
end
it 'is disabled if emails are disabled' do
group.update_attribute(:emails_disabled, true)
visit path
expect(page).to have_selector('.notifications-btn.disabled', visible: true)
end
end
end
......@@ -27,4 +27,14 @@ describe "User toggles subscription", :js do
# Check we're unsubscribed.
expect(subscription_button).to have_css("button:not(.is-checked)")
end
context 'when project emails are disabled' do
let(:project) { create(:project_empty_repo, :public, emails_disabled: true) }
it 'is disabled' do
expect(page).to have_content('Notifications have been disabled by the project or group owner')
expect(page).to have_selector('.js-emails-disabled', visible: true)
expect(page).not_to have_selector('.js-issuable-subscribe-button')
end
end
end
......@@ -20,4 +20,12 @@ describe 'User visits the notifications tab', :js do
expect(page).to have_selector('#notifications-button', text: 'On mention')
end
context 'when project emails are disabled' do
let(:project) { create(:project, emails_disabled: true) }
it 'notification button is disabled' do
expect(page).to have_selector('.notifications-btn.disabled', visible: true)
end
end
end
......@@ -59,6 +59,12 @@ describe 'Projects > Settings > Visibility settings', :js do
end
end
end
context 'disable email notifications' do
it 'is visible' do
expect(page).to have_selector('.js-emails-disabled', visible: true)
end
end
end
context 'as maintainer' do
......@@ -76,5 +82,11 @@ describe 'Projects > Settings > Visibility settings', :js do
expect(visibility_select_container).to have_selector 'select[name="project[visibility_level]"]:disabled'
expect(visibility_select_container).to have_content 'The project can be accessed by anyone, regardless of authentication.'
end
context 'disable email notifications' do
it 'is not available' do
expect(page).not_to have_selector('.js-emails-disabled', visible: true)
end
end
end
end
......@@ -65,4 +65,12 @@ describe 'Projects > Show > User manages notifications', :js do
end
end
end
context 'when project emails are disabled' do
let(:project) { create(:project, :public, :repository, emails_disabled: true) }
it 'is disabled' do
expect(page).to have_selector('.notifications-btn.disabled', visible: true)
end
end
end
......@@ -262,4 +262,44 @@ describe GroupsHelper do
expect(parent_group_options(group2)).to eq([{ id: group.id, text: group.human_name }].to_json)
end
end
describe '#can_disable_group_emails?' do
let(:current_user) { create(:user) }
let(:group) { create(:group, name: 'group') }
let(:subgroup) { create(:group, name: 'subgroup', parent: group) }
before do
allow(helper).to receive(:current_user) { current_user }
end
it 'returns true for the group owner' do
allow(helper).to receive(:can?).with(current_user, :set_emails_disabled, group) { true }
expect(helper.can_disable_group_emails?(group)).to be_truthy
end
it 'returns false for anyone else' do
allow(helper).to receive(:can?).with(current_user, :set_emails_disabled, group) { false }
expect(helper.can_disable_group_emails?(group)).to be_falsey
end
context 'when subgroups' do
before do
allow(helper).to receive(:can?).with(current_user, :set_emails_disabled, subgroup) { true }
end
it 'returns false if parent group is disabling emails' do
allow(group).to receive(:emails_disabled?).and_return(true)
expect(helper.can_disable_group_emails?(subgroup)).to be_falsey
end
it 'returns true if parent group is not disabling emails' do
allow(group).to receive(:emails_disabled?).and_return(false)
expect(helper.can_disable_group_emails?(subgroup)).to be_truthy
end
end
end
end
......@@ -3,6 +3,7 @@ require 'spec_helper'
describe NotificationsHelper do
describe 'notification_icon' do
it { expect(notification_icon(:disabled)).to match('class="fa fa-microphone-slash fa-fw"') }
it { expect(notification_icon(:owner_disabled)).to match('class="fa fa-microphone-slash fa-fw"') }
it { expect(notification_icon(:participating)).to match('class="fa fa-volume-up fa-fw"') }
it { expect(notification_icon(:mention)).to match('class="fa fa-at fa-fw"') }
it { expect(notification_icon(:global)).to match('class="fa fa-globe fa-fw"') }
......@@ -19,4 +20,14 @@ describe NotificationsHelper do
it { expect(notification_event_name(:success_pipeline)).to match('Successful pipeline') }
it { expect(notification_event_name(:failed_pipeline)).to match('Failed pipeline') }
end
describe '#notification_icon_level' do
let(:user) { create(:user) }
let(:global_setting) { user.global_notification_setting }
let(:notification_setting) { create(:notification_setting, level: :watch) }
it { expect(notification_icon_level(notification_setting, true)).to eq 'owner_disabled' }
it { expect(notification_icon_level(notification_setting)).to eq 'watch' }
it { expect(notification_icon_level(global_setting)).to eq 'participating' }
end
end
......@@ -107,6 +107,30 @@ describe ProjectsHelper do
end
end
describe '#can_disable_emails?' do
let(:project) { create(:project) }
let(:user) { create(:project_member, :maintainer, user: create(:user), project: project).user }
it 'returns true for the project owner' do
allow(helper).to receive(:can?).with(project.owner, :set_emails_disabled, project) { true }
expect(helper.can_disable_emails?(project, project.owner)).to be_truthy
end
it 'returns false for anyone else' do
allow(helper).to receive(:can?).with(user, :set_emails_disabled, project) { false }
expect(helper.can_disable_emails?(project, user)).to be_falsey
end
it 'returns false if group emails disabled' do
project = create(:project, group: create(:group))
allow(project.group).to receive(:emails_disabled?).and_return(true)
expect(helper.can_disable_emails?(project, project.owner)).to be_falsey
end
end
describe "readme_cache_key" do
let(:project) { create(:project, :repository) }
......@@ -477,6 +501,7 @@ describe ProjectsHelper do
it 'returns the command to push to create project over SSH' do
allow(Gitlab::CurrentSettings.current_application_settings).to receive(:enabled_git_access_protocol) { 'ssh' }
allow(Gitlab.config.gitlab_shell).to receive(:ssh_path_prefix).and_return('git@localhost:')
expect(helper.push_to_create_project_command(user)).to eq('git push --set-upstream git@localhost:john/$(git rev-parse --show-toplevel | xargs basename).git $(git rev-parse --abbrev-ref HEAD)')
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