Commit 4428bb27 authored by Phil Hughes's avatar Phil Hughes Committed by Fatih Acet

Removed Masonry, instead uses groups of data

Added some error handling which reverts the frontend data changes &
notifies the user
parent b4113dba
...@@ -5,7 +5,6 @@ ...@@ -5,7 +5,6 @@
//= require vue //= require vue
//= require vue-resource //= require vue-resource
//= require Sortable //= require Sortable
//= require masonry
//= require_tree ./models //= require_tree ./models
//= require_tree ./stores //= require_tree ./stores
//= require_tree ./services //= require_tree ./services
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
filterByLabel(label, e) { filterByLabel(label, e) {
let labelToggleText = label.title; let labelToggleText = label.title;
const labelIndex = Store.state.filters.label_name.indexOf(label.title); const labelIndex = Store.state.filters.label_name.indexOf(label.title);
$(e.target).tooltip('hide'); $(e.currentTarget).tooltip('hide');
if (labelIndex === -1) { if (labelIndex === -1) {
Store.state.filters.label_name.push(label.title); Store.state.filters.label_name.push(label.title);
...@@ -55,6 +55,12 @@ ...@@ -55,6 +55,12 @@
Store.updateFiltersUrl(); Store.updateFiltersUrl();
}, },
labelStyle(label) {
return {
backgroundColor: label.color,
color: label.textColor,
};
},
}, },
template: ` template: `
<div> <div>
...@@ -93,7 +99,7 @@ ...@@ -93,7 +99,7 @@
type="button" type="button"
v-if="showLabel(label)" v-if="showLabel(label)"
@click="filterByLabel(label, $event)" @click="filterByLabel(label, $event)"
:style="{ backgroundColor: label.color, color: label.textColor }" :style="labelStyle(label)"
:title="label.description" :title="label.description"
data-container="body"> data-container="body">
{{ label.title }} {{ label.title }}
......
/* eslint-disable no-new */
//= require ./lists_dropdown //= require ./lists_dropdown
/* global Vue */ /* global Vue */
/* global Flash */
(() => { (() => {
const ModalStore = gl.issueBoards.ModalStore; const ModalStore = gl.issueBoards.ModalStore;
...@@ -15,7 +17,7 @@ ...@@ -15,7 +17,7 @@
submitText() { submitText() {
const count = ModalStore.selectedCount(); const count = ModalStore.selectedCount();
return `Add ${count > 0 ? count : ''} issue${count > 1 || !count ? 's' : ''}`; return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`;
}, },
}, },
methods: { methods: {
...@@ -27,6 +29,13 @@ ...@@ -27,6 +29,13 @@
// Post the data to the backend // Post the data to the backend
gl.boardService.bulkUpdate(issueIds, { gl.boardService.bulkUpdate(issueIds, {
add_label_ids: [list.label.id], add_label_ids: [list.label.id],
}).catch(() => {
new Flash('Failed to update issues, please try again.', 'alert');
selectedIssues.forEach((issue) => {
list.removeIssue(issue);
list.issuesSize -= 1;
});
}); });
// Add the issues on the frontend // Add the issues on the frontend
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
(() => { (() => {
const ModalStore = gl.issueBoards.ModalStore; const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.IssuesModalHeader = Vue.extend({ gl.issueBoards.ModalHeader = Vue.extend({
mixins: [gl.issueBoards.ModalMixins], mixins: [gl.issueBoards.ModalMixins],
data() { data() {
return ModalStore.store; return ModalStore.store;
...@@ -16,6 +16,9 @@ ...@@ -16,6 +16,9 @@
return 'Deselect all'; return 'Deselect all';
}, },
showSearch() {
return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
},
}, },
methods: { methods: {
toggleAll() { toggleAll() {
...@@ -45,7 +48,7 @@ ...@@ -45,7 +48,7 @@
<modal-tabs v-if="!loading && issuesCount > 0"></modal-tabs> <modal-tabs v-if="!loading && issuesCount > 0"></modal-tabs>
<div <div
class="add-issues-search append-bottom-10" class="add-issues-search append-bottom-10"
v-if="activeTab == 'all' && !loading && issuesCount > 0"> v-if="showSearch">
<input <input
placeholder="Search issues..." placeholder="Search issues..."
class="form-control" class="form-control"
......
...@@ -53,10 +53,9 @@ ...@@ -53,10 +53,9 @@
}, },
methods: { methods: {
searchOperation: _.debounce(function searchOperationDebounce() { searchOperation: _.debounce(function searchOperationDebounce() {
this.issues = []; this.loadIssues(true);
this.loadIssues();
}, 500), }, 500),
loadIssues() { loadIssues(clearIssues = false) {
return gl.boardService.getBacklog({ return gl.boardService.getBacklog({
search: this.searchTerm, search: this.searchTerm,
page: this.page, page: this.page,
...@@ -64,10 +63,14 @@ ...@@ -64,10 +63,14 @@
}).then((res) => { }).then((res) => {
const data = res.json(); const data = res.json();
if (clearIssues) {
this.issues = [];
}
data.issues.forEach((issueObj) => { data.issues.forEach((issueObj) => {
const issue = new ListIssue(issueObj); const issue = new ListIssue(issueObj);
const foundSelectedIssue = ModalStore.findSelectedIssue(issue); const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
issue.selected = foundSelectedIssue !== undefined; issue.selected = !!foundSelectedIssue;
this.issues.push(issue); this.issues.push(issue);
}); });
...@@ -75,7 +78,7 @@ ...@@ -75,7 +78,7 @@
this.loadingNewPage = false; this.loadingNewPage = false;
if (!this.issuesCount) { if (!this.issuesCount) {
this.issuesCount = this.issues.length; this.issuesCount = data.size;
} }
}); });
}, },
...@@ -88,9 +91,16 @@ ...@@ -88,9 +91,16 @@
return this.issuesCount > 0; return this.issuesCount > 0;
}, },
showEmptyState() {
if (!this.loading && this.issuesCount === 0) {
return true;
}
return this.activeTab === 'selected' && this.selectedIssues.length === 0;
},
}, },
components: { components: {
'modal-header': gl.issueBoards.IssuesModalHeader, 'modal-header': gl.issueBoards.ModalHeader,
'modal-list': gl.issueBoards.ModalList, 'modal-list': gl.issueBoards.ModalList,
'modal-footer': gl.issueBoards.ModalFooter, 'modal-footer': gl.issueBoards.ModalFooter,
'empty-state': gl.issueBoards.ModalEmptyState, 'empty-state': gl.issueBoards.ModalEmptyState,
...@@ -106,7 +116,7 @@ ...@@ -106,7 +116,7 @@
:root-path="rootPath" :root-path="rootPath"
v-if="!loading && showList"></modal-list> v-if="!loading && showList"></modal-list>
<empty-state <empty-state
v-if="(!loading && issuesCount === 0) || (activeTab === 'selected' && selectedIssues.length === 0)" v-if="showEmptyState"
:image="blankStateImage" :image="blankStateImage"
:new-issue-path="newIssuePath"></empty-state> :new-issue-path="newIssuePath"></empty-state>
<section <section
......
/* global Vue */ /* global Vue */
/* global ListIssue */ /* global ListIssue */
/* global Masonry */ /* global bp */
(() => { (() => {
let listMasonry;
const ModalStore = gl.issueBoards.ModalStore; const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.ModalList = Vue.extend({ gl.issueBoards.ModalList = Vue.extend({
...@@ -21,18 +20,10 @@ ...@@ -21,18 +20,10 @@
}, },
watch: { watch: {
activeTab() { activeTab() {
this.initMasonry();
if (this.activeTab === 'all') { if (this.activeTab === 'all') {
ModalStore.purgeUnselectedIssues(); ModalStore.purgeUnselectedIssues();
} }
}, },
issues: {
handler() {
this.initMasonry();
},
deep: true,
},
}, },
computed: { computed: {
loopIssues() { loopIssues() {
...@@ -42,8 +33,31 @@ ...@@ -42,8 +33,31 @@
return this.selectedIssues; return this.selectedIssues;
}, },
groupedIssues() {
const groups = [];
this.loopIssues.forEach((issue, i) => {
const index = i % this.columns;
if (!groups[index]) {
groups.push([]);
}
groups[index].push(issue);
});
return groups;
},
}, },
methods: { methods: {
scrollHandler() {
const currentPage = Math.floor(this.issues.length / this.perPage);
if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage
&& currentPage === this.page) {
this.loadingNewPage = true;
this.page += 1;
}
},
toggleIssue(e, issue) { toggleIssue(e, issue) {
if (e.target.tagName !== 'A') { if (e.target.tagName !== 'A') {
ModalStore.toggleIssue(issue); ModalStore.toggleIssue(issue);
...@@ -65,40 +79,29 @@ ...@@ -65,40 +79,29 @@
return index !== -1; return index !== -1;
}, },
initMasonry() { setColumnCount() {
const listScrollTop = this.$refs.list.scrollTop; const breakpoint = bp.getBreakpointSize();
this.$nextTick(() => {
this.destroyMasonry();
listMasonry = new Masonry(this.$refs.list, {
transitionDuration: 0,
});
this.$refs.list.scrollTop = listScrollTop; if (breakpoint === 'lg' || breakpoint === 'md') {
}); this.columns = 3;
}, } else if (breakpoint === 'sm') {
destroyMasonry() { this.columns = 2;
if (listMasonry) { } else {
listMasonry.destroy(); this.columns = 1;
listMasonry = undefined;
} }
}, },
}, },
mounted() { mounted() {
this.initMasonry(); this.scrollHandlerWrapper = this.scrollHandler.bind(this);
this.setColumnCountWrapper = this.setColumnCount.bind(this);
this.$refs.list.onscroll = () => { this.setColumnCount();
const currentPage = Math.floor(this.issues.length / this.perPage);
if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
&& currentPage === this.page) { window.addEventListener('resize', this.setColumnCountWrapper);
this.loadingNewPage = true;
this.page += 1;
}
};
}, },
destroyed() { beforeDestroy() {
this.destroyMasonry(); this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
window.removeEventListener('resize', this.setColumnCountWrapper);
}, },
components: { components: {
'issue-card-inner': gl.issueBoards.IssueCardInner, 'issue-card-inner': gl.issueBoards.IssueCardInner,
...@@ -108,7 +111,10 @@ ...@@ -108,7 +111,10 @@
class="add-issues-list add-issues-list-columns" class="add-issues-list add-issues-list-columns"
ref="list"> ref="list">
<div <div
v-for="issue in loopIssues" v-for="group in groupedIssues"
class="add-issues-list-column">
<div
v-for="issue in group"
v-if="showIssue(issue)" v-if="showIssue(issue)"
class="card-parent"> class="card-parent">
<div <div
...@@ -129,6 +135,7 @@ ...@@ -129,6 +135,7 @@
</span> </span>
</div> </div>
</div> </div>
</div>
</section> </section>
`, `,
}); });
......
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
href="#" href="#"
role="button" role="button"
:class="{ 'is-active': list.id == selected.id }" :class="{ 'is-active': list.id == selected.id }"
@click="modal.selectedList = list"> @click.prevent="modal.selectedList = list">
<span <span
class="dropdown-label-box" class="dropdown-label-box"
:style="{ backgroundColor: list.label.color }"> :style="{ backgroundColor: list.label.color }">
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
href="#" href="#"
role="button" role="button"
@click.prevent="changeTab('all')"> @click.prevent="changeTab('all')">
<span>All issues</span> All issues
<span class="badge"> <span class="badge">
{{ issuesCount }} {{ issuesCount }}
</span> </span>
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
href="#" href="#"
role="button" role="button"
@click.prevent="changeTab('selected')"> @click.prevent="changeTab('selected')">
<span>Selected issues</span> Selected issues
<span class="badge"> <span class="badge">
{{ selectedCount }} {{ selectedCount }}
</span> </span>
......
/* eslint-disable no-new */
/* global Vue */ /* global Vue */
/* global Flash */
(() => { (() => {
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
...@@ -18,17 +20,24 @@ ...@@ -18,17 +20,24 @@
}, },
methods: { methods: {
removeIssue() { removeIssue() {
const lists = this.issue.getLists(); const issue = this.issue;
const lists = issue.getLists();
const labelIds = lists.map(list => list.label.id); const labelIds = lists.map(list => list.label.id);
// Post the remove data // Post the remove data
gl.boardService.bulkUpdate([this.issue.globalId], { gl.boardService.bulkUpdate([issue.globalId], {
remove_label_ids: labelIds, remove_label_ids: labelIds,
}).catch(() => {
new Flash('Failed to remove issue from board, please try again.', 'alert');
lists.forEach((list) => {
list.addIssue(issue);
});
}); });
// Remove from the frontend store // Remove from the frontend store
lists.forEach((list) => { lists.forEach((list) => {
list.removeIssue(this.issue); list.removeIssue(issue);
}); });
Store.detail.issue = {}; Store.detail.issue = {};
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
class ModalStore { class ModalStore {
constructor() { constructor() {
this.store = { this.store = {
columns: 3,
issues: [], issues: [],
issuesCount: false, issuesCount: false,
selectedIssues: [], selectedIssues: [],
...@@ -25,9 +26,11 @@ ...@@ -25,9 +26,11 @@
toggleIssue(issueObj) { toggleIssue(issueObj) {
const issue = issueObj; const issue = issueObj;
issue.selected = !issue.selected; const selected = issue.selected;
if (issue.selected) { issue.selected = !selected;
if (!selected) {
this.addSelectedIssue(issue); this.addSelectedIssue(issue);
} else { } else {
this.removeSelectedIssue(issue); this.removeSelectedIssue(issue);
......
...@@ -161,6 +161,9 @@ ...@@ -161,6 +161,9 @@
gl.text.humanize = function(string) { gl.text.humanize = function(string) {
return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1); return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
}; };
gl.text.pluralize = function(str, count) {
return str + (count > 1 || count === 0 ? 's' : '');
};
return gl.text.truncate = function(string, maxLength) { return gl.text.truncate = function(string, maxLength) {
return string.substr(0, (maxLength - 3)) + '...'; return string.substr(0, (maxLength - 3)) + '...';
}; };
......
...@@ -418,6 +418,18 @@ ...@@ -418,6 +418,18 @@
display: flex; display: flex;
} }
.add-issues-list-column {
width: 100%;
@media (min-width: $screen-sm-min) {
width: 50%;
}
@media (min-width: $screen-md-min) {
width: (100% / 3);
}
}
.add-issues-list { .add-issues-list {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
...@@ -429,16 +441,7 @@ ...@@ -429,16 +441,7 @@
overflow-y: scroll; overflow-y: scroll;
.card-parent { .card-parent {
width: 100%;
padding: 0 5px 5px; padding: 0 5px 5px;
@media (min-width: $screen-sm-min) {
width: 50%;
}
@media (min-width: $screen-md-min) {
width: (100% / 3);
}
} }
.card { .card {
...@@ -480,6 +483,6 @@ ...@@ -480,6 +483,6 @@
color: $white-light; color: $white-light;
border: 1px solid $border-blue-light; border: 1px solid $border-blue-light;
font-size: 9px; font-size: 9px;
line-height: 17px; line-height: 15px;
border-radius: 50%; border-radius: 50%;
} }
...@@ -20,7 +20,7 @@ describe('Issue model', () => { ...@@ -20,7 +20,7 @@ describe('Issue model', () => {
let issue; let issue;
beforeEach(() => { beforeEach(() => {
gl.boardService = new BoardService('/test/issue-boards/board', '1'); gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
gl.issueBoards.BoardsStore.create(); gl.issueBoards.BoardsStore.create();
issue = new ListIssue({ issue = new ListIssue({
......
...@@ -24,7 +24,7 @@ describe('List model', () => { ...@@ -24,7 +24,7 @@ describe('List model', () => {
beforeEach(() => { beforeEach(() => {
Vue.http.interceptors.push(boardsMockInterceptor); Vue.http.interceptors.push(boardsMockInterceptor);
gl.boardService = new BoardService('/test/issue-boards/board', '1'); gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
gl.issueBoards.BoardsStore.create(); gl.issueBoards.BoardsStore.create();
list = new List(listObj); list = new List(listObj);
......
...@@ -21,5 +21,19 @@ ...@@ -21,5 +21,19 @@
expect(largeFont > regular).toBe(true); expect(largeFont > regular).toBe(true);
}); });
}); });
describe('gl.text.pluralize', () => {
it('returns pluralized', () => {
expect(gl.text.pluralize('test', 2)).toBe('tests');
});
it('returns pluralized', () => {
expect(gl.text.pluralize('test', 0)).toBe('tests');
});
it('does not return pluralized', () => {
expect(gl.text.pluralize('test', 1)).toBe('test');
});
});
}); });
})(); })();
This diff is collapsed.
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