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