Commit 351757d9 authored by Martin Wortschack's avatar Martin Wortschack Committed by Brandon Labuschagne

Add notifications dropdown to groups and projects

- This adds the vue notificatio
dropdown to the group detail page
as well as to the user's
notification settings
parent 3a809280
......@@ -45,6 +45,9 @@ export default {
groupId: {
default: null,
},
showLabel: {
default: false,
},
},
data() {
return {
......@@ -70,6 +73,11 @@ export default {
return this.selectedNotificationLevel === 'disabled' ? 'notifications-off' : 'notifications';
},
buttonText() {
return this.showLabel
? this.$options.i18n.notificationTitles[this.selectedNotificationLevel]
: null;
},
buttonTooltip() {
const notificationTitle =
this.$options.i18n.notificationTitles[this.selectedNotificationLevel] ||
......@@ -114,7 +122,9 @@ export default {
data-testid="notificationButton"
:size="buttonSize"
>
<gl-button :size="buttonSize" :icon="buttonIcon" :loading="isLoading" :disabled="disabled" />
<gl-button :size="buttonSize" :icon="buttonIcon" :loading="isLoading" :disabled="disabled">
<template v-if="buttonText">{{ buttonText }}</template>
</gl-button>
<gl-dropdown :size="buttonSize" :disabled="disabled">
<notifications-dropdown-item
v-for="item in notificationLevels"
......@@ -141,6 +151,7 @@ export default {
v-else
v-gl-tooltip="{ title: buttonTooltip }"
data-testid="notificationButton"
:text="buttonText"
:icon="buttonIcon"
:loading="isLoading"
:size="buttonSize"
......
......@@ -6,33 +6,37 @@ import NotificationsDropdown from './components/notifications_dropdown.vue';
Vue.use(GlToast);
export default () => {
const el = document.querySelector('.js-vue-notification-dropdown');
const containers = document.querySelectorAll('.js-vue-notification-dropdown');
if (!el) return false;
if (!containers.length) return false;
const {
containerClass,
buttonSize,
disabled,
dropdownItems,
notificationLevel,
projectId,
groupId,
} = el.dataset;
return new Vue({
el,
provide: {
return containers.forEach((el) => {
const {
containerClass,
buttonSize,
disabled: parseBoolean(disabled),
dropdownItems: JSON.parse(dropdownItems),
initialNotificationLevel: notificationLevel,
disabled,
dropdownItems,
notificationLevel,
projectId,
groupId,
},
render(h) {
return h(NotificationsDropdown);
},
showLabel,
} = el.dataset;
return new Vue({
el,
provide: {
containerClass,
buttonSize,
disabled: parseBoolean(disabled),
dropdownItems: JSON.parse(dropdownItems),
initialNotificationLevel: notificationLevel,
projectId,
groupId,
showLabel: parseBoolean(showLabel),
},
render(h) {
return h(NotificationsDropdown);
},
});
});
};
......@@ -6,10 +6,11 @@ import notificationsDropdown from '~/notifications_dropdown';
import NotificationsForm from '~/notifications_form';
import ProjectsList from '~/projects_list';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import GroupTabs from './group_tabs';
import initInviteMembersBanner from '~/groups/init_invite_members_banner';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import GroupTabs from './group_tabs';
import initNotificationsDropdown from '~/notifications';
export default function initGroupDetails(actionName = 'show') {
const loadableActions = [ACTIVE_TAB_SHARED, ACTIVE_TAB_ARCHIVED];
......@@ -22,7 +23,13 @@ export default function initGroupDetails(actionName = 'show') {
new GroupTabs({ parentEl: '.groups-listing', action });
new ShortcutsNavigation();
new NotificationsForm();
notificationsDropdown();
if (gon.features?.vueNotificationDropdown) {
initNotificationsDropdown();
} else {
notificationsDropdown();
}
new ProjectsList();
initInviteMembersBanner();
......
import NotificationsForm from '../../../../notifications_form';
import notificationsDropdown from '../../../../notifications_dropdown';
import initNotificationsDropdown from '~/notifications';
document.addEventListener('DOMContentLoaded', () => {
new NotificationsForm(); // eslint-disable-line no-new
notificationsDropdown();
initNotificationsDropdown();
});
......@@ -30,6 +30,7 @@ class GroupsController < Groups::ApplicationController
before_action do
push_frontend_feature_flag(:vue_issuables_list, @group)
push_frontend_feature_flag(:vue_notification_dropdown, @group, default_enabled: :yaml)
end
before_action do
......
......@@ -23,7 +23,11 @@
.home-panel-buttons.col-md-12.col-lg-6
- if current_user
.gl-display-flex.gl-flex-wrap.gl-lg-justify-content-end.gl-mx-n2{ data: { testid: 'group-buttons' } }
= render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn gl-button gl-sm-w-auto gl-w-full', dropdown_container_class: 'gl-mr-0 gl-px-2 gl-sm-w-auto gl-w-full', emails_disabled: emails_disabled
- if Feature.enabled?(:vue_notification_dropdown, @group, default_enabled: :yaml)
- if @notification_setting
.js-vue-notification-dropdown{ data: { disabled: emails_disabled, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), group_id: @group.id, container_class: 'gl-mr-3 gl-mt-3 gl-vertical-align-top' } }
- else
= render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn gl-button gl-sm-w-auto gl-w-full', dropdown_container_class: 'gl-mr-0 gl-px-2 gl-sm-w-auto gl-w-full', emails_disabled: emails_disabled
- if can_create_subgroups
.gl-px-2.gl-sm-w-auto.gl-w-full
= link_to _("New subgroup"), new_group_path(parent_id: @group.id), class: "btn btn-success btn-md gl-button btn-success-secondary gl-mt-3 gl-sm-w-auto gl-w-full", data: { qa_selector: 'new_subgroup_button' }
......
......@@ -9,7 +9,11 @@
= link_to group.name, group_path(group)
.table-section.section-30.text-right
= render 'shared/notifications/button', notification_setting: setting, emails_disabled: emails_disabled
- if Feature.enabled?(:vue_notification_dropdown, default_enabled: :yaml)
- if setting
.js-vue-notification-dropdown{ data: { disabled: emails_disabled, dropdown_items: notification_dropdown_items(setting).to_json, notification_level: setting.level, group_id: group.id, container_class: 'gl-mr-3', show_label: "true" } }
- else
= render 'shared/notifications/button', notification_setting: setting, emails_disabled: emails_disabled
.table-section.section-30
= form_for setting, url: profile_notifications_group_path(group), method: :put, html: { class: 'update-notifications gl-display-flex' } do |f|
......
......@@ -8,4 +8,8 @@
= link_to_project(project)
.float-right
= render 'shared/notifications/button', notification_setting: setting, emails_disabled: emails_disabled
- if Feature.enabled?(:vue_notification_dropdown, default_enabled: :yaml)
- if setting
.js-vue-notification-dropdown{ data: { disabled: emails_disabled, dropdown_items: notification_dropdown_items(setting).to_json, notification_level: setting.level, project_id: project.id, container_class: 'gl-mr-3', show_label: "true" } }
- else
= render 'shared/notifications/button', notification_setting: setting, emails_disabled: emails_disabled
......@@ -32,7 +32,11 @@
%br
.clearfix
.form-group.float-left.global-notification-setting
= render 'shared/notifications/button', notification_setting: @global_notification_setting
- if Feature.enabled?(:vue_notification_dropdown, default_enabled: :yaml)
- if @global_notification_setting
.js-vue-notification-dropdown{ data: { dropdown_items: notification_dropdown_items(@global_notification_setting).to_json, notification_level: @global_notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), show_label: 'true' } }
- else
= render 'shared/notifications/button', notification_setting: @global_notification_setting
.clearfix
......
......@@ -163,6 +163,7 @@ RSpec.describe 'Group show page' do
let!(:project) { create(:project, namespace: group) }
before do
stub_feature_flags(vue_notification_dropdown: false)
group.add_maintainer(maintainer)
sign_in(maintainer)
end
......
......@@ -7,6 +7,7 @@ RSpec.describe 'User visits the notifications tab', :js do
let(:user) { create(:user) }
before do
stub_feature_flags(vue_notification_dropdown: false)
project.add_maintainer(user)
sign_in(user)
visit(profile_notifications_path)
......
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { shallowMount } from '@vue/test-utils';
import { GlButtonGroup, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { GlButtonGroup, GlButton, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import waitForPromises from 'helpers/wait_for_promises';
import httpStatus from '~/lib/utils/http_status';
......@@ -27,6 +27,8 @@ describe('NotificationsDropdown', () => {
GlTooltip: createMockDirective(),
},
provide: {
dropdownItems: mockDropdownItems,
initialNotificationLevel: 'global',
...injectedProperties,
},
mocks: {
......@@ -38,6 +40,7 @@ describe('NotificationsDropdown', () => {
}
const findButtonGroup = () => wrapper.find(GlButtonGroup);
const findButton = () => wrapper.find(GlButton);
const findDropdown = () => wrapper.find(GlDropdown);
const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
const findAllNotificationsDropdownItems = () => wrapper.findAll(NotificationsDropdownItem);
......@@ -66,7 +69,6 @@ describe('NotificationsDropdown', () => {
describe('when notification level is "custom"', () => {
beforeEach(() => {
wrapper = createComponent({
dropdownItems: mockDropdownItems,
initialNotificationLevel: 'custom',
});
});
......@@ -74,12 +76,29 @@ describe('NotificationsDropdown', () => {
it('renders a button group', () => {
expect(findButtonGroup().exists()).toBe(true);
});
it('shows the button text when showLabel is true', () => {
wrapper = createComponent({
initialNotificationLevel: 'custom',
showLabel: true,
});
expect(findButton().text()).toBe('Custom');
});
it("doesn't show the button text when showLabel is false", () => {
wrapper = createComponent({
initialNotificationLevel: 'custom',
showLabel: false,
});
expect(findButton().text()).toBe('');
});
});
describe('when notification level is not "custom"', () => {
beforeEach(() => {
wrapper = createComponent({
dropdownItems: mockDropdownItems,
initialNotificationLevel: 'global',
});
});
......@@ -87,6 +106,22 @@ describe('NotificationsDropdown', () => {
it('does not render a button group', () => {
expect(findButtonGroup().exists()).toBe(false);
});
it('shows the button text when showLabel is true', () => {
wrapper = createComponent({
showLabel: true,
});
expect(findDropdown().props('text')).toBe('Global');
});
it("doesn't show the button text when showLabel is false", () => {
wrapper = createComponent({
showLabel: false,
});
expect(findDropdown().props('text')).toBe(null);
});
});
describe('button tooltip', () => {
......@@ -101,7 +136,6 @@ describe('NotificationsDropdown', () => {
${'custom'} | ${'Custom'}
`(`renders "${tooltipTitlePrefix} - $title" for "$level" level`, ({ level, title }) => {
wrapper = createComponent({
dropdownItems: mockDropdownItems,
initialNotificationLevel: level,
});
......@@ -115,7 +149,6 @@ describe('NotificationsDropdown', () => {
describe('button icon', () => {
beforeEach(() => {
wrapper = createComponent({
dropdownItems: mockDropdownItems,
initialNotificationLevel: 'disabled',
});
});
......@@ -125,10 +158,7 @@ describe('NotificationsDropdown', () => {
});
it('renders the "notifications" icon when notification level is not "disabled"', () => {
wrapper = createComponent({
dropdownItems: mockDropdownItems,
initialNotificationLevel: 'global',
});
wrapper = createComponent();
expect(findDropdown().props('icon')).toBe('notifications');
});
......@@ -144,10 +174,7 @@ describe('NotificationsDropdown', () => {
${4} | ${'disabled'} | ${'Disabled'} | ${'You will not get any notifications via email'}
${5} | ${'custom'} | ${'Custom'} | ${'You will only receive notifications for the events you choose'}
`('displays "$title" and "$description"', ({ dropdownIndex, title, description }) => {
wrapper = createComponent({
dropdownItems: mockDropdownItems,
initialNotificationLevel: 'global',
});
wrapper = createComponent();
expect(findAllNotificationsDropdownItems().at(dropdownIndex).props('title')).toBe(title);
expect(findAllNotificationsDropdownItems().at(dropdownIndex).props('description')).toBe(
......@@ -171,8 +198,6 @@ describe('NotificationsDropdown', () => {
'calls the $endpointType endpoint when $condition',
async ({ projectId, groupId, endpointUrl }) => {
wrapper = createComponent({
dropdownItems: mockDropdownItems,
initialNotificationLevel: 'global',
projectId,
groupId,
});
......@@ -187,10 +212,7 @@ describe('NotificationsDropdown', () => {
it('updates the selectedNotificationLevel and marks the item with a checkmark', async () => {
mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.OK, {});
wrapper = createComponent({
dropdownItems: mockDropdownItems,
initialNotificationLevel: 'global',
});
wrapper = createComponent();
const dropdownItem = findDropdownItemAt(1);
......@@ -202,10 +224,7 @@ describe('NotificationsDropdown', () => {
it("won't update the selectedNotificationLevel and shows a toast message when the request fails and ", async () => {
mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.NOT_FOUND, {});
wrapper = createComponent({
dropdownItems: mockDropdownItems,
initialNotificationLevel: 'global',
});
wrapper = createComponent();
await clickDropdownItemAt(1);
......
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