summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-12-30 09:08:38 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-12-30 09:08:38 +0000
commit8ca437b79438e914b2eede563906e7889c81dc19 (patch)
treea736452eff9c2d6353498f9ad366b231103062ad
parent142658ee280a72a0cb45b241c43ddc495d852814 (diff)
downloadgitlab-ce-8ca437b79438e914b2eede563906e7889c81dc19.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/notifications/components/notification_email_listbox_input.vue70
-rw-r--r--app/assets/javascripts/notifications/index.js27
-rw-r--r--app/assets/javascripts/profile/profile.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue18
-rw-r--r--app/views/profiles/notifications/_email_settings.html.haml3
-rw-r--r--app/views/profiles/notifications/_group_settings.html.haml2
-rw-r--r--locale/gitlab.pot3
-rw-r--r--spec/frontend/notifications/components/notification_email_listbox_input_spec.js81
-rw-r--r--spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js23
-rw-r--r--spec/views/profiles/notifications/show.html.haml_spec.rb25
10 files changed, 243 insertions, 13 deletions
diff --git a/app/assets/javascripts/notifications/components/notification_email_listbox_input.vue b/app/assets/javascripts/notifications/components/notification_email_listbox_input.vue
new file mode 100644
index 00000000000..c58f4856d44
--- /dev/null
+++ b/app/assets/javascripts/notifications/components/notification_email_listbox_input.vue
@@ -0,0 +1,70 @@
+<script>
+import ListboxInput from '~/vue_shared/components/listbox_input/listbox_input.vue';
+
+export default {
+ components: {
+ ListboxInput,
+ },
+ inject: {
+ label: {
+ from: 'label',
+ default: '',
+ },
+ name: {
+ from: 'name',
+ },
+ emails: {
+ from: 'emails',
+ default: () => [],
+ },
+ emptyValueText: {
+ from: 'emptyValueText',
+ required: true,
+ },
+ value: {
+ from: 'value',
+ default: '',
+ },
+ disabled: {
+ from: 'disabled',
+ default: false,
+ },
+ },
+ data() {
+ return {
+ selected: this.value,
+ };
+ },
+ computed: {
+ options() {
+ return [
+ {
+ value: '',
+ text: this.emptyValueText,
+ },
+ ...this.emails.map((email) => ({
+ text: email,
+ value: email,
+ })),
+ ];
+ },
+ },
+ methods: {
+ async onSelect() {
+ await this.$nextTick();
+ this.$el.closest('form').submit();
+ },
+ },
+};
+</script>
+
+<template>
+ <listbox-input
+ v-model="selected"
+ :label="label"
+ :name="name"
+ :items="options"
+ :disabled="disabled"
+ @select="onSelect"
+ />
+</template>
diff --git a/app/assets/javascripts/notifications/index.js b/app/assets/javascripts/notifications/index.js
index a81f2c2590b..42271b44e7c 100644
--- a/app/assets/javascripts/notifications/index.js
+++ b/app/assets/javascripts/notifications/index.js
@@ -2,10 +2,37 @@ import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import NotificationsDropdown from './components/notifications_dropdown.vue';
+import NotificationEmailListboxInput from './components/notification_email_listbox_input.vue';
Vue.use(GlToast);
+const initNotificationEmailListboxInputs = () => {
+ const els = [...document.querySelectorAll('.js-notification-email-listbox-input')];
+
+ els.forEach((el, index) => {
+ const { label, name, emptyValueText, value } = el.dataset;
+
+ return new Vue({
+ el,
+ name: `NotificationEmailListboxInputRoot${index + 1}`,
+ provide: {
+ label,
+ name,
+ emails: JSON.parse(el.dataset.emails),
+ emptyValueText,
+ value,
+ disabled: parseBoolean(el.dataset.disabled),
+ },
+ render(h) {
+ return h(NotificationEmailListboxInput);
+ },
+ });
+ });
+};
+
export default () => {
+ initNotificationEmailListboxInputs();
+
const containers = document.querySelectorAll('.js-vue-notification-dropdown');
if (!containers.length) return false;
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index 93bc203d391..c031c5e5e8e 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -30,10 +30,6 @@ export default class Profile {
bindEvents() {
$('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
- $('.js-group-notification-email').on('change', this.submitForm);
- $('#user_notification_email').on('select2-selecting', (event) => {
- setTimeout(this.submitForm.bind(event.currentTarget));
- });
$('#user_email_opted_in').on('change', this.submitForm);
$('#user_notified_of_own_activity').on('change', this.submitForm);
this.form.on('submit', this.onSubmitForm);
diff --git a/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue b/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue
index a7d3bcfd59f..8340ee19084 100644
--- a/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue
+++ b/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue
@@ -19,6 +19,11 @@ export default {
required: false,
default: '',
},
+ description: {
+ type: String,
+ required: false,
+ default: '',
+ },
name: {
type: String,
required: true,
@@ -37,6 +42,11 @@ export default {
type: GlListbox.props.items.type,
required: true,
},
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -44,6 +54,9 @@ export default {
};
},
computed: {
+ wrapperComponent() {
+ return this.label || this.description ? 'gl-form-group' : 'div';
+ },
allOptions() {
const allOptions = [];
@@ -102,16 +115,17 @@ export default {
</script>
<template>
- <gl-form-group :label="label">
+ <component :is="wrapperComponent" :label="label" :description="description">
<gl-listbox
:selected="selected"
:toggle-text="toggleText"
:items="filteredItems"
:searchable="isSearchable"
:no-results-text="$options.i18n.noResultsText"
+ :disabled="disabled"
@search="search"
@select="$emit($options.model.event, $event)"
/>
<input ref="input" type="hidden" :name="name" :value="selected" />
- </gl-form-group>
+ </component>
</template>
diff --git a/app/views/profiles/notifications/_email_settings.html.haml b/app/views/profiles/notifications/_email_settings.html.haml
index c4de33dcd9e..cd7a7ced1d4 100644
--- a/app/views/profiles/notifications/_email_settings.html.haml
+++ b/app/views/profiles/notifications/_email_settings.html.haml
@@ -1,7 +1,6 @@
- form = local_assigns.fetch(:form)
.form-group
- = form.label :notification_email, _('Notification Email'), class: "label-bold"
- = form.select :notification_email, @user.public_verified_emails, { include_blank: _('Use primary email (%{email})') % { email: @user.email }, selected: @user.notification_email }, class: "select2", disabled: local_assigns.fetch(:email_change_disabled, nil)
+ .js-notification-email-listbox-input{ data: { label: _('Notification Email'), name: 'user[notification_email]', emails: @user.public_verified_emails.to_json, empty_value_text: _('Use primary email (%{email})') % { email: @user.email }, value: @user.notification_email, disabled: local_assigns.fetch(:email_change_disabled, nil) } }
.help-block
= local_assigns.fetch(:help_text, nil)
.form-group
diff --git a/app/views/profiles/notifications/_group_settings.html.haml b/app/views/profiles/notifications/_group_settings.html.haml
index 23fce8e04b6..898762ca78a 100644
--- a/app/views/profiles/notifications/_group_settings.html.haml
+++ b/app/views/profiles/notifications/_group_settings.html.haml
@@ -14,4 +14,4 @@
.table-section.section-30
= form_for setting, url: profile_group_notifications_path(group), method: :put, html: { class: 'update-notifications gl-display-flex' } do |f|
- = f.select :notification_email, @user.public_verified_emails, { include_blank: 'Global notification email' }, class: 'select2 js-group-notification-email'
+ .js-notification-email-listbox-input{ data: { name: 'notification_setting[notification_email]', emails: @user.public_verified_emails.to_json, empty_value_text: _('Global notification email') , value: setting.notification_email } }
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f69467e7d14..461e63df9b4 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -18829,6 +18829,9 @@ msgstr ""
msgid "Global Shortcuts"
msgstr ""
+msgid "Global notification email"
+msgstr ""
+
msgid "Global notification level"
msgstr ""
diff --git a/spec/frontend/notifications/components/notification_email_listbox_input_spec.js b/spec/frontend/notifications/components/notification_email_listbox_input_spec.js
new file mode 100644
index 00000000000..c490c737cf1
--- /dev/null
+++ b/spec/frontend/notifications/components/notification_email_listbox_input_spec.js
@@ -0,0 +1,81 @@
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import ListboxInput from '~/vue_shared/components/listbox_input/listbox_input.vue';
+import NotificationEmailListboxInput from '~/notifications/components/notification_email_listbox_input.vue';
+
+describe('NotificationEmailListboxInput', () => {
+ let wrapper;
+
+ // Props
+ const label = 'label';
+ const name = 'name';
+ const emails = ['test@gitlab.com'];
+ const emptyValueText = 'emptyValueText';
+ const value = 'value';
+ const disabled = false;
+
+ // Finders
+ const findListboxInput = () => wrapper.findComponent(ListboxInput);
+
+ const createComponent = (attachTo) => {
+ wrapper = shallowMount(NotificationEmailListboxInput, {
+ provide: {
+ label,
+ name,
+ emails,
+ emptyValueText,
+ value,
+ disabled,
+ },
+ attachTo,
+ });
+ };
+
+ describe('props', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it.each`
+ propName | propValue
+ ${'label'} | ${label}
+ ${'name'} | ${name}
+ ${'selected'} | ${value}
+ ${'disabled'} | ${disabled}
+ `('passes the $propName prop to ListboxInput', ({ propName, propValue }) => {
+ expect(findListboxInput().props(propName)).toBe(propValue);
+ });
+
+ it('passes the options to ListboxInput', () => {
+ expect(findListboxInput().props('items')).toStrictEqual([
+ { text: emptyValueText, value: '' },
+ { text: emails[0], value: emails[0] },
+ ]);
+ });
+ });
+
+ describe('form', () => {
+ let form;
+
+ beforeEach(() => {
+ form = document.createElement('form');
+ const root = document.createElement('div');
+ form.appendChild(root);
+ createComponent(root);
+ });
+
+ afterEach(() => {
+ form = null;
+ });
+
+ it('submits the parent form when the value changes', async () => {
+ jest.spyOn(form, 'submit');
+ expect(form.submit).not.toHaveBeenCalled();
+
+ findListboxInput().vm.$emit('select');
+ await nextTick();
+
+ expect(form.submit).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js b/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js
index 2b62cbb9ab3..f7c705c495a 100644
--- a/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js
+++ b/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js
@@ -7,6 +7,7 @@ describe('ListboxInput', () => {
// Props
const label = 'label';
+ const decription = 'decription';
const name = 'name';
const defaultToggleText = 'defaultToggleText';
const items = [
@@ -32,6 +33,7 @@ describe('ListboxInput', () => {
wrapper = shallowMount(ListboxInput, {
propsData: {
label,
+ decription,
name,
defaultToggleText,
items,
@@ -40,6 +42,23 @@ describe('ListboxInput', () => {
});
};
+ describe('wrapper', () => {
+ it.each`
+ description | labelProp | descriptionProp | rendersGlFormGroup
+ ${'does not render'} | ${''} | ${''} | ${false}
+ ${'renders'} | ${'labelProp'} | ${''} | ${true}
+ ${'renders'} | ${''} | ${'descriptionProp'} | ${true}
+ ${'renders'} | ${'labelProp'} | ${'descriptionProp'} | ${true}
+ `(
+ "$description a GlFormGroup when label is '$labelProp' and description is '$descriptionProp'",
+ ({ labelProp, descriptionProp, rendersGlFormGroup }) => {
+ createComponent({ label: labelProp, description: descriptionProp });
+
+ expect(findGlFormGroup().exists()).toBe(rendersGlFormGroup);
+ },
+ );
+ });
+
describe('options', () => {
beforeEach(() => {
createComponent();
@@ -49,6 +68,10 @@ describe('ListboxInput', () => {
expect(findGlFormGroup().attributes('label')).toBe(label);
});
+ it('passes the decription to the form group', () => {
+ expect(findGlFormGroup().attributes('decription')).toBe(decription);
+ });
+
it('sets the input name', () => {
expect(findInput().attributes('name')).toBe(name);
});
diff --git a/spec/views/profiles/notifications/show.html.haml_spec.rb b/spec/views/profiles/notifications/show.html.haml_spec.rb
index 9cdf8124fcf..1cfd8847bf8 100644
--- a/spec/views/profiles/notifications/show.html.haml_spec.rb
+++ b/spec/views/profiles/notifications/show.html.haml_spec.rb
@@ -5,6 +5,11 @@ require 'spec_helper'
RSpec.describe 'profiles/notifications/show' do
let(:groups) { GroupsFinder.new(user).execute.page(1) }
let(:user) { create(:user) }
+ let(:option_default) { _('Use primary email (%{email})') % { email: user.email } }
+ let(:option_primary_email) { user.email }
+ let(:expected_primary_email_attr) { "[data-emails='#{[option_primary_email].to_json}']" }
+ let(:expected_default_attr) { "[data-empty-value-text='#{option_default}']" }
+ let(:expected_selector) { expected_primary_email_attr + expected_default_attr + expected_value_attr }
before do
assign(:group_notifications, [])
@@ -16,14 +21,26 @@ RSpec.describe 'profiles/notifications/show' do
end
context 'when there is no database value for User#notification_email' do
- let(:option_default) { _('Use primary email (%{email})') % { email: user.email } }
- let(:option_primary_email) { user.email }
- let(:options) { [option_default, option_primary_email] }
+ let(:expected_value_attr) { ":not([data-value])" }
it 'displays the correct elements' do
render
- expect(rendered).to have_select('user_notification_email', options: options, selected: nil)
+ expect(rendered).to have_selector(expected_selector)
+ end
+ end
+
+ context 'when there is a database value for User#notification_email' do
+ let(:expected_value_attr) { "[data-value='#{option_primary_email}']" }
+
+ before do
+ user.notification_email = option_primary_email
+ end
+
+ it 'displays the correct elements' do
+ render
+
+ expect(rendered).to have_selector(expected_selector)
end
end
end