Commit bcc3873e authored by Tim Zallmann's avatar Tim Zallmann

Merge branch 'winh-issue-board-switcher-gitlab-ui-ee' into 'master'

Convert issue board switcher to gitlab-ui dropdown components

Closes #9088 and #8855

See merge request gitlab-org/gitlab-ee!8591
parents 735da7ec ea14d2bc
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import { throttle } from 'underscore'; import { throttle } from 'underscore';
import { GlLoadingIcon, GlSearchBox } from '@gitlab/ui'; import {
GlLoadingIcon,
GlSearchBox,
GlDropdown,
GlDropdownDivider,
GlDropdownHeader,
GlDropdownItem,
} from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import boardsStore from '~/boards/stores/boards_store'; import boardsStore from '~/boards/stores/boards_store';
import BoardForm from './board_form.vue'; import BoardForm from './board_form.vue';
...@@ -15,6 +22,10 @@ export default { ...@@ -15,6 +22,10 @@ export default {
BoardForm, BoardForm,
GlLoadingIcon, GlLoadingIcon,
GlSearchBox, GlSearchBox,
GlDropdown,
GlDropdownDivider,
GlDropdownHeader,
GlDropdownItem,
}, },
props: { props: {
currentBoard: { currentBoard: {
...@@ -68,7 +79,6 @@ export default { ...@@ -68,7 +79,6 @@ export default {
}, },
data() { data() {
return { return {
open: false,
loading: true, loading: true,
hasScrollFade: false, hasScrollFade: false,
hasAssigneesListMounted: false, hasAssigneesListMounted: false,
...@@ -134,46 +144,30 @@ export default { ...@@ -134,46 +144,30 @@ export default {
$('#js-add-list').on('hide.bs.dropdown', this.handleDropdownHide); $('#js-add-list').on('hide.bs.dropdown', this.handleDropdownHide);
$('.js-new-board-list-tabs').on('click', this.handleDropdownTabClick); $('.js-new-board-list-tabs').on('click', this.handleDropdownTabClick);
}, },
mounted() {
// prevent dropdown from closing when search box is clicked
$(this.$el)
.find('.dropdown-header')
.on('click', event => event.stopPropagation());
},
methods: { methods: {
showPage(page) { showPage(page) {
this.state.reload = false; this.state.reload = false;
this.state.currentPage = page; this.state.currentPage = page;
}, },
toggleDropdown() {
this.open = !this.open;
if (this.open && !this.loading) {
this.$nextTick(this.focusSearchInput);
}
},
loadBoards(toggleDropdown = true) { loadBoards(toggleDropdown = true) {
if (toggleDropdown) { if (toggleDropdown && this.boards.length > 0) {
this.toggleDropdown(); return;
} }
if (this.open && !this.boards.length) { gl.boardService
gl.boardService .allBoards()
.allBoards() .then(res => res.data)
.then(res => res.data) .then(json => {
.then(json => { this.loading = false;
this.loading = false; this.boards = json;
this.boards = json; })
}) .then(() => this.$nextTick()) // Wait for boards list in DOM
.then(() => this.$nextTick()) // Wait for boards list in DOM .then(() => {
.then(() => { this.setScrollFade();
this.setScrollFade(); })
this.focusSearchInput(); .catch(() => {
}) this.loading = false;
.catch(() => { });
this.loading = false;
});
}
}, },
isScrolledUp() { isScrolledUp() {
const { content } = this.$refs; const { content } = this.$refs;
...@@ -214,10 +208,6 @@ export default { ...@@ -214,10 +208,6 @@ export default {
this.hasMilestoneListMounted = true; this.hasMilestoneListMounted = true;
} }
}, },
focusSearchInput() {
const { searchBox } = this.$refs;
searchBox.$el.querySelector('input').focus();
},
}, },
}; };
</script> </script>
...@@ -225,69 +215,76 @@ export default { ...@@ -225,69 +215,76 @@ export default {
<template> <template>
<div class="boards-switcher js-boards-selector append-right-10"> <div class="boards-switcher js-boards-selector append-right-10">
<span class="boards-selector-wrapper js-boards-selector-wrapper"> <span class="boards-selector-wrapper js-boards-selector-wrapper">
<div class="dropdown"> <gl-dropdown
<button toggle-class="dropdown-menu-toggle js-dropdown-toggle"
class="dropdown-menu-toggle js-dropdown-toggle" menu-class="flex-column dropdown-extended-height"
type="button" :text="board.name"
data-toggle="dropdown" @show="loadBoards"
@click="loadBoards" >
> <div>
{{ board.name }} <icon name="chevron-down" /> <div class="dropdown-title mb-0" @mousedown.prevent>
</button> {{ s__('IssueBoards|Switch board') }}
<div class="dropdown-menu" :class="{ 'is-loading': loading }">
<div class="dropdown-header position-relative">
<gl-search-box v-if="!loading" ref="searchBox" v-model="filterTerm" />
</div> </div>
</div>
<div class="dropdown-content-faded-mask js-scroll-fade" :class="scrollFadeClass"> <gl-dropdown-header class="mt-0">
<ul <gl-search-box ref="searchBox" v-model="filterTerm" />
v-if="!loading" </gl-dropdown-header>
ref="content"
class="dropdown-list js-dropdown-list"
@scroll.passive="throttledSetScrollFade"
>
<li
v-show="filteredBoards.length === 0"
class="dropdown-item no-pointer-events text-secondary"
>
{{ s__('IssueBoards|No matching boards found') }}
</li>
<li <div
v-for="otherBoard in filteredBoards" v-if="!loading"
:key="otherBoard.id" ref="content"
class="js-dropdown-item" class="dropdown-content flex-fill"
> @scroll.passive="throttledSetScrollFade"
<a :href="`${boardBaseUrl}/${otherBoard.id}`"> {{ otherBoard.name }} </a> >
</li> <gl-dropdown-item
<li v-if="hasMissingBoards" class="small unclickable"> v-show="filteredBoards.length === 0"
{{ class="no-pointer-events text-secondary"
s__( >
'IssueBoards|Some of your boards are hidden, activate a license to see them again.', {{ s__('IssueBoards|No matching boards found') }}
) </gl-dropdown-item>
}}
</li>
</ul>
</div>
<gl-loading-icon v-if="loading" class="dropdown-loading" /> <gl-dropdown-item
v-for="otherBoard in filteredBoards"
:key="otherBoard.id"
class="js-dropdown-item"
:href="`${boardBaseUrl}/${otherBoard.id}`"
>
{{ otherBoard.name }}
</gl-dropdown-item>
<gl-dropdown-item v-if="hasMissingBoards" class="small unclickable">
{{
s__(
'IssueBoards|Some of your boards are hidden, activate a license to see them again.',
)
}}
</gl-dropdown-item>
</div>
<div v-if="canAdminBoard" class="dropdown-footer"> <div
<ul class="dropdown-footer-list"> v-show="filteredBoards.length > 0"
<li v-if="multipleIssueBoardsAvailable"> class="dropdown-content-faded-mask"
<button type="button" @click.prevent="showPage('new')"> :class="scrollFadeClass"
{{ s__('IssueBoards|Create new board') }} ></div>
</button>
</li> <gl-loading-icon v-if="loading" class="dropdown-loading" />
<li v-if="showDelete">
<button type="button" class="text-danger" @click.prevent="showPage('delete')"> <div v-if="canAdminBoard">
{{ s__('IssueBoards|Delete board') }} <gl-dropdown-divider />
</button>
</li> <gl-dropdown-item v-if="multipleIssueBoardsAvailable" @click.prevent="showPage('new')">
</ul> {{ s__('IssueBoards|Create new board') }}
</div> </gl-dropdown-item>
<gl-dropdown-item
v-if="showDelete"
class="text-danger"
@click.prevent="showPage('delete')"
>
{{ s__('IssueBoards|Delete board') }}
</gl-dropdown-item>
</div> </div>
</div> </gl-dropdown>
<board-form <board-form
v-if="currentPage" v-if="currentPage"
......
...@@ -104,10 +104,10 @@ ...@@ -104,10 +104,10 @@
} }
.boards-switcher { .boards-switcher {
.dropdown-content-faded-mask .dropdown-list { .dropdown-menu.show {
// the following is no longer necessary after https://gitlab.com/gitlab-org/gitlab-ee/issues/8855 // we cannot use d-flex from Bootstrap because of !important
$search-box-height: 50px; // see https://gitlab.com/gitlab-org/gitlab-ui/issues/38
max-height: calc(#{$dropdown-max-height} - #{$search-box-height}); display: flex;
} }
// the following is a workaround until we have it fixed in gitlab-ui // the following is a workaround until we have it fixed in gitlab-ui
......
---
title: Add keyboard navigation to issue board switcher and remove duplicate scroll
bar
merge_request: 8591
author:
type: fixed
...@@ -7,6 +7,8 @@ describe 'Multiple Issue Boards', :js do ...@@ -7,6 +7,8 @@ describe 'Multiple Issue Boards', :js do
let!(:board) { create(:board, project: project) } let!(:board) { create(:board, project: project) }
let!(:board2) { create(:board, project: project) } let!(:board2) { create(:board, project: project) }
dropdown_selector = '.js-boards-selector .dropdown-menu'
context 'with multiple issue boards enabled' do context 'with multiple issue boards enabled' do
context 'authorized user' do context 'authorized user' do
before do before do
...@@ -27,7 +29,7 @@ describe 'Multiple Issue Boards', :js do ...@@ -27,7 +29,7 @@ describe 'Multiple Issue Boards', :js do
it 'shows a list of boards' do it 'shows a list of boards' do
click_button board.name click_button board.name
page.within('.dropdown-menu') do page.within(dropdown_selector) do
expect(page).to have_content(board.name) expect(page).to have_content(board.name)
expect(page).to have_content(board2.name) expect(page).to have_content(board2.name)
end end
...@@ -36,7 +38,7 @@ describe 'Multiple Issue Boards', :js do ...@@ -36,7 +38,7 @@ describe 'Multiple Issue Boards', :js do
it 'switches current board' do it 'switches current board' do
click_button board.name click_button board.name
page.within('.dropdown-menu') do page.within(dropdown_selector) do
click_link board2.name click_link board2.name
end end
...@@ -50,7 +52,7 @@ describe 'Multiple Issue Boards', :js do ...@@ -50,7 +52,7 @@ describe 'Multiple Issue Boards', :js do
it 'creates new board without detailed configuration' do it 'creates new board without detailed configuration' do
click_button board.name click_button board.name
page.within('.dropdown-menu') do page.within(dropdown_selector) do
click_button 'Create new board' click_button 'Create new board'
end end
...@@ -66,7 +68,7 @@ describe 'Multiple Issue Boards', :js do ...@@ -66,7 +68,7 @@ describe 'Multiple Issue Boards', :js do
wait_for_requests wait_for_requests
page.within('.dropdown-menu') do page.within(dropdown_selector) do
click_button 'Delete board' click_button 'Delete board'
end end
...@@ -74,7 +76,7 @@ describe 'Multiple Issue Boards', :js do ...@@ -74,7 +76,7 @@ describe 'Multiple Issue Boards', :js do
click_button 'Delete' click_button 'Delete'
click_button board2.name click_button board2.name
page.within('.dropdown-menu') do page.within(dropdown_selector) do
expect(page).not_to have_content(board.name) expect(page).not_to have_content(board.name)
expect(page).to have_content(board2.name) expect(page).to have_content(board2.name)
end end
...@@ -83,7 +85,7 @@ describe 'Multiple Issue Boards', :js do ...@@ -83,7 +85,7 @@ describe 'Multiple Issue Boards', :js do
it 'adds a list to the none default board' do it 'adds a list to the none default board' do
click_button board.name click_button board.name
page.within('.dropdown-menu') do page.within(dropdown_selector) do
click_link board2.name click_link board2.name
end end
...@@ -107,7 +109,7 @@ describe 'Multiple Issue Boards', :js do ...@@ -107,7 +109,7 @@ describe 'Multiple Issue Boards', :js do
click_button board2.name click_button board2.name
page.within('.dropdown-menu') do page.within(dropdown_selector) do
click_link board.name click_link board.name
end end
...@@ -136,7 +138,7 @@ describe 'Multiple Issue Boards', :js do ...@@ -136,7 +138,7 @@ describe 'Multiple Issue Boards', :js do
it 'does not show action links' do it 'does not show action links' do
click_button board.name click_button board.name
page.within('.dropdown-menu') do page.within(dropdown_selector) do
expect(page).not_to have_content('Create new board') expect(page).not_to have_content('Create new board')
expect(page).not_to have_content('Delete board') expect(page).not_to have_content('Delete board')
end end
...@@ -158,7 +160,7 @@ describe 'Multiple Issue Boards', :js do ...@@ -158,7 +160,7 @@ describe 'Multiple Issue Boards', :js do
click_button board.name click_button board.name
page.within('.dropdown-menu') do page.within(dropdown_selector) do
expect(page).not_to have_content('Create new board') expect(page).not_to have_content('Create new board')
expect(page).not_to have_content('Delete board') expect(page).not_to have_content('Delete board')
end end
......
import Vue from 'vue'; import Vue from 'vue';
import BoardService from 'ee/boards/services/board_service'; import BoardService from 'ee/boards/services/board_service';
import BoardsSelector from 'ee/boards/components/boards_selector.vue'; import BoardsSelector from 'ee/boards/components/boards_selector.vue';
import setTimeoutPromiseHelper from 'spec/helpers/set_timeout_promise_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { TEST_HOST } from 'spec/test_constants'; import { TEST_HOST } from 'spec/test_constants';
const throttleDuration = 1; const throttleDuration = 1;
function waitForScroll() {
return Vue.nextTick()
.then(() => setTimeoutPromiseHelper(throttleDuration))
.then(() => Vue.nextTick());
}
describe('BoardsSelector', () => { describe('BoardsSelector', () => {
let vm; let vm;
let scrollContainer;
let scrollFade;
let boardServiceResponse; let boardServiceResponse;
const boards = new Array(20).fill().map((board, id) => { const boards = new Array(20).fill().map((board, id) => {
const name = `board${id}`; const name = `board${id}`;
...@@ -75,13 +66,6 @@ describe('BoardsSelector', () => { ...@@ -75,13 +66,6 @@ describe('BoardsSelector', () => {
boardServiceResponse boardServiceResponse
.then(() => vm.$nextTick()) .then(() => vm.$nextTick())
.then(() => {
scrollFade = vm.$el.querySelector('.js-scroll-fade');
scrollContainer = scrollFade.querySelector('.js-dropdown-list');
scrollContainer.style.maxHeight = '100px';
scrollContainer.style.overflowY = 'scroll';
})
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
}); });
...@@ -91,29 +75,6 @@ describe('BoardsSelector', () => { ...@@ -91,29 +75,6 @@ describe('BoardsSelector', () => {
window.gl.boardService = undefined; window.gl.boardService = undefined;
}); });
it('shows the scroll fade if isScrolledUp', done => {
vm.scrollFadeInitialized = false;
scrollContainer.scrollTop = 0;
waitForScroll()
.then(() => {
expect(scrollFade.classList.contains('fade-out')).toEqual(false);
})
.then(done)
.catch(done.fail);
});
it('hides the scroll fade if not isScrolledUp', done => {
scrollContainer.scrollTop = scrollContainer.scrollHeight;
waitForScroll()
.then(() => {
expect(scrollFade.classList.contains('fade-out')).toEqual(true);
})
.then(done)
.catch(done.fail);
});
describe('filtering', () => { describe('filtering', () => {
const fillSearchBox = filterTerm => { const fillSearchBox = filterTerm => {
const { searchBox } = vm.$refs; const { searchBox } = vm.$refs;
...@@ -122,10 +83,15 @@ describe('BoardsSelector', () => { ...@@ -122,10 +83,15 @@ describe('BoardsSelector', () => {
searchBoxInput.dispatchEvent(new Event('input')); searchBoxInput.dispatchEvent(new Event('input'));
}; };
it('shows all boards without filtering', () => { it('shows all boards without filtering', done => {
const dropdownItemCount = vm.$el.querySelectorAll('.js-dropdown-item'); vm.$nextTick()
.then(() => {
const dropdownItemCount = vm.$el.querySelectorAll('.js-dropdown-item');
expect(dropdownItemCount.length).toBe(boards.length); expect(dropdownItemCount.length).toBe(boards.length);
})
.then(done)
.catch(done.fail);
}); });
it('shows only matching boards when filtering', done => { it('shows only matching boards when filtering', done => {
......
...@@ -5025,6 +5025,9 @@ msgstr "" ...@@ -5025,6 +5025,9 @@ msgstr ""
msgid "IssueBoards|Some of your boards are hidden, activate a license to see them again." msgid "IssueBoards|Some of your boards are hidden, activate a license to see them again."
msgstr "" msgstr ""
msgid "IssueBoards|Switch board"
msgstr ""
msgid "Issues" msgid "Issues"
msgstr "" msgstr ""
......
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