Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-12-30 09:08:38 +00:00
parent 142658ee28
commit 8ca437b794
10 changed files with 243 additions and 13 deletions

View File

@ -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>

View File

@ -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;

View File

@ -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);

View File

@ -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>

View File

@ -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

View File

@ -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 } }

View File

@ -18829,6 +18829,9 @@ msgstr ""
msgid "Global Shortcuts"
msgstr ""
msgid "Global notification email"
msgstr ""
msgid "Global notification level"
msgstr ""

View File

@ -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();
});
});
});

View File

@ -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);
});

View File

@ -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