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:
parent
b4113dba03
commit
4428bb27b7
16 changed files with 154 additions and 2551 deletions
|
@ -5,7 +5,6 @@
|
|||
//= require vue
|
||||
//= require vue-resource
|
||||
//= require Sortable
|
||||
//= require masonry
|
||||
//= require_tree ./models
|
||||
//= require_tree ./stores
|
||||
//= require_tree ./services
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
filterByLabel(label, e) {
|
||||
let labelToggleText = label.title;
|
||||
const labelIndex = Store.state.filters.label_name.indexOf(label.title);
|
||||
$(e.target).tooltip('hide');
|
||||
$(e.currentTarget).tooltip('hide');
|
||||
|
||||
if (labelIndex === -1) {
|
||||
Store.state.filters.label_name.push(label.title);
|
||||
|
@ -55,6 +55,12 @@
|
|||
|
||||
Store.updateFiltersUrl();
|
||||
},
|
||||
labelStyle(label) {
|
||||
return {
|
||||
backgroundColor: label.color,
|
||||
color: label.textColor,
|
||||
};
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
|
@ -93,7 +99,7 @@
|
|||
type="button"
|
||||
v-if="showLabel(label)"
|
||||
@click="filterByLabel(label, $event)"
|
||||
:style="{ backgroundColor: label.color, color: label.textColor }"
|
||||
:style="labelStyle(label)"
|
||||
:title="label.description"
|
||||
data-container="body">
|
||||
{{ label.title }}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
/* eslint-disable no-new */
|
||||
//= require ./lists_dropdown
|
||||
/* global Vue */
|
||||
/* global Flash */
|
||||
(() => {
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
|
||||
|
@ -15,7 +17,7 @@
|
|||
submitText() {
|
||||
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: {
|
||||
|
@ -27,6 +29,13 @@
|
|||
// Post the data to the backend
|
||||
gl.boardService.bulkUpdate(issueIds, {
|
||||
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
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
(() => {
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
|
||||
gl.issueBoards.IssuesModalHeader = Vue.extend({
|
||||
gl.issueBoards.ModalHeader = Vue.extend({
|
||||
mixins: [gl.issueBoards.ModalMixins],
|
||||
data() {
|
||||
return ModalStore.store;
|
||||
|
@ -16,6 +16,9 @@
|
|||
|
||||
return 'Deselect all';
|
||||
},
|
||||
showSearch() {
|
||||
return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleAll() {
|
||||
|
@ -45,7 +48,7 @@
|
|||
<modal-tabs v-if="!loading && issuesCount > 0"></modal-tabs>
|
||||
<div
|
||||
class="add-issues-search append-bottom-10"
|
||||
v-if="activeTab == 'all' && !loading && issuesCount > 0">
|
||||
v-if="showSearch">
|
||||
<input
|
||||
placeholder="Search issues..."
|
||||
class="form-control"
|
||||
|
|
|
@ -53,10 +53,9 @@
|
|||
},
|
||||
methods: {
|
||||
searchOperation: _.debounce(function searchOperationDebounce() {
|
||||
this.issues = [];
|
||||
this.loadIssues();
|
||||
this.loadIssues(true);
|
||||
}, 500),
|
||||
loadIssues() {
|
||||
loadIssues(clearIssues = false) {
|
||||
return gl.boardService.getBacklog({
|
||||
search: this.searchTerm,
|
||||
page: this.page,
|
||||
|
@ -64,10 +63,14 @@
|
|||
}).then((res) => {
|
||||
const data = res.json();
|
||||
|
||||
if (clearIssues) {
|
||||
this.issues = [];
|
||||
}
|
||||
|
||||
data.issues.forEach((issueObj) => {
|
||||
const issue = new ListIssue(issueObj);
|
||||
const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
|
||||
issue.selected = foundSelectedIssue !== undefined;
|
||||
issue.selected = !!foundSelectedIssue;
|
||||
|
||||
this.issues.push(issue);
|
||||
});
|
||||
|
@ -75,7 +78,7 @@
|
|||
this.loadingNewPage = false;
|
||||
|
||||
if (!this.issuesCount) {
|
||||
this.issuesCount = this.issues.length;
|
||||
this.issuesCount = data.size;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
@ -88,9 +91,16 @@
|
|||
|
||||
return this.issuesCount > 0;
|
||||
},
|
||||
showEmptyState() {
|
||||
if (!this.loading && this.issuesCount === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.activeTab === 'selected' && this.selectedIssues.length === 0;
|
||||
},
|
||||
},
|
||||
components: {
|
||||
'modal-header': gl.issueBoards.IssuesModalHeader,
|
||||
'modal-header': gl.issueBoards.ModalHeader,
|
||||
'modal-list': gl.issueBoards.ModalList,
|
||||
'modal-footer': gl.issueBoards.ModalFooter,
|
||||
'empty-state': gl.issueBoards.ModalEmptyState,
|
||||
|
@ -106,7 +116,7 @@
|
|||
:root-path="rootPath"
|
||||
v-if="!loading && showList"></modal-list>
|
||||
<empty-state
|
||||
v-if="(!loading && issuesCount === 0) || (activeTab === 'selected' && selectedIssues.length === 0)"
|
||||
v-if="showEmptyState"
|
||||
:image="blankStateImage"
|
||||
:new-issue-path="newIssuePath"></empty-state>
|
||||
<section
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
/* global Vue */
|
||||
/* global ListIssue */
|
||||
/* global Masonry */
|
||||
/* global bp */
|
||||
(() => {
|
||||
let listMasonry;
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
|
||||
gl.issueBoards.ModalList = Vue.extend({
|
||||
|
@ -21,18 +20,10 @@
|
|||
},
|
||||
watch: {
|
||||
activeTab() {
|
||||
this.initMasonry();
|
||||
|
||||
if (this.activeTab === 'all') {
|
||||
ModalStore.purgeUnselectedIssues();
|
||||
}
|
||||
},
|
||||
issues: {
|
||||
handler() {
|
||||
this.initMasonry();
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
loopIssues() {
|
||||
|
@ -42,8 +33,31 @@
|
|||
|
||||
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: {
|
||||
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) {
|
||||
if (e.target.tagName !== 'A') {
|
||||
ModalStore.toggleIssue(issue);
|
||||
|
@ -65,40 +79,29 @@
|
|||
|
||||
return index !== -1;
|
||||
},
|
||||
initMasonry() {
|
||||
const listScrollTop = this.$refs.list.scrollTop;
|
||||
setColumnCount() {
|
||||
const breakpoint = bp.getBreakpointSize();
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.destroyMasonry();
|
||||
listMasonry = new Masonry(this.$refs.list, {
|
||||
transitionDuration: 0,
|
||||
});
|
||||
|
||||
this.$refs.list.scrollTop = listScrollTop;
|
||||
});
|
||||
},
|
||||
destroyMasonry() {
|
||||
if (listMasonry) {
|
||||
listMasonry.destroy();
|
||||
listMasonry = undefined;
|
||||
if (breakpoint === 'lg' || breakpoint === 'md') {
|
||||
this.columns = 3;
|
||||
} else if (breakpoint === 'sm') {
|
||||
this.columns = 2;
|
||||
} else {
|
||||
this.columns = 1;
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.initMasonry();
|
||||
this.scrollHandlerWrapper = this.scrollHandler.bind(this);
|
||||
this.setColumnCountWrapper = this.setColumnCount.bind(this);
|
||||
this.setColumnCount();
|
||||
|
||||
this.$refs.list.onscroll = () => {
|
||||
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;
|
||||
}
|
||||
};
|
||||
this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
|
||||
window.addEventListener('resize', this.setColumnCountWrapper);
|
||||
},
|
||||
destroyed() {
|
||||
this.destroyMasonry();
|
||||
beforeDestroy() {
|
||||
this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
|
||||
window.removeEventListener('resize', this.setColumnCountWrapper);
|
||||
},
|
||||
components: {
|
||||
'issue-card-inner': gl.issueBoards.IssueCardInner,
|
||||
|
@ -108,25 +111,29 @@
|
|||
class="add-issues-list add-issues-list-columns"
|
||||
ref="list">
|
||||
<div
|
||||
v-for="issue in loopIssues"
|
||||
v-if="showIssue(issue)"
|
||||
class="card-parent">
|
||||
v-for="group in groupedIssues"
|
||||
class="add-issues-list-column">
|
||||
<div
|
||||
class="card"
|
||||
:class="{ 'is-active': issue.selected }"
|
||||
@click="toggleIssue($event, issue)">
|
||||
<issue-card-inner
|
||||
:issue="issue"
|
||||
:issue-link-base="issueLinkBase"
|
||||
:root-path="rootPath">
|
||||
</issue-card-inner>
|
||||
<span
|
||||
:aria-label="'Issue #' + issue.id + ' selected'"
|
||||
aria-checked="true"
|
||||
v-if="issue.selected"
|
||||
class="issue-card-selected text-center">
|
||||
<i class="fa fa-check"></i>
|
||||
</span>
|
||||
v-for="issue in group"
|
||||
v-if="showIssue(issue)"
|
||||
class="card-parent">
|
||||
<div
|
||||
class="card"
|
||||
:class="{ 'is-active': issue.selected }"
|
||||
@click="toggleIssue($event, issue)">
|
||||
<issue-card-inner
|
||||
:issue="issue"
|
||||
:issue-link-base="issueLinkBase"
|
||||
:root-path="rootPath">
|
||||
</issue-card-inner>
|
||||
<span
|
||||
:aria-label="'Issue #' + issue.id + ' selected'"
|
||||
aria-checked="true"
|
||||
v-if="issue.selected"
|
||||
class="issue-card-selected text-center">
|
||||
<i class="fa fa-check"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
href="#"
|
||||
role="button"
|
||||
:class="{ 'is-active': list.id == selected.id }"
|
||||
@click="modal.selectedList = list">
|
||||
@click.prevent="modal.selectedList = list">
|
||||
<span
|
||||
class="dropdown-label-box"
|
||||
:style="{ backgroundColor: list.label.color }">
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
href="#"
|
||||
role="button"
|
||||
@click.prevent="changeTab('all')">
|
||||
<span>All issues</span>
|
||||
All issues
|
||||
<span class="badge">
|
||||
{{ issuesCount }}
|
||||
</span>
|
||||
|
@ -34,7 +34,7 @@
|
|||
href="#"
|
||||
role="button"
|
||||
@click.prevent="changeTab('selected')">
|
||||
<span>Selected issues</span>
|
||||
Selected issues
|
||||
<span class="badge">
|
||||
{{ selectedCount }}
|
||||
</span>
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
/* eslint-disable no-new */
|
||||
/* global Vue */
|
||||
/* global Flash */
|
||||
(() => {
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
||||
|
@ -18,17 +20,24 @@
|
|||
},
|
||||
methods: {
|
||||
removeIssue() {
|
||||
const lists = this.issue.getLists();
|
||||
const issue = this.issue;
|
||||
const lists = issue.getLists();
|
||||
const labelIds = lists.map(list => list.label.id);
|
||||
|
||||
// Post the remove data
|
||||
gl.boardService.bulkUpdate([this.issue.globalId], {
|
||||
gl.boardService.bulkUpdate([issue.globalId], {
|
||||
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
|
||||
lists.forEach((list) => {
|
||||
list.removeIssue(this.issue);
|
||||
list.removeIssue(issue);
|
||||
});
|
||||
|
||||
Store.detail.issue = {};
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
class ModalStore {
|
||||
constructor() {
|
||||
this.store = {
|
||||
columns: 3,
|
||||
issues: [],
|
||||
issuesCount: false,
|
||||
selectedIssues: [],
|
||||
|
@ -25,9 +26,11 @@
|
|||
|
||||
toggleIssue(issueObj) {
|
||||
const issue = issueObj;
|
||||
issue.selected = !issue.selected;
|
||||
const selected = issue.selected;
|
||||
|
||||
if (issue.selected) {
|
||||
issue.selected = !selected;
|
||||
|
||||
if (!selected) {
|
||||
this.addSelectedIssue(issue);
|
||||
} else {
|
||||
this.removeSelectedIssue(issue);
|
||||
|
|
|
@ -161,6 +161,9 @@
|
|||
gl.text.humanize = function(string) {
|
||||
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 string.substr(0, (maxLength - 3)) + '...';
|
||||
};
|
||||
|
|
|
@ -418,6 +418,18 @@
|
|||
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 {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
|
@ -429,16 +441,7 @@
|
|||
overflow-y: scroll;
|
||||
|
||||
.card-parent {
|
||||
width: 100%;
|
||||
padding: 0 5px 5px;
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
@media (min-width: $screen-md-min) {
|
||||
width: (100% / 3);
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
|
@ -480,6 +483,6 @@
|
|||
color: $white-light;
|
||||
border: 1px solid $border-blue-light;
|
||||
font-size: 9px;
|
||||
line-height: 17px;
|
||||
line-height: 15px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ describe('Issue model', () => {
|
|||
let issue;
|
||||
|
||||
beforeEach(() => {
|
||||
gl.boardService = new BoardService('/test/issue-boards/board', '1');
|
||||
gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
|
||||
gl.issueBoards.BoardsStore.create();
|
||||
|
||||
issue = new ListIssue({
|
||||
|
|
|
@ -24,7 +24,7 @@ describe('List model', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
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();
|
||||
|
||||
list = new List(listObj);
|
||||
|
|
|
@ -21,5 +21,19 @@
|
|||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
|
2463
vendor/assets/javascripts/masonry.js
vendored
2463
vendor/assets/javascripts/masonry.js
vendored
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue