Commit dd18faff authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'issue-boards-list-template-in-js' into 'master'

Issue boards list template in JS file

See merge request !9958
parents 6831ee8f 6718c188
/* eslint-disable comma-dangle, space-before-function-paren, one-var */ /* eslint-disable comma-dangle, space-before-function-paren, one-var */
/* global Sortable */ /* global Sortable */
import Vue from 'vue'; import Vue from 'vue';
import boardList from './board_list';
import boardBlankState from './board_blank_state'; import boardBlankState from './board_blank_state';
require('./board_delete'); require('./board_delete');
...@@ -16,7 +16,7 @@ require('./board_list'); ...@@ -16,7 +16,7 @@ require('./board_list');
gl.issueBoards.Board = Vue.extend({ gl.issueBoards.Board = Vue.extend({
template: '#js-board-template', template: '#js-board-template',
components: { components: {
'board-list': gl.issueBoards.BoardList, boardList,
'board-delete': gl.issueBoards.BoardDelete, 'board-delete': gl.issueBoards.BoardDelete,
boardBlankState, boardBlankState,
}, },
......
/* eslint-disable comma-dangle, space-before-function-paren, max-len */
/* global Sortable */ /* global Sortable */
import Vue from 'vue';
import boardNewIssue from './board_new_issue'; import boardNewIssue from './board_new_issue';
import boardCard from './board_card'; import boardCard from './board_card';
import eventHub from '../eventhub';
(() => { const Store = gl.issueBoards.BoardsStore;
const Store = gl.issueBoards.BoardsStore;
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardList = Vue.extend({ export default {
template: '#js-board-list-template', name: 'BoardList',
components: {
boardCard,
boardNewIssue,
},
props: { props: {
disabled: Boolean, disabled: {
list: Object, type: Boolean,
issues: Array, required: true,
loading: Boolean, },
issueLinkBase: String, list: {
rootPath: String, type: Object,
}, required: true,
data () { },
issues: {
type: Array,
required: true,
},
loading: {
type: Boolean,
required: true,
},
issueLinkBase: {
type: String,
required: true,
},
rootPath: {
type: String,
required: true,
},
},
data() {
return { return {
scrollOffset: 250, scrollOffset: 250,
filters: Store.state.filters, filters: Store.state.filters,
showCount: false, showCount: false,
showIssueForm: false showIssueForm: false,
}; };
}, },
watch: { components: {
filters: { boardCard,
handler () { boardNewIssue,
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);
}
if (this.scrollHeight() > Math.ceil(this.listHeight())) {
this.showCount = true;
} else {
this.showCount = false;
}
});
}
}, },
methods: { methods: {
listHeight () { listHeight() {
return this.$refs.list.getBoundingClientRect().height; return this.$refs.list.getBoundingClientRect().height;
}, },
scrollHeight () { scrollHeight() {
return this.$refs.list.scrollHeight; return this.$refs.list.scrollHeight;
}, },
scrollTop () { scrollTop() {
return this.$refs.list.scrollTop + this.listHeight(); return this.$refs.list.scrollTop + this.listHeight();
}, },
loadNextPage () { loadNextPage() {
const getIssues = this.list.nextPage(); const getIssues = this.list.nextPage();
if (getIssues) { if (getIssues) {
...@@ -79,11 +68,40 @@ import boardCard from './board_card'; ...@@ -79,11 +68,40 @@ import boardCard from './board_card';
toggleForm() { toggleForm() {
this.showIssueForm = !this.showIssueForm; 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;
},
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 (this.scrollHeight() > Math.ceil(this.listHeight())) {
this.showCount = true;
} else {
this.showCount = false;
}
});
},
}, },
created() { created() {
gl.IssueBoardsApp.$on(`hide-issue-form-${this.list.id}`, this.toggleForm); eventHub.$on(`hide-issue-form-${this.list.id}`, this.toggleForm);
}, },
mounted () { mounted() {
const options = gl.issueBoards.getBoardSortableDefaultOptions({ const options = gl.issueBoards.getBoardSortableDefaultOptions({
scroll: document.querySelectorAll('.boards-list')[0], scroll: document.querySelectorAll('.boards-list')[0],
group: 'issues', group: 'issues',
...@@ -100,7 +118,8 @@ import boardCard from './board_card'; ...@@ -100,7 +118,8 @@ import boardCard from './board_card';
gl.issueBoards.onStart(); gl.issueBoards.onStart();
}, },
onAdd: (e) => { onAdd: (e) => {
gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex); gl.issueBoards.BoardsStore
.moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex);
this.$nextTick(() => { this.$nextTick(() => {
e.item.remove(); e.item.remove();
...@@ -108,24 +127,71 @@ import boardCard from './board_card'; ...@@ -108,24 +127,71 @@ import boardCard from './board_card';
}, },
onUpdate: (e) => { onUpdate: (e) => {
const sortedArray = this.sortable.toArray().filter(id => id !== '-1'); const sortedArray = this.sortable.toArray().filter(id => id !== '-1');
gl.issueBoards.BoardsStore.moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray); gl.issueBoards.BoardsStore
.moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray);
}, },
onMove(e) { onMove(e) {
return !e.related.classList.contains('board-list-count'); 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 // Scroll event on list to load more
this.$refs.list.onscroll = () => { this.$refs.list.addEventListener('scroll', this.onScroll);
if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) {
this.loadNextPage();
}
};
}, },
beforeDestroy() { beforeDestroy() {
gl.IssueBoardsApp.$off(`hide-issue-form-${this.list.id}`, this.toggleForm); 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>
`,
};
/* global ListIssue */ /* global ListIssue */
import eventHub from '../eventhub';
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
export default { export default {
...@@ -49,7 +51,7 @@ export default { ...@@ -49,7 +51,7 @@ export default {
}, },
cancel() { cancel() {
this.title = ''; this.title = '';
gl.IssueBoardsApp.$emit(`hide-issue-form-${this.list.id}`); eventHub.$emit(`hide-issue-form-${this.list.id}`);
}, },
}, },
mounted() { mounted() {
......
...@@ -8,7 +8,6 @@ ...@@ -8,7 +8,6 @@
= page_specific_javascript_bundle_tag('boards') = page_specific_javascript_bundle_tag('boards')
%script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board" %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 %script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
= render "projects/issues/head" = render "projects/issues/head"
......
.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
/* 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();
});
});
});
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