diff options
-rw-r--r-- | app/assets/javascripts/boards/components/board.js | 4 | ||||
-rw-r--r-- | app/assets/javascripts/boards/components/board_list.js | 286 | ||||
-rw-r--r-- | app/assets/javascripts/boards/components/board_new_issue.js | 4 | ||||
-rw-r--r-- | app/views/projects/boards/_show.html.haml | 1 | ||||
-rw-r--r-- | app/views/projects/boards/components/_board_list.html.haml | 26 | ||||
-rw-r--r-- | spec/javascripts/boards/board_list_spec.js | 201 |
6 files changed, 382 insertions, 140 deletions
diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index 35b3205cca7..93b8960da2e 100644 --- a/app/assets/javascripts/boards/components/board.js +++ b/app/assets/javascripts/boards/components/board.js @@ -1,7 +1,7 @@ /* eslint-disable comma-dangle, space-before-function-paren, one-var */ /* global Sortable */ - import Vue from 'vue'; +import boardList from './board_list'; import boardBlankState from './board_blank_state'; require('./board_delete'); @@ -16,7 +16,7 @@ require('./board_list'); gl.issueBoards.Board = Vue.extend({ template: '#js-board-template', components: { - 'board-list': gl.issueBoards.BoardList, + boardList, 'board-delete': gl.issueBoards.BoardDelete, boardBlankState, }, diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js index 86e6c26e570..adbd82cb687 100644 --- a/app/assets/javascripts/boards/components/board_list.js +++ b/app/assets/javascripts/boards/components/board_list.js @@ -1,131 +1,197 @@ -/* eslint-disable comma-dangle, space-before-function-paren, max-len */ /* global Sortable */ - -import Vue from 'vue'; import boardNewIssue from './board_new_issue'; import boardCard from './board_card'; +import eventHub from '../eventhub'; -(() => { - const Store = gl.issueBoards.BoardsStore; - - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; +const Store = gl.issueBoards.BoardsStore; - gl.issueBoards.BoardList = Vue.extend({ - template: '#js-board-list-template', - components: { - boardCard, - boardNewIssue, +export default { + name: 'BoardList', + props: { + disabled: { + type: Boolean, + required: true, }, - props: { - disabled: Boolean, - list: Object, - issues: Array, - loading: Boolean, - issueLinkBase: String, - rootPath: String, + list: { + type: Object, + required: true, }, - data () { - return { - scrollOffset: 250, - filters: Store.state.filters, - showCount: false, - showIssueForm: false - }; + issues: { + type: Array, + required: true, }, - watch: { - filters: { - handler () { - this.list.loadingMore = false; - this.$refs.list.scrollTop = 0; - }, - deep: true - }, - issues () { - this.$nextTick(() => { - if (this.scrollHeight() <= this.listHeight() && this.list.issuesSize > this.list.issues.length) { - this.list.page += 1; - this.list.getIssues(false); - } + loading: { + type: Boolean, + required: true, + }, + issueLinkBase: { + type: String, + required: true, + }, + rootPath: { + type: String, + required: true, + }, + }, + data() { + return { + scrollOffset: 250, + filters: Store.state.filters, + showCount: false, + showIssueForm: false, + }; + }, + components: { + boardCard, + boardNewIssue, + }, + methods: { + listHeight() { + return this.$refs.list.getBoundingClientRect().height; + }, + scrollHeight() { + return this.$refs.list.scrollHeight; + }, + scrollTop() { + return this.$refs.list.scrollTop + this.listHeight(); + }, + loadNextPage() { + const getIssues = this.list.nextPage(); - if (this.scrollHeight() > Math.ceil(this.listHeight())) { - this.showCount = true; - } else { - this.showCount = false; - } + if (getIssues) { + this.list.loadingMore = true; + getIssues.then(() => { + this.list.loadingMore = false; }); } }, - methods: { - listHeight () { - return this.$refs.list.getBoundingClientRect().height; - }, - scrollHeight () { - return this.$refs.list.scrollHeight; - }, - scrollTop () { - return this.$refs.list.scrollTop + this.listHeight(); + toggleForm() { + this.showIssueForm = !this.showIssueForm; + }, + onScroll() { + if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) { + this.loadNextPage(); + } + }, + }, + watch: { + filters: { + handler() { + this.list.loadingMore = false; + this.$refs.list.scrollTop = 0; }, - loadNextPage () { - const getIssues = this.list.nextPage(); + deep: true, + }, + issues() { + this.$nextTick(() => { + if (this.scrollHeight() <= this.listHeight() && + this.list.issuesSize > this.list.issues.length) { + this.list.page += 1; + this.list.getIssues(false); + } - if (getIssues) { - this.list.loadingMore = true; - getIssues.then(() => { - this.list.loadingMore = false; - }); + if (this.scrollHeight() > Math.ceil(this.listHeight())) { + this.showCount = true; + } else { + this.showCount = false; } - }, - toggleForm() { - this.showIssueForm = !this.showIssueForm; - }, - }, - created() { - gl.IssueBoardsApp.$on(`hide-issue-form-${this.list.id}`, this.toggleForm); + }); }, - mounted () { - const options = gl.issueBoards.getBoardSortableDefaultOptions({ - scroll: document.querySelectorAll('.boards-list')[0], - group: 'issues', - disabled: this.disabled, - filter: '.board-list-count, .is-disabled', - dataIdAttr: 'data-issue-id', - onStart: (e) => { - const card = this.$refs.issue[e.oldIndex]; + }, + created() { + eventHub.$on(`hide-issue-form-${this.list.id}`, this.toggleForm); + }, + mounted() { + const options = gl.issueBoards.getBoardSortableDefaultOptions({ + scroll: document.querySelectorAll('.boards-list')[0], + group: 'issues', + disabled: this.disabled, + filter: '.board-list-count, .is-disabled', + dataIdAttr: 'data-issue-id', + onStart: (e) => { + const card = this.$refs.issue[e.oldIndex]; - card.showDetail = false; - Store.moving.list = card.list; - Store.moving.issue = Store.moving.list.findIssue(+e.item.dataset.issueId); + card.showDetail = false; + Store.moving.list = card.list; + Store.moving.issue = Store.moving.list.findIssue(+e.item.dataset.issueId); - gl.issueBoards.onStart(); - }, - onAdd: (e) => { - gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex); + gl.issueBoards.onStart(); + }, + onAdd: (e) => { + gl.issueBoards.BoardsStore + .moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex); - this.$nextTick(() => { - e.item.remove(); - }); - }, - onUpdate: (e) => { - const sortedArray = this.sortable.toArray().filter(id => id !== '-1'); - gl.issueBoards.BoardsStore.moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray); - }, - onMove(e) { - return !e.related.classList.contains('board-list-count'); - } - }); + this.$nextTick(() => { + e.item.remove(); + }); + }, + onUpdate: (e) => { + const sortedArray = this.sortable.toArray().filter(id => id !== '-1'); + gl.issueBoards.BoardsStore + .moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray); + }, + onMove(e) { + return !e.related.classList.contains('board-list-count'); + }, + }); - this.sortable = Sortable.create(this.$refs.list, options); + this.sortable = Sortable.create(this.$refs.list, options); - // Scroll event on list to load more - this.$refs.list.onscroll = () => { - if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) { - this.loadNextPage(); - } - }; - }, - beforeDestroy() { - gl.IssueBoardsApp.$off(`hide-issue-form-${this.list.id}`, this.toggleForm); - }, - }); -})(); + // Scroll event on list to load more + this.$refs.list.addEventListener('scroll', this.onScroll); + }, + beforeDestroy() { + eventHub.$off(`hide-issue-form-${this.list.id}`, this.toggleForm); + this.$refs.list.removeEventListener('scroll', this.onScroll); + }, + template: ` + <div class="board-list-component"> + <div + class="board-list-loading text-center" + aria-label="Loading issues" + v-if="loading"> + <i + class="fa fa-spinner fa-spin" + aria-hidden="true"> + </i> + </div> + <board-new-issue + :list="list" + v-if="list.type !== 'closed' && showIssueForm"/> + <ul + class="board-list" + v-show="!loading" + ref="list" + :data-board="list.id" + :class="{ 'is-smaller': showIssueForm }"> + <board-card + v-for="(issue, index) in issues" + ref="issue" + :index="index" + :list="list" + :issue="issue" + :issue-link-base="issueLinkBase" + :root-path="rootPath" + :disabled="disabled" + :key="issue.id" /> + <li + class="board-list-count text-center" + v-if="showCount" + data-id="-1"> + <i + class="fa fa-spinner fa-spin" + aria-label="Loading more issues" + aria-hidden="true" + v-show="list.loadingMore"> + </i> + <span v-if="list.issues.length === list.issuesSize"> + Showing all issues + </span> + <span v-else> + Showing {{ list.issues.length }} of {{ list.issuesSize }} issues + </span> + </li> + </ul> + </div> + `, +}; diff --git a/app/assets/javascripts/boards/components/board_new_issue.js b/app/assets/javascripts/boards/components/board_new_issue.js index b88f59dd6d4..0fa85b6fe14 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.js +++ b/app/assets/javascripts/boards/components/board_new_issue.js @@ -1,4 +1,6 @@ /* global ListIssue */ +import eventHub from '../eventhub'; + const Store = gl.issueBoards.BoardsStore; export default { @@ -49,7 +51,7 @@ export default { }, cancel() { this.title = ''; - gl.IssueBoardsApp.$emit(`hide-issue-form-${this.list.id}`); + eventHub.$emit(`hide-issue-form-${this.list.id}`); }, }, mounted() { diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml index d93eac63bd3..7ca0ec8ed2b 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/projects/boards/_show.html.haml @@ -8,7 +8,6 @@ = page_specific_javascript_bundle_tag('boards') %script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board" - %script#js-board-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list" %script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal = render "projects/issues/head" diff --git a/app/views/projects/boards/components/_board_list.html.haml b/app/views/projects/boards/components/_board_list.html.haml deleted file mode 100644 index 4a0b2110601..00000000000 --- a/app/views/projects/boards/components/_board_list.html.haml +++ /dev/null @@ -1,26 +0,0 @@ -.board-list-component - .board-list-loading.text-center{ "v-if" => "loading" } - = icon("spinner spin") - - if can? current_user, :create_issue, @project - %board-new-issue{ ":list" => "list", - "v-if" => 'list.type !== "closed" && showIssueForm' } - %ul.board-list{ "ref" => "list", - "v-show" => "!loading", - ":data-board" => "list.id", - ":class" => '{ "is-smaller": showIssueForm }' } - %board-card{ "v-for" => "(issue, index) in issues", - "ref" => "issue", - ":index" => "index", - ":list" => "list", - ":issue" => "issue", - ":issue-link-base" => "issueLinkBase", - ":root-path" => "rootPath", - ":disabled" => "disabled", - ":key" => "issue.id" } - %li.board-list-count.text-center{ "v-if" => "showCount", - "data-issue-id" => "-1" } - = icon("spinner spin", "v-show" => "list.loadingMore" ) - %span{ "v-if" => "list.issues.length === list.issuesSize" } - Showing all issues - %span{ "v-else" => true } - Showing {{ list.issues.length }} of {{ list.issuesSize }} issues diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js new file mode 100644 index 00000000000..3f598887603 --- /dev/null +++ b/spec/javascripts/boards/board_list_spec.js @@ -0,0 +1,201 @@ +/* global BoardService */ +/* global boardsMockInterceptor */ +/* global List */ +/* global listObj */ +/* global ListIssue */ +import Vue from 'vue'; +import _ from 'underscore'; +import Sortable from 'vendor/Sortable'; +import BoardList from '~/boards/components/board_list'; +import eventHub from '~/boards/eventhub'; +import '~/boards/mixins/sortable_default_options'; +import '~/boards/models/issue'; +import '~/boards/models/list'; +import '~/boards/stores/boards_store'; +import './mock_data'; + +window.Sortable = Sortable; + +describe('Board list component', () => { + let component; + + beforeEach((done) => { + const el = document.createElement('div'); + + document.body.appendChild(el); + Vue.http.interceptors.push(boardsMockInterceptor); + gl.boardService = new BoardService('/test/issue-boards/board', '', '1'); + gl.issueBoards.BoardsStore.create(); + gl.IssueBoardsApp = new Vue(); + + const BoardListComp = Vue.extend(BoardList); + const list = new List(listObj); + const issue = new ListIssue({ + title: 'Testing', + iid: 1, + confidential: false, + labels: [], + }); + list.issuesSize = 1; + list.issues.push(issue); + + component = new BoardListComp({ + el, + propsData: { + disabled: false, + list, + issues: list.issues, + loading: false, + issueLinkBase: '/issues', + rootPath: '/', + }, + }).$mount(); + + Vue.nextTick(() => { + done(); + }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor); + }); + + it('renders component', () => { + expect( + component.$el.classList.contains('board-list-component'), + ).toBe(true); + }); + + it('renders loading icon', (done) => { + component.loading = true; + + Vue.nextTick(() => { + expect( + component.$el.querySelector('.board-list-loading'), + ).not.toBeNull(); + + done(); + }); + }); + + it('renders issues', () => { + expect( + component.$el.querySelectorAll('.card').length, + ).toBe(1); + }); + + it('sets data attribute with issue id', () => { + expect( + component.$el.querySelector('.card').getAttribute('data-issue-id'), + ).toBe('1'); + }); + + it('shows new issue form', (done) => { + component.toggleForm(); + + Vue.nextTick(() => { + expect( + component.$el.querySelector('.board-new-issue-form'), + ).not.toBeNull(); + + expect( + component.$el.querySelector('.is-smaller'), + ).not.toBeNull(); + + done(); + }); + }); + + it('shows new issue form after eventhub event', (done) => { + eventHub.$emit(`hide-issue-form-${component.list.id}`); + + Vue.nextTick(() => { + expect( + component.$el.querySelector('.board-new-issue-form'), + ).not.toBeNull(); + + expect( + component.$el.querySelector('.is-smaller'), + ).not.toBeNull(); + + done(); + }); + }); + + it('does not show new issue form for closed list', (done) => { + component.list.type = 'closed'; + component.toggleForm(); + + Vue.nextTick(() => { + expect( + component.$el.querySelector('.board-new-issue-form'), + ).toBeNull(); + + done(); + }); + }); + + it('shows count list item', (done) => { + component.showCount = true; + + Vue.nextTick(() => { + expect( + component.$el.querySelector('.board-list-count'), + ).not.toBeNull(); + + expect( + component.$el.querySelector('.board-list-count').textContent.trim(), + ).toBe('Showing all issues'); + + done(); + }); + }); + + it('shows how many more issues to load', (done) => { + component.showCount = true; + component.list.issuesSize = 20; + + Vue.nextTick(() => { + expect( + component.$el.querySelector('.board-list-count').textContent.trim(), + ).toBe('Showing 1 of 20 issues'); + + done(); + }); + }); + + it('loads more issues after scrolling', (done) => { + spyOn(component.list, 'nextPage'); + component.$refs.list.style.height = '100px'; + component.$refs.list.style.overflow = 'scroll'; + + for (let i = 0; i < 19; i += 1) { + const issue = component.list.issues[0]; + issue.id += 1; + component.list.issues.push(issue); + } + + Vue.nextTick(() => { + component.$refs.list.scrollTop = 20000; + + setTimeout(() => { + expect(component.list.nextPage).toHaveBeenCalled(); + + done(); + }); + }); + }); + + it('shows loading more spinner', (done) => { + component.showCount = true; + component.list.loadingMore = true; + + Vue.nextTick(() => { + expect( + component.$el.querySelector('.board-list-count .fa-spinner'), + ).not.toBeNull(); + + done(); + }); + }); +}); |