提交 4428bb27 编写于 作者: P Phil Hughes 提交者: Fatih Acet

Removed Masonry, instead uses groups of data

Added some error handling which reverts the frontend data changes &
notifies the user
上级 b4113dba
......@@ -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 }}
......
/* 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
......
/* 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;
this.$nextTick(() => {
this.destroyMasonry();
listMasonry = new Masonry(this.$refs.list, {
transitionDuration: 0,
});
setColumnCount() {
const breakpoint = bp.getBreakpointSize();
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.$refs.list.onscroll = () => {
const currentPage = Math.floor(this.issues.length / this.perPage);
this.scrollHandlerWrapper = this.scrollHandler.bind(this);
this.setColumnCountWrapper = this.setColumnCount.bind(this);
this.setColumnCount();
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>
......
/* 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');
});
});
});
})();
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册