Commit 513c000f authored by Brett Walker's avatar Brett Walker Committed by Paul Slaughter
Browse files

UI for disabling group/project email notification

- Adds UI to configure in group and project settings
- Removes notification configuration for users when
disabled at group or project level
parent 4fd917630136
......@@ -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 @@ def can_change_share_with_group_lock?(group)
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 @@ def notification_icon_class(level)
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 @@ def notification_description(level)
_('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
......
......@@ -155,6 +155,12 @@ def can_change_visibility_level?(project, current_user)
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
......@@ -541,13 +547,15 @@ def project_permissions_settings(project)
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:
......
......@@ -3943,6 +3943,9 @@ msgstr ""
msgid "Disable"
msgstr ""
msgid "Disable email notifications"
msgstr ""
msgid "Disable for this project"
msgstr ""
......@@ -5481,6 +5484,9 @@ msgstr ""
msgid "GroupSettings|Default to Auto DevOps pipeline for all projects within this group"
msgstr ""
msgid "GroupSettings|Disable email notifications"
msgstr ""
msgid "GroupSettings|Learn more about badges."
msgstr ""
......@@ -5508,6 +5514,9 @@ msgstr ""
msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually."
msgstr ""
msgid "GroupSettings|This setting will override user notification preferences for all members of the group, subgroups, and projects."
msgstr ""
msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
msgstr ""
......@@ -7592,6 +7601,9 @@ msgstr ""
msgid "Notifications"
msgstr ""
msgid "Notifications have been disabled by the project or group owner"
msgstr ""
msgid "Notifications off"
msgstr ""
......@@ -11661,6 +11673,9 @@ msgstr ""
msgid "This setting can be overridden in each project."
msgstr ""
msgid "This setting will override user notification preferences for all project members."
msgstr ""
msgid "This setting will update the hostname that is used to generate private commit emails. %{learn_more}"
msgstr ""
......
......@@ -161,4 +161,27 @@
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
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