Commit dcbdc9d0 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch...

Merge branch '284704-refactor-jquery-dropdown-implementation-to-gitlab-ui-gldropdown-in-namespace_select-js' into 'master'

Resolve "Refactor jQuery dropdown implementation to GitLab UI GlDropdown in namespace_select.js"

See merge request gitlab-org/gitlab!60677
parents 55d7a125 79a320b7
import $ from 'jquery';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { parseBoolean } from '~/lib/utils/common_utils';
import Api from './api';
import { mergeUrlParams } from './lib/utils/url_utility';
import { __ } from './locale';
export default class NamespaceSelect {
constructor(opts) {
const isFilter = parseBoolean(opts.dropdown.dataset.isFilter);
const fieldName = opts.dropdown.dataset.fieldName || 'namespace_id';
initDeprecatedJQueryDropdown($(opts.dropdown), {
filterable: true,
selectable: true,
filterRemote: true,
search: {
fields: ['path'],
},
fieldName,
toggleLabel(selected) {
if (selected.id == null) {
return selected.text;
}
return `${selected.kind}: ${selected.full_path}`;
},
data(term, dataCallback) {
return Api.namespaces(term, (namespaces) => {
if (isFilter) {
const anyNamespace = {
text: __('Any namespace'),
id: null,
};
namespaces.unshift(anyNamespace);
namespaces.splice(1, 0, { type: 'divider' });
}
return dataCallback(namespaces);
});
},
text(namespace) {
if (namespace.id == null) {
return namespace.text;
}
return `${namespace.kind}: ${namespace.full_path}`;
},
renderRow: this.renderRow,
clicked(options) {
if (!isFilter) {
const { e } = options;
e.preventDefault();
}
},
url(namespace) {
return mergeUrlParams({ [fieldName]: namespace.id }, window.location.href);
},
});
}
}
<script>
import {
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlSearchBoxByType,
GlLoadingIcon,
} from '@gitlab/ui';
import Api from '~/api';
import { __ } from '~/locale';
export default {
i18n: {
dropdownHeader: __('Namespaces'),
searchPlaceholder: __('Search for Namespace'),
anyNamespace: __('Any namespace'),
},
components: {
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlLoadingIcon,
GlSearchBoxByType,
},
props: {
showAny: {
type: Boolean,
required: false,
default: false,
},
placeholder: {
type: String,
required: false,
default: __('Namespace'),
},
fieldName: {
type: String,
required: false,
default: null,
},
},
data() {
return {
namespaceOptions: [],
selectedNamespaceId: null,
selectedNamespace: null,
searchTerm: '',
isLoading: false,
};
},
computed: {
selectedNamespaceName() {
if (this.selectedNamespaceId === null) {
return this.placeholder;
}
return this.selectedNamespace;
},
},
watch: {
searchTerm() {
this.fetchNamespaces(this.searchTerm);
},
},
mounted() {
this.fetchNamespaces();
},
methods: {
fetchNamespaces(filter) {
this.isLoading = true;
this.namespaceOptions = [];
return Api.namespaces(filter, (namespaces) => {
this.namespaceOptions = namespaces;
this.isLoading = false;
});
},
selectNamespace(key) {
this.selectedNamespaceId = this.namespaceOptions[key].id;
this.selectedNamespace = this.getNamespaceString(this.namespaceOptions[key]);
this.$emit('setNamespace', this.selectedNamespaceId);
},
selectAnyNamespace() {
this.selectedNamespaceId = null;
this.selectedNamespace = null;
this.$emit('setNamespace', null);
},
getNamespaceString(namespace) {
return `${namespace.kind}: ${namespace.full_path}`;
},
},
};
</script>
<template>
<div class="gl-display-flex">
<input
v-if="fieldName"
:name="fieldName"
:value="selectedNamespaceId"
type="hidden"
data-testid="hidden-input"
/>
<gl-dropdown
:text="selectedNamespaceName"
:header-text="$options.i18n.dropdownHeader"
toggle-class="dropdown-menu-toggle large"
data-testid="namespace-dropdown"
:right="true"
>
<template #header>
<gl-search-box-by-type
v-model.trim="searchTerm"
class="namespace-search-box"
debounce="250"
:placeholder="$options.i18n.searchPlaceholder"
/>
</template>
<template v-if="showAny">
<gl-dropdown-item @click="selectAnyNamespace">
{{ $options.i18n.anyNamespace }}
</gl-dropdown-item>
<gl-dropdown-divider />
</template>
<gl-loading-icon v-if="isLoading" />
<gl-dropdown-item
v-for="(namespace, key) in namespaceOptions"
:key="namespace.id"
@click="selectNamespace(key)"
>
{{ getNamespaceString(namespace) }}
</gl-dropdown-item>
</gl-dropdown>
</div>
</template>
<style scoped>
/* workaround position: relative imposed by .top-area .nav-controls */
.namespace-search-box >>> input {
position: static;
}
</style>
import NamespaceSelect from '~/namespace_select'; import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import ProjectsList from '~/projects_list'; import ProjectsList from '~/projects_list';
import NamespaceSelect from './components/namespace_select.vue';
new ProjectsList(); // eslint-disable-line no-new new ProjectsList(); // eslint-disable-line no-new
document function mountNamespaceSelect() {
.querySelectorAll('.js-namespace-select') const el = document.querySelector('.js-namespace-select');
.forEach((dropdown) => new NamespaceSelect({ dropdown })); if (!el) {
return false;
}
const { showAny, fieldName, placeholder, updateLocation } = el.dataset;
return new Vue({
el,
render(createComponent) {
return createComponent(NamespaceSelect, {
props: {
showAny: parseBoolean(showAny),
fieldName,
placeholder,
},
on: {
setNamespace(newNamespace) {
if (fieldName && updateLocation) {
window.location = mergeUrlParams({ [fieldName]: newNamespace }, window.location.href);
}
},
},
});
},
});
}
mountNamespaceSelect();
...@@ -11,18 +11,14 @@ ...@@ -11,18 +11,14 @@
.nav-controls .nav-controls
.search-holder .search-holder
= render 'shared/projects/search_form', autofocus: true, admin_view: true = render 'shared/projects/search_form', autofocus: true, admin_view: true
.dropdown - current_namespace = _('Namespace')
- toggle_text = _('Namespace') - if params[:namespace_id].present?
- if params[:namespace_id].present? - namespace = Namespace.find(params[:namespace_id])
= hidden_field_tag :namespace_id, params[:namespace_id] - current_namespace = "#{namespace.kind}: #{namespace.full_path}"
- namespace = Namespace.find(params[:namespace_id]) %button.dropdown-menu-toggle.btn.btn-default.btn-md.gl-button.js-namespace-select{ data: { show_any: 'true', field_name: 'namespace_id', placeholder: current_namespace, update_location: 'true' }, type: 'button' }
- toggle_text = "#{namespace.kind}: #{namespace.full_path}" %span.gl-new-dropdown-button-text
= dropdown_toggle(toggle_text, { toggle: 'dropdown', is_filter: 'true' }, { toggle_class: 'js-namespace-select large' }) = current_namespace
.dropdown-menu.dropdown-select.dropdown-menu-right
= dropdown_title(_('Namespaces'))
= dropdown_filter(_("Search for Namespace"))
= dropdown_content
= dropdown_loading
= render 'shared/projects/dropdown' = render 'shared/projects/dropdown'
= link_to new_project_path, class: 'gl-button btn btn-confirm' do = link_to new_project_path, class: 'gl-button btn btn-confirm' do
= _('New Project') = _('New Project')
......
...@@ -143,13 +143,10 @@ ...@@ -143,13 +143,10 @@
.col-sm-3.col-form-label .col-sm-3.col-form-label
= f.label :new_namespace_id, _("Namespace") = f.label :new_namespace_id, _("Namespace")
.col-sm-9 .col-sm-9
.dropdown - placeholder = _('Search for Namespace')
= dropdown_toggle(_('Search for Namespace'), { toggle: 'dropdown', field_name: 'new_namespace_id' }, { toggle_class: 'js-namespace-select large' }) %button.dropdown-menu-toggle.btn.btn-default.btn-md.gl-button.js-namespace-select{ data: { field_name: 'new_namespace_id', placeholder: placeholder }, type: 'button' }
.dropdown-menu.dropdown-select %span.gl-new-dropdown-button-text
= dropdown_title(_('Namespaces')) = placeholder
= dropdown_filter(_('Search for Namespace'))
= dropdown_content
= dropdown_loading
.form-group.row .form-group.row
.offset-sm-3.col-sm-9 .offset-sm-3.col-sm-9
......
...@@ -96,7 +96,7 @@ RSpec.describe "Admin::Projects" do ...@@ -96,7 +96,7 @@ RSpec.describe "Admin::Projects" do
visit admin_project_path(project) visit admin_project_path(project)
click_button 'Search for Namespace' click_button 'Search for Namespace'
click_link 'group: web' click_button 'group: web'
click_button 'Transfer' click_button 'Transfer'
expect(page).to have_content("Web / #{project.name}") expect(page).to have_content("Web / #{project.name}")
......
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import NamespaceSelect from '~/namespace_select';
jest.mock('~/deprecated_jquery_dropdown');
describe('NamespaceSelect', () => {
it('initializes deprecatedJQueryDropdown', () => {
const dropdown = document.createElement('div');
// eslint-disable-next-line no-new
new NamespaceSelect({ dropdown });
expect(initDeprecatedJQueryDropdown).toHaveBeenCalled();
});
describe('as input', () => {
let deprecatedJQueryDropdownOptions;
beforeEach(() => {
const dropdown = document.createElement('div');
// eslint-disable-next-line no-new
new NamespaceSelect({ dropdown });
[[, deprecatedJQueryDropdownOptions]] = initDeprecatedJQueryDropdown.mock.calls;
});
it('prevents click events', () => {
const dummyEvent = new Event('dummy');
jest.spyOn(dummyEvent, 'preventDefault').mockImplementation(() => {});
// expect(foo).toContain('test');
deprecatedJQueryDropdownOptions.clicked({ e: dummyEvent });
expect(dummyEvent.preventDefault).toHaveBeenCalled();
});
});
describe('as filter', () => {
let deprecatedJQueryDropdownOptions;
beforeEach(() => {
const dropdown = document.createElement('div');
dropdown.dataset.isFilter = 'true';
// eslint-disable-next-line no-new
new NamespaceSelect({ dropdown });
[[, deprecatedJQueryDropdownOptions]] = initDeprecatedJQueryDropdown.mock.calls;
});
it('does not prevent click events', () => {
const dummyEvent = new Event('dummy');
jest.spyOn(dummyEvent, 'preventDefault').mockImplementation(() => {});
deprecatedJQueryDropdownOptions.clicked({ e: dummyEvent });
expect(dummyEvent.preventDefault).not.toHaveBeenCalled();
});
it('sets URL of dropdown items', () => {
const dummyNamespace = { id: 'eal' };
const itemUrl = deprecatedJQueryDropdownOptions.url(dummyNamespace);
expect(itemUrl).toContain(`namespace_id=${dummyNamespace.id}`);
});
});
});
import { mount } from '@vue/test-utils';
import Api from '~/api';
import NamespaceSelect from '~/pages/admin/projects/components/namespace_select.vue';
describe('Dropdown select component', () => {
let wrapper;
const mountDropdown = (propsData) => {
wrapper = mount(NamespaceSelect, { propsData });
};
const findDropdownToggle = () => wrapper.find('button.dropdown-toggle');
const findNamespaceInput = () => wrapper.find('[data-testid="hidden-input"]');
const findFilterInput = () => wrapper.find('.namespace-search-box input');
const findDropdownOption = (match) => {
const buttons = wrapper
.findAll('button.dropdown-item')
.filter((node) => node.text().match(match));
return buttons.length ? buttons.at(0) : buttons;
};
const setFieldValue = async (field, value) => {
await field.setValue(value);
field.trigger('blur');
};
beforeEach(() => {
setFixtures('<div class="test-container"></div>');
jest.spyOn(Api, 'namespaces').mockImplementation((_, callback) =>
callback([
{ id: 10, kind: 'user', full_path: 'Administrator' },
{ id: 20, kind: 'group', full_path: 'GitLab Org' },
]),
);
});
it('creates a hidden input if fieldName is provided', () => {
mountDropdown({ fieldName: 'namespace-input' });
expect(findNamespaceInput()).toExist();
expect(findNamespaceInput().attributes('name')).toBe('namespace-input');
});
describe('clicking dropdown options', () => {
it('retrieves namespaces based on filter query', async () => {
mountDropdown();
await setFieldValue(findFilterInput(), 'test');
expect(Api.namespaces).toHaveBeenCalledWith('test', expect.anything());
});
it('updates the dropdown value based upon selection', async () => {
mountDropdown({ fieldName: 'namespace-input' });
// wait for dropdown options to populate
await wrapper.vm.$nextTick();
expect(findDropdownOption('user: Administrator')).toExist();
expect(findDropdownOption('group: GitLab Org')).toExist();
expect(findDropdownOption('group: Foobar')).not.toExist();
findDropdownOption('user: Administrator').trigger('click');
await wrapper.vm.$nextTick();
expect(findNamespaceInput().attributes('value')).toBe('10');
expect(findDropdownToggle().text()).toBe('user: Administrator');
});
it('triggers a setNamespace event upon selection', async () => {
mountDropdown();
// wait for dropdown options to populate
await wrapper.vm.$nextTick();
findDropdownOption('group: GitLab Org').trigger('click');
expect(wrapper.emitted('setNamespace')).toHaveLength(1);
expect(wrapper.emitted('setNamespace')[0][0]).toBe(20);
});
it('displays "Any Namespace" option when showAny prop provided', () => {
mountDropdown({ showAny: true });
expect(wrapper.text()).toContain('Any namespace');
});
it('does not display "Any Namespace" option when showAny prop not provided', () => {
mountDropdown();
expect(wrapper.text()).not.toContain('Any namespace');
});
});
});
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