diff options
-rw-r--r-- | app/assets/javascripts/boards/components/board_form.vue | 216 | ||||
-rw-r--r-- | app/assets/javascripts/boards/components/boards_selector.vue | 334 | ||||
-rw-r--r-- | app/assets/javascripts/boards/index.js | 2 | ||||
-rw-r--r-- | app/assets/javascripts/boards/mount_multiple_boards_switcher.js | 37 | ||||
-rw-r--r-- | app/views/shared/boards/_switcher.html.haml | 16 | ||||
-rw-r--r-- | app/views/shared/issuable/_search_bar.html.haml | 2 | ||||
-rw-r--r-- | changelogs/unreleased/winh-multiple-issueboards-core.yml | 5 | ||||
-rw-r--r-- | locale/gitlab.pot | 18 | ||||
-rw-r--r-- | spec/javascripts/boards/components/board_form_spec.js | 56 | ||||
-rw-r--r-- | spec/javascripts/boards/components/boards_selector_spec.js | 205 |
10 files changed, 887 insertions, 4 deletions
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue new file mode 100644 index 00000000000..6754abf4019 --- /dev/null +++ b/app/assets/javascripts/boards/components/board_form.vue @@ -0,0 +1,216 @@ +<script> +import Flash from '~/flash'; +import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; +import { visitUrl } from '~/lib/utils/url_utility'; +import boardsStore from '~/boards/stores/boards_store'; + +const boardDefaults = { + id: false, + name: '', + labels: [], + milestone_id: undefined, + assignee: {}, + assignee_id: undefined, + weight: null, +}; + +export default { + components: { + BoardScope: () => import('ee_component/boards/components/board_scope.vue'), + DeprecatedModal, + }, + props: { + canAdminBoard: { + type: Boolean, + required: true, + }, + milestonePath: { + type: String, + required: true, + }, + labelsPath: { + type: String, + required: true, + }, + scopedIssueBoardFeatureEnabled: { + type: Boolean, + required: false, + default: false, + }, + projectId: { + type: Number, + required: false, + default: 0, + }, + groupId: { + type: Number, + required: false, + default: 0, + }, + weights: { + type: Array, + required: false, + default: () => [], + }, + enableScopedLabels: { + type: Boolean, + required: false, + default: false, + }, + scopedLabelsDocumentationLink: { + type: String, + required: false, + default: '#', + }, + }, + data() { + return { + board: { ...boardDefaults, ...this.currentBoard }, + currentBoard: boardsStore.state.currentBoard, + currentPage: boardsStore.state.currentPage, + isLoading: false, + }; + }, + computed: { + isNewForm() { + return this.currentPage === 'new'; + }, + isDeleteForm() { + return this.currentPage === 'delete'; + }, + isEditForm() { + return this.currentPage === 'edit'; + }, + isVisible() { + return this.currentPage !== ''; + }, + buttonText() { + if (this.isNewForm) { + return 'Create board'; + } + if (this.isDeleteForm) { + return 'Delete'; + } + return 'Save changes'; + }, + buttonKind() { + if (this.isNewForm) { + return 'success'; + } + if (this.isDeleteForm) { + return 'danger'; + } + return 'info'; + }, + title() { + if (this.isNewForm) { + return 'Create new board'; + } + if (this.isDeleteForm) { + return 'Delete board'; + } + if (this.readonly) { + return 'Board scope'; + } + return 'Edit board'; + }, + readonly() { + return !this.canAdminBoard; + }, + submitDisabled() { + return this.isLoading || this.board.name.length === 0; + }, + }, + mounted() { + this.resetFormState(); + if (this.$refs.name) { + this.$refs.name.focus(); + } + }, + methods: { + submit() { + if (this.board.name.length === 0) return; + this.isLoading = true; + if (this.isDeleteForm) { + gl.boardService + .deleteBoard(this.currentBoard) + .then(() => { + visitUrl(boardsStore.rootPath); + }) + .catch(() => { + Flash('Failed to delete board. Please try again.'); + this.isLoading = false; + }); + } else { + gl.boardService + .createBoard(this.board) + .then(resp => resp.data) + .then(data => { + visitUrl(data.board_path); + }) + .catch(() => { + Flash('Unable to save your changes. Please try again.'); + this.isLoading = false; + }); + } + }, + cancel() { + boardsStore.showPage(''); + }, + resetFormState() { + if (this.isNewForm) { + // Clear the form when we open the "New board" modal + this.board = { ...boardDefaults }; + } else if (this.currentBoard && Object.keys(this.currentBoard).length) { + this.board = { ...boardDefaults, ...this.currentBoard }; + } + }, + }, +}; +</script> + +<template> + <deprecated-modal + v-show="isVisible" + :hide-footer="readonly" + :title="title" + :primary-button-label="buttonText" + :kind="buttonKind" + :submit-disabled="submitDisabled" + modal-dialog-class="board-config-modal" + @cancel="cancel" + @submit="submit" + > + <template slot="body"> + <p v-if="isDeleteForm">Are you sure you want to delete this board?</p> + <form v-else class="js-board-config-modal" @submit.prevent> + <div v-if="!readonly" class="append-bottom-20"> + <label class="form-section-title label-bold" for="board-new-name"> Board name </label> + <input + id="board-new-name" + ref="name" + v-model="board.name" + class="form-control" + type="text" + placeholder="Enter board name" + @keyup.enter="submit" + /> + </div> + + <board-scope + v-if="scopedIssueBoardFeatureEnabled" + :collapse-scope="isNewForm" + :board="board" + :can-admin-board="canAdminBoard" + :milestone-path="milestonePath" + :labels-path="labelsPath" + :scoped-labels-documentation-link="scopedLabelsDocumentationLink" + :enable-scoped-labels="enableScopedLabels" + :project-id="projectId" + :group-id="groupId" + :weights="weights" + /> + </form> + </template> + </deprecated-modal> +</template> diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue new file mode 100644 index 00000000000..b05de4538f2 --- /dev/null +++ b/app/assets/javascripts/boards/components/boards_selector.vue @@ -0,0 +1,334 @@ +<script> +import { throttle } from 'underscore'; +import { + GlLoadingIcon, + GlSearchBoxByType, + GlDropdown, + GlDropdownDivider, + GlDropdownHeader, + GlDropdownItem, +} from '@gitlab/ui'; + +import Icon from '~/vue_shared/components/icon.vue'; +import httpStatusCodes from '~/lib/utils/http_status'; +import boardsStore from '../stores/boards_store'; +import BoardForm from './board_form.vue'; + +const MIN_BOARDS_TO_VIEW_RECENT = 10; + +export default { + name: 'BoardsSelector', + components: { + Icon, + BoardForm, + GlLoadingIcon, + GlSearchBoxByType, + GlDropdown, + GlDropdownDivider, + GlDropdownHeader, + GlDropdownItem, + }, + props: { + currentBoard: { + type: Object, + required: true, + }, + milestonePath: { + type: String, + required: true, + }, + throttleDuration: { + type: Number, + default: 200, + }, + boardBaseUrl: { + type: String, + required: true, + }, + hasMissingBoards: { + type: Boolean, + required: true, + }, + canAdminBoard: { + type: Boolean, + required: true, + }, + multipleIssueBoardsAvailable: { + type: Boolean, + required: true, + }, + labelsPath: { + type: String, + required: true, + }, + projectId: { + type: Number, + required: true, + }, + groupId: { + type: Number, + required: true, + }, + scopedIssueBoardFeatureEnabled: { + type: Boolean, + required: true, + }, + weights: { + type: Array, + required: true, + }, + enabledScopedLabels: { + type: Boolean, + required: false, + default: false, + }, + scopedLabelsDocumentationLink: { + type: String, + required: false, + default: '#', + }, + }, + data() { + return { + loading: true, + hasScrollFade: false, + scrollFadeInitialized: false, + boards: [], + recentBoards: [], + state: boardsStore.state, + throttledSetScrollFade: throttle(this.setScrollFade, this.throttleDuration), + contentClientHeight: 0, + maxPosition: 0, + store: boardsStore, + filterTerm: '', + }; + }, + computed: { + currentPage() { + return this.state.currentPage; + }, + filteredBoards() { + return this.boards.filter(board => + board.name.toLowerCase().includes(this.filterTerm.toLowerCase()), + ); + }, + reload: { + get() { + return this.state.reload; + }, + set(newValue) { + this.state.reload = newValue; + }, + }, + board() { + return this.state.currentBoard; + }, + showDelete() { + return this.boards.length > 1; + }, + scrollFadeClass() { + return { + 'fade-out': !this.hasScrollFade, + }; + }, + showRecentSection() { + return ( + this.recentBoards.length && + this.boards.length > MIN_BOARDS_TO_VIEW_RECENT && + !this.filterTerm.length + ); + }, + }, + watch: { + filteredBoards() { + this.scrollFadeInitialized = false; + this.$nextTick(this.setScrollFade); + }, + reload() { + if (this.reload) { + this.boards = []; + this.recentBoards = []; + this.loading = true; + this.reload = false; + + this.loadBoards(false); + } + }, + }, + created() { + boardsStore.setCurrentBoard(this.currentBoard); + }, + methods: { + showPage(page) { + boardsStore.showPage(page); + }, + loadBoards(toggleDropdown = true) { + if (toggleDropdown && this.boards.length > 0) { + return; + } + + const recentBoardsPromise = new Promise((resolve, reject) => + gl.boardService + .recentBoards() + .then(resolve) + .catch(err => { + /** + * If user is unauthorized we'd still want to resolve the + * request to display all boards. + */ + if (err.response.status === httpStatusCodes.UNAUTHORIZED) { + resolve({ data: [] }); // recent boards are empty + return; + } + reject(err); + }), + ); + + Promise.all([gl.boardService.allBoards(), recentBoardsPromise]) + .then(([allBoards, recentBoards]) => [allBoards.data, recentBoards.data]) + .then(([allBoardsJson, recentBoardsJson]) => { + this.loading = false; + this.boards = allBoardsJson; + this.recentBoards = recentBoardsJson; + }) + .then(() => this.$nextTick()) // Wait for boards list in DOM + .then(() => { + this.setScrollFade(); + }) + .catch(() => { + this.loading = false; + }); + }, + isScrolledUp() { + const { content } = this.$refs; + const currentPosition = this.contentClientHeight + content.scrollTop; + + return content && currentPosition < this.maxPosition; + }, + initScrollFade() { + this.scrollFadeInitialized = true; + + const { content } = this.$refs; + + this.contentClientHeight = content.clientHeight; + this.maxPosition = content.scrollHeight; + }, + setScrollFade() { + if (!this.scrollFadeInitialized) this.initScrollFade(); + + this.hasScrollFade = this.isScrolledUp(); + }, + }, +}; +</script> + +<template> + <div class="boards-switcher js-boards-selector append-right-10"> + <span class="boards-selector-wrapper js-boards-selector-wrapper"> + <gl-dropdown + toggle-class="dropdown-menu-toggle js-dropdown-toggle" + menu-class="flex-column dropdown-extended-height" + :text="board.name" + @show="loadBoards" + > + <div> + <div class="dropdown-title mb-0" @mousedown.prevent> + {{ s__('IssueBoards|Switch board') }} + </div> + </div> + + <gl-dropdown-header class="mt-0"> + <gl-search-box-by-type ref="searchBox" v-model="filterTerm" /> + </gl-dropdown-header> + + <div + v-if="!loading" + ref="content" + class="dropdown-content flex-fill" + @scroll.passive="throttledSetScrollFade" + > + <gl-dropdown-item + v-show="filteredBoards.length === 0" + class="no-pointer-events text-secondary" + > + {{ s__('IssueBoards|No matching boards found') }} + </gl-dropdown-item> + + <h6 v-if="showRecentSection" class="dropdown-bold-header my-0"> + {{ __('Recent') }} + </h6> + + <template v-if="showRecentSection"> + <gl-dropdown-item + v-for="recentBoard in recentBoards" + :key="`recent-${recentBoard.id}`" + class="js-dropdown-item" + :href="`${boardBaseUrl}/${recentBoard.id}`" + > + {{ recentBoard.name }} + </gl-dropdown-item> + </template> + + <hr v-if="showRecentSection" class="my-1" /> + + <h6 v-if="showRecentSection" class="dropdown-bold-header my-0"> + {{ __('All') }} + </h6> + + <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-show="filteredBoards.length > 0" + class="dropdown-content-faded-mask" + :class="scrollFadeClass" + ></div> + + <gl-loading-icon v-if="loading" /> + + <div v-if="canAdminBoard"> + <gl-dropdown-divider /> + + <gl-dropdown-item v-if="multipleIssueBoardsAvailable" @click.prevent="showPage('new')"> + {{ s__('IssueBoards|Create new board') }} + </gl-dropdown-item> + + <gl-dropdown-item + v-if="showDelete" + class="text-danger" + @click.prevent="showPage('delete')" + > + {{ s__('IssueBoards|Delete board') }} + </gl-dropdown-item> + </div> + </gl-dropdown> + + <board-form + v-if="currentPage" + :milestone-path="milestonePath" + :labels-path="labelsPath" + :project-id="projectId" + :group-id="groupId" + :can-admin-board="canAdminBoard" + :scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled" + :weights="weights" + :enable-scoped-labels="enabledScopedLabels" + :scoped-labels-documentation-link="scopedLabelsDocumentationLink" + /> + </span> + </div> +</template> diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index d6a5372b22d..f5a617a85ad 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -1,7 +1,6 @@ import $ from 'jquery'; import Vue from 'vue'; -import mountMultipleBoardsSwitcher from 'ee_else_ce/boards/mount_multiple_boards_switcher'; import Flash from '~/flash'; import { __ } from '~/locale'; import './models/label'; @@ -31,6 +30,7 @@ import { } from '~/lib/utils/common_utils'; import boardConfigToggle from 'ee_else_ce/boards/config_toggle'; import toggleFocusMode from 'ee_else_ce/boards/toggle_focus'; +import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher'; let issueBoardsApp; diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js index bdb14a7f2f2..8d22f009784 100644 --- a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js +++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js @@ -1,2 +1,35 @@ -// this will be moved from EE to CE as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/53811 -export default () => {}; +import Vue from 'vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import BoardsSelector from '~/boards/components/boards_selector.vue'; + +export default () => { + const boardsSwitcherElement = document.getElementById('js-multiple-boards-switcher'); + return new Vue({ + el: boardsSwitcherElement, + components: { + BoardsSelector, + }, + data() { + const { dataset } = boardsSwitcherElement; + + const boardsSelectorProps = { + ...dataset, + currentBoard: JSON.parse(dataset.currentBoard), + hasMissingBoards: parseBoolean(dataset.hasMissingBoards), + canAdminBoard: parseBoolean(dataset.canAdminBoard), + multipleIssueBoardsAvailable: parseBoolean(dataset.multipleIssueBoardsAvailable), + projectId: Number(dataset.projectId), + groupId: Number(dataset.groupId), + scopedIssueBoardFeatureEnabled: parseBoolean(dataset.scopedIssueBoardFeatureEnabled), + weights: JSON.parse(dataset.weights), + }; + + return { boardsSelectorProps }; + }, + render(createElement) { + return createElement(BoardsSelector, { + props: this.boardsSelectorProps, + }); + }, + }); +}; diff --git a/app/views/shared/boards/_switcher.html.haml b/app/views/shared/boards/_switcher.html.haml new file mode 100644 index 00000000000..79118630762 --- /dev/null +++ b/app/views/shared/boards/_switcher.html.haml @@ -0,0 +1,16 @@ +- parent = board.parent +- milestone_filter_opts = { format: :json } +- milestone_filter_opts = milestone_filter_opts.merge(only_group_milestones: true) if board.group_board? +- weights = Gitlab.ee? ? ([Issue::WEIGHT_ANY] + Issue.weight_options) : [] + +#js-multiple-boards-switcher.inline.boards-switcher{ data: { current_board: current_board_json.to_json, + milestone_path: milestones_filter_path(milestone_filter_opts), + board_base_url: board_base_url, + has_missing_boards: (!multiple_boards_available? && current_board_parent.boards.size > 1).to_s, + can_admin_board: can?(current_user, :admin_board, parent).to_s, + multiple_issue_boards_available: parent.multiple_issue_boards_available?.to_s, + labels_path: labels_filter_path_with_defaults(only_group_labels: true, include_descendant_groups: true), + project_id: @project&.id, + group_id: @group&.id, + scoped_issue_board_feature_enabled: Gitlab.ee? && parent.feature_available?(:scoped_issue_board) ? 'true' : 'false', + weights: weights.to_json } } diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index a97ac5e2a2d..e253413929a 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -6,7 +6,7 @@ .issues-filters{ class: ("w-100" if type == :boards_modal) } .issues-details-filters.filtered-search-block.d-flex.flex-column.flex-md-row{ class: block_css_class, "v-pre" => type == :boards_modal } - if type == :boards - = render_if_exists "shared/boards/switcher", board: board + = render "shared/boards/switcher", board: board = form_tag page_filter_path, method: :get, class: 'filter-form js-filter-form w-100' do - if params[:search].present? = hidden_field_tag :search, params[:search] diff --git a/changelogs/unreleased/winh-multiple-issueboards-core.yml b/changelogs/unreleased/winh-multiple-issueboards-core.yml new file mode 100644 index 00000000000..c45e420c133 --- /dev/null +++ b/changelogs/unreleased/winh-multiple-issueboards-core.yml @@ -0,0 +1,5 @@ +--- +title: Move multiple issue boards to core +merge_request: 30503 +author: +type: changed diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 5e8d1ac206a..7b742660a4c 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -5717,6 +5717,21 @@ msgstr "" msgid "IssueBoards|Boards" msgstr "" +msgid "IssueBoards|Create new board" +msgstr "" + +msgid "IssueBoards|Delete board" +msgstr "" + +msgid "IssueBoards|No matching boards found" +msgstr "" + +msgid "IssueBoards|Some of your boards are hidden, activate a license to see them again." +msgstr "" + +msgid "IssueBoards|Switch board" +msgstr "" + msgid "IssueTracker|Bugzilla issue tracker" msgstr "" @@ -8698,6 +8713,9 @@ msgstr "" msgid "Receive notifications about your own activity" msgstr "" +msgid "Recent" +msgstr "" + msgid "Recent Project Activity" msgstr "" diff --git a/spec/javascripts/boards/components/board_form_spec.js b/spec/javascripts/boards/components/board_form_spec.js new file mode 100644 index 00000000000..e9014156a98 --- /dev/null +++ b/spec/javascripts/boards/components/board_form_spec.js @@ -0,0 +1,56 @@ +import $ from 'jquery'; +import Vue from 'vue'; +import boardsStore from '~/boards/stores/boards_store'; +import boardForm from '~/boards/components/board_form.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; + +describe('board_form.vue', () => { + const props = { + canAdminBoard: false, + labelsPath: `${gl.TEST_HOST}/labels/path`, + milestonePath: `${gl.TEST_HOST}/milestone/path`, + }; + let vm; + + beforeEach(() => { + spyOn($, 'ajax'); + boardsStore.state.currentPage = 'edit'; + const Component = Vue.extend(boardForm); + vm = mountComponent(Component, props); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('methods', () => { + describe('cancel', () => { + it('resets currentPage', done => { + vm.cancel(); + + Vue.nextTick() + .then(() => { + expect(boardsStore.state.currentPage).toBe(''); + }) + .then(done) + .catch(done.fail); + }); + }); + }); + + describe('buttons', () => { + it('cancel button triggers cancel()', done => { + spyOn(vm, 'cancel'); + + Vue.nextTick() + .then(() => { + const cancelButton = vm.$el.querySelector('button[data-dismiss="modal"]'); + cancelButton.click(); + + expect(vm.cancel).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/boards/components/boards_selector_spec.js b/spec/javascripts/boards/components/boards_selector_spec.js new file mode 100644 index 00000000000..504bc51778c --- /dev/null +++ b/spec/javascripts/boards/components/boards_selector_spec.js @@ -0,0 +1,205 @@ +import Vue from 'vue'; +import BoardService from '~/boards/services/board_service'; +import BoardsSelector from '~/boards/components/boards_selector.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { TEST_HOST } from 'spec/test_constants'; +import boardsStore from '~/boards/stores/boards_store'; + +const throttleDuration = 1; + +function boardGenerator(n) { + return new Array(n).fill().map((board, id) => { + const name = `board${id}`; + + return { + id, + name, + }; + }); +} + +describe('BoardsSelector', () => { + let vm; + let allBoardsResponse; + let recentBoardsResponse; + let fillSearchBox; + const boards = boardGenerator(20); + const recentBoards = boardGenerator(5); + + beforeEach(done => { + setFixtures('<div class="js-boards-selector"></div>'); + window.gl = window.gl || {}; + + boardsStore.setEndpoints({ + boardsEndpoint: '', + recentBoardsEndpoint: '', + listsEndpoint: '', + bulkUpdatePath: '', + boardId: '', + }); + window.gl.boardService = new BoardService(); + + allBoardsResponse = Promise.resolve({ + data: boards, + }); + recentBoardsResponse = Promise.resolve({ + data: recentBoards, + }); + + spyOn(BoardService.prototype, 'allBoards').and.returnValue(allBoardsResponse); + spyOn(BoardService.prototype, 'recentBoards').and.returnValue(recentBoardsResponse); + + const Component = Vue.extend(BoardsSelector); + vm = mountComponent( + Component, + { + throttleDuration, + currentBoard: { + id: 1, + name: 'Development', + milestone_id: null, + weight: null, + assignee_id: null, + labels: [], + }, + milestonePath: `${TEST_HOST}/milestone/path`, + boardBaseUrl: `${TEST_HOST}/board/base/url`, + hasMissingBoards: false, + canAdminBoard: true, + multipleIssueBoardsAvailable: true, + labelsPath: `${TEST_HOST}/labels/path`, + projectId: 42, + groupId: 19, + scopedIssueBoardFeatureEnabled: true, + weights: [], + }, + document.querySelector('.js-boards-selector'), + ); + + vm.$el.querySelector('.js-dropdown-toggle').click(); + + Promise.all([allBoardsResponse, recentBoardsResponse]) + .then(() => vm.$nextTick()) + .then(done) + .catch(done.fail); + + fillSearchBox = filterTerm => { + const { searchBox } = vm.$refs; + const searchBoxInput = searchBox.$el.querySelector('input'); + searchBoxInput.value = filterTerm; + searchBoxInput.dispatchEvent(new Event('input')); + }; + }); + + afterEach(() => { + vm.$destroy(); + window.gl.boardService = undefined; + }); + + describe('filtering', () => { + it('shows all boards without filtering', done => { + vm.$nextTick() + .then(() => { + const dropdownItem = vm.$el.querySelectorAll('.js-dropdown-item'); + + expect(dropdownItem.length).toBe(boards.length + recentBoards.length); + }) + .then(done) + .catch(done.fail); + }); + + it('shows only matching boards when filtering', done => { + const filterTerm = 'board1'; + const expectedCount = boards.filter(board => board.name.includes(filterTerm)).length; + + fillSearchBox(filterTerm); + + vm.$nextTick() + .then(() => { + const dropdownItems = vm.$el.querySelectorAll('.js-dropdown-item'); + + expect(dropdownItems.length).toBe(expectedCount); + }) + .then(done) + .catch(done.fail); + }); + + it('shows message if there are no matching boards', done => { + fillSearchBox('does not exist'); + + vm.$nextTick() + .then(() => { + const dropdownItems = vm.$el.querySelectorAll('.js-dropdown-item'); + + expect(dropdownItems.length).toBe(0); + expect(vm.$el).toContainText('No matching boards found'); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('recent boards section', () => { + it('shows only when boards are greater than 10', done => { + vm.$nextTick() + .then(() => { + const headerEls = vm.$el.querySelectorAll('.dropdown-bold-header'); + + const expectedCount = 2; // Recent + All + + expect(expectedCount).toBe(headerEls.length); + }) + .then(done) + .catch(done.fail); + }); + + it('does not show when boards are less than 10', done => { + spyOn(vm, 'initScrollFade'); + spyOn(vm, 'setScrollFade'); + + vm.$nextTick() + .then(() => { + vm.boards = vm.boards.slice(0, 5); + }) + .then(vm.$nextTick) + .then(() => { + const headerEls = vm.$el.querySelectorAll('.dropdown-bold-header'); + const expectedCount = 0; + + expect(expectedCount).toBe(headerEls.length); + }) + .then(done) + .catch(done.fail); + }); + + it('does not show when recentBoards api returns empty array', done => { + vm.$nextTick() + .then(() => { + vm.recentBoards = []; + }) + .then(vm.$nextTick) + .then(() => { + const headerEls = vm.$el.querySelectorAll('.dropdown-bold-header'); + const expectedCount = 0; + + expect(expectedCount).toBe(headerEls.length); + }) + .then(done) + .catch(done.fail); + }); + + it('does not show when search is active', done => { + fillSearchBox('Random string'); + + vm.$nextTick() + .then(() => { + const headerEls = vm.$el.querySelectorAll('.dropdown-bold-header'); + const expectedCount = 0; + + expect(expectedCount).toBe(headerEls.length); + }) + .then(done) + .catch(done.fail); + }); + }); +}); |