Removed Masonry, instead uses groups of data

Added some error handling which reverts the frontend data changes &
notifies the user
This commit is contained in:
Phil Hughes 2017-02-01 15:23:01 +00:00 committed by Fatih Acet
parent b4113dba03
commit 4428bb27b7
16 changed files with 154 additions and 2551 deletions

View file

@ -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

View file

@ -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 @@
Store.updateFiltersUrl(); Store.updateFiltersUrl();
}, },
labelStyle(label) {
return {
backgroundColor: label.color,
color: label.textColor,
};
},
}, },
template: ` template: `
<div> <div>
@ -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 }}

View file

@ -1,5 +1,7 @@
/* 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 @@
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 @@
// 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

View file

@ -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 @@
return 'Deselect all'; return 'Deselect all';
}, },
showSearch() {
return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
},
}, },
methods: { methods: {
toggleAll() { toggleAll() {
@ -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"

View file

@ -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 @@
}).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 @@
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 @@
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 @@
: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

View file

@ -1,8 +1,7 @@
/* 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 @@
}, },
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 @@
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 @@
return index !== -1; return index !== -1;
}, },
initMasonry() { setColumnCount() {
const listScrollTop = this.$refs.list.scrollTop; const breakpoint = bp.getBreakpointSize();
this.$nextTick(() => { if (breakpoint === 'lg' || breakpoint === 'md') {
this.destroyMasonry(); this.columns = 3;
listMasonry = new Masonry(this.$refs.list, { } else if (breakpoint === 'sm') {
transitionDuration: 0, this.columns = 2;
}); } else {
this.columns = 1;
this.$refs.list.scrollTop = listScrollTop;
});
},
destroyMasonry() {
if (listMasonry) {
listMasonry.destroy();
listMasonry = undefined;
} }
}, },
}, },
mounted() { mounted() {
this.initMasonry(); this.scrollHandlerWrapper = this.scrollHandler.bind(this);
this.setColumnCountWrapper = this.setColumnCount.bind(this);
this.setColumnCount();
this.$refs.list.onscroll = () => { this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
const currentPage = Math.floor(this.issues.length / this.perPage); window.addEventListener('resize', this.setColumnCountWrapper);
if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage
&& currentPage === this.page) {
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,25 +111,29 @@
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"
v-if="showIssue(issue)" class="add-issues-list-column">
class="card-parent">
<div <div
class="card" v-for="issue in group"
:class="{ 'is-active': issue.selected }" v-if="showIssue(issue)"
@click="toggleIssue($event, issue)"> class="card-parent">
<issue-card-inner <div
:issue="issue" class="card"
:issue-link-base="issueLinkBase" :class="{ 'is-active': issue.selected }"
:root-path="rootPath"> @click="toggleIssue($event, issue)">
</issue-card-inner> <issue-card-inner
<span :issue="issue"
:aria-label="'Issue #' + issue.id + ' selected'" :issue-link-base="issueLinkBase"
aria-checked="true" :root-path="rootPath">
v-if="issue.selected" </issue-card-inner>
class="issue-card-selected text-center"> <span
<i class="fa fa-check"></i> :aria-label="'Issue #' + issue.id + ' selected'"
</span> aria-checked="true"
v-if="issue.selected"
class="issue-card-selected text-center">
<i class="fa fa-check"></i>
</span>
</div>
</div> </div>
</div> </div>
</section> </section>

View file

@ -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 }">

View file

@ -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 @@
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>

View file

@ -1,4 +1,6 @@
/* eslint-disable no-new */
/* global Vue */ /* global Vue */
/* global Flash */
(() => { (() => {
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
@ -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 = {};

View file

@ -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 @@
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);

View file

@ -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)) + '...';
}; };

View file

@ -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 @@
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 @@
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%;
} }

View file

@ -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({

View file

@ -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);

View file

@ -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');
});
});
}); });
})(); })();

File diff suppressed because it is too large Load diff