Remove IIFEs in boards_bundle.js

This commit is contained in:
Alfredo Sumaran 2017-04-10 18:52:46 -05:00
parent 84da388d43
commit 6c4d611772
18 changed files with 1269 additions and 1306 deletions

View File

@ -7,100 +7,98 @@ import boardBlankState from './board_blank_state';
require('./board_delete'); require('./board_delete');
require('./board_list'); require('./board_list');
(() => { const Store = gl.issueBoards.BoardsStore;
const Store = gl.issueBoards.BoardsStore;
window.gl = window.gl || {}; window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {}; window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.Board = Vue.extend({ gl.issueBoards.Board = Vue.extend({
template: '#js-board-template', template: '#js-board-template',
components: { components: {
boardList, boardList,
'board-delete': gl.issueBoards.BoardDelete, 'board-delete': gl.issueBoards.BoardDelete,
boardBlankState, boardBlankState,
}, },
props: { props: {
list: Object, list: Object,
disabled: Boolean, disabled: Boolean,
issueLinkBase: String, issueLinkBase: String,
rootPath: String, rootPath: String,
}, },
data () { data () {
return { return {
detailIssue: Store.detail, detailIssue: Store.detail,
filter: Store.filter, filter: Store.filter,
}; };
}, },
watch: { watch: {
filter: { filter: {
handler() { handler() {
this.list.page = 1; this.list.page = 1;
this.list.getIssues(true); this.list.getIssues(true);
},
deep: true,
}, },
detailIssue: { deep: true,
handler () { },
if (!Object.keys(this.detailIssue.issue).length) return; detailIssue: {
handler () {
if (!Object.keys(this.detailIssue.issue).length) return;
const issue = this.list.findIssue(this.detailIssue.issue.id); const issue = this.list.findIssue(this.detailIssue.issue.id);
if (issue) { if (issue) {
const offsetLeft = this.$el.offsetLeft; const offsetLeft = this.$el.offsetLeft;
const boardsList = document.querySelectorAll('.boards-list')[0]; const boardsList = document.querySelectorAll('.boards-list')[0];
const left = boardsList.scrollLeft - offsetLeft; const left = boardsList.scrollLeft - offsetLeft;
let right = (offsetLeft + this.$el.offsetWidth); let right = (offsetLeft + this.$el.offsetWidth);
if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) { if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) {
// -290 here because width of boardsList is animating so therefore // -290 here because width of boardsList is animating so therefore
// getting the width here is incorrect // getting the width here is incorrect
// 290 is the width of the sidebar // 290 is the width of the sidebar
right -= (boardsList.offsetWidth - 290); right -= (boardsList.offsetWidth - 290);
} else { } else {
right -= boardsList.offsetWidth; right -= boardsList.offsetWidth;
}
if (right - boardsList.scrollLeft > 0) {
$(boardsList).animate({
scrollLeft: right
}, this.sortableOptions.animation);
} else if (left > 0) {
$(boardsList).animate({
scrollLeft: offsetLeft
}, this.sortableOptions.animation);
}
} }
},
deep: true
}
},
methods: {
showNewIssueForm() {
this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
}
},
mounted () {
this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({
disabled: this.disabled,
group: 'boards',
draggable: '.is-draggable',
handle: '.js-board-handle',
onEnd: (e) => {
gl.issueBoards.onEnd();
if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) { if (right - boardsList.scrollLeft > 0) {
const order = this.sortable.toArray(); $(boardsList).animate({
const list = Store.findList('id', parseInt(e.item.dataset.id, 10)); scrollLeft: right
}, this.sortableOptions.animation);
this.$nextTick(() => { } else if (left > 0) {
Store.moveList(list, order); $(boardsList).animate({
}); scrollLeft: offsetLeft
}, this.sortableOptions.animation);
} }
} }
}); },
deep: true
}
},
methods: {
showNewIssueForm() {
this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
}
},
mounted () {
this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({
disabled: this.disabled,
group: 'boards',
draggable: '.is-draggable',
handle: '.js-board-handle',
onEnd: (e) => {
gl.issueBoards.onEnd();
this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions); if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
}, const order = this.sortable.toArray();
}); const list = Store.findList('id', parseInt(e.item.dataset.id, 10));
})();
this.$nextTick(() => {
Store.moveList(list, order);
});
}
}
});
this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions);
},
});

View File

@ -2,22 +2,20 @@
import Vue from 'vue'; import Vue from 'vue';
(() => { window.gl = window.gl || {};
window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {};
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardDelete = Vue.extend({ gl.issueBoards.BoardDelete = Vue.extend({
props: { props: {
list: Object list: Object
}, },
methods: { methods: {
deleteBoard () { deleteBoard () {
$(this.$el).tooltip('hide'); $(this.$el).tooltip('hide');
if (confirm('Are you sure you want to delete this list?')) { if (confirm('Are you sure you want to delete this list?')) {
this.list.destroy(); this.list.destroy();
}
} }
} }
}); }
})(); });

View File

@ -8,66 +8,64 @@ import Vue from 'vue';
require('./sidebar/remove_issue'); require('./sidebar/remove_issue');
(() => { const Store = gl.issueBoards.BoardsStore;
const Store = gl.issueBoards.BoardsStore;
window.gl = window.gl || {}; window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {}; window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardSidebar = Vue.extend({ gl.issueBoards.BoardSidebar = Vue.extend({
props: { props: {
currentUser: Object currentUser: Object
}, },
data() { data() {
return { return {
detail: Store.detail, detail: Store.detail,
issue: {}, issue: {},
list: {}, list: {},
}; };
}, },
computed: { computed: {
showSidebar () { showSidebar () {
return Object.keys(this.issue).length; return Object.keys(this.issue).length;
} }
}, },
watch: { watch: {
detail: { detail: {
handler () { handler () {
if (this.issue.id !== this.detail.issue.id) { if (this.issue.id !== this.detail.issue.id) {
$('.js-issue-board-sidebar', this.$el).each((i, el) => { $('.js-issue-board-sidebar', this.$el).each((i, el) => {
$(el).data('glDropdown').clearMenu(); $(el).data('glDropdown').clearMenu();
});
}
this.issue = this.detail.issue;
this.list = this.detail.list;
},
deep: true
},
issue () {
if (this.showSidebar) {
this.$nextTick(() => {
$('.right-sidebar').getNiceScroll(0).doScrollTop(0, 0);
$('.right-sidebar').getNiceScroll().resize();
}); });
} }
this.issue = this.detail.issue;
this.list = this.detail.list;
},
deep: true
},
issue () {
if (this.showSidebar) {
this.$nextTick(() => {
$('.right-sidebar').getNiceScroll(0).doScrollTop(0, 0);
$('.right-sidebar').getNiceScroll().resize();
});
} }
}, }
methods: { },
closeSidebar () { methods: {
this.detail.issue = {}; closeSidebar () {
} this.detail.issue = {};
}, }
mounted () { },
new IssuableContext(this.currentUser); mounted () {
new MilestoneSelect(); new IssuableContext(this.currentUser);
new gl.DueDateSelectors(); new MilestoneSelect();
new LabelsSelect(); new gl.DueDateSelectors();
new Sidebar(); new LabelsSelect();
gl.Subscription.bindAll('.subscription'); new Sidebar();
}, gl.Subscription.bindAll('.subscription');
components: { },
removeBtn: gl.issueBoards.RemoveIssueBtn, components: {
}, removeBtn: gl.issueBoards.RemoveIssueBtn,
}); },
})(); });

View File

@ -1,141 +1,139 @@
import Vue from 'vue'; import Vue from 'vue';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
(() => { const Store = gl.issueBoards.BoardsStore;
const Store = gl.issueBoards.BoardsStore;
window.gl = window.gl || {}; window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {}; window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.IssueCardInner = Vue.extend({ gl.issueBoards.IssueCardInner = Vue.extend({
props: { props: {
issue: { issue: {
type: Object, type: Object,
required: true, required: true,
},
issueLinkBase: {
type: String,
required: true,
},
list: {
type: Object,
required: false,
default: () => ({}),
},
rootPath: {
type: String,
required: true,
},
updateFilters: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { issueLinkBase: {
cardUrl() { type: String,
return `${this.issueLinkBase}/${this.issue.id}`; required: true,
},
assigneeUrl() {
return `${this.rootPath}${this.issue.assignee.username}`;
},
assigneeUrlTitle() {
return `Assigned to ${this.issue.assignee.name}`;
},
avatarUrlTitle() {
return `Avatar for ${this.issue.assignee.name}`;
},
issueId() {
return `#${this.issue.id}`;
},
showLabelFooter() {
return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
},
}, },
methods: { list: {
showLabel(label) { type: Object,
if (!this.list) return true; required: false,
default: () => ({}),
return !this.list.label || label.id !== this.list.label.id;
},
filterByLabel(label, e) {
if (!this.updateFilters) return;
const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&');
const labelTitle = encodeURIComponent(label.title);
const param = `label_name[]=${labelTitle}`;
const labelIndex = filterPath.indexOf(param);
$(e.currentTarget).tooltip('hide');
if (labelIndex === -1) {
filterPath.push(param);
} else {
filterPath.splice(labelIndex, 1);
}
gl.issueBoards.BoardsStore.filter.path = filterPath.join('&');
Store.updateFiltersUrl();
eventHub.$emit('updateTokens');
},
labelStyle(label) {
return {
backgroundColor: label.color,
color: label.textColor,
};
},
}, },
template: ` rootPath: {
<div> type: String,
<div class="card-header"> required: true,
<h4 class="card-title"> },
<i updateFilters: {
class="fa fa-eye-slash confidential-icon" type: Boolean,
v-if="issue.confidential" required: false,
aria-hidden="true" default: false,
/> },
<a },
class="js-no-trigger" computed: {
:href="cardUrl" cardUrl() {
:title="issue.title">{{ issue.title }}</a> return `${this.issueLinkBase}/${this.issue.id}`;
<span },
class="card-number" assigneeUrl() {
v-if="issue.id" return `${this.rootPath}${this.issue.assignee.username}`;
> },
{{ issueId }} assigneeUrlTitle() {
</span> return `Assigned to ${this.issue.assignee.name}`;
</h4> },
avatarUrlTitle() {
return `Avatar for ${this.issue.assignee.name}`;
},
issueId() {
return `#${this.issue.id}`;
},
showLabelFooter() {
return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
},
},
methods: {
showLabel(label) {
if (!this.list) return true;
return !this.list.label || label.id !== this.list.label.id;
},
filterByLabel(label, e) {
if (!this.updateFilters) return;
const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&');
const labelTitle = encodeURIComponent(label.title);
const param = `label_name[]=${labelTitle}`;
const labelIndex = filterPath.indexOf(param);
$(e.currentTarget).tooltip('hide');
if (labelIndex === -1) {
filterPath.push(param);
} else {
filterPath.splice(labelIndex, 1);
}
gl.issueBoards.BoardsStore.filter.path = filterPath.join('&');
Store.updateFiltersUrl();
eventHub.$emit('updateTokens');
},
labelStyle(label) {
return {
backgroundColor: label.color,
color: label.textColor,
};
},
},
template: `
<div>
<div class="card-header">
<h4 class="card-title">
<i
class="fa fa-eye-slash confidential-icon"
v-if="issue.confidential"
aria-hidden="true"
/>
<a <a
class="card-assignee has-tooltip js-no-trigger" class="js-no-trigger"
:href="assigneeUrl" :href="cardUrl"
:title="assigneeUrlTitle" :title="issue.title">{{ issue.title }}</a>
v-if="issue.assignee" <span
data-container="body" class="card-number"
v-if="issue.id"
> >
<img {{ issueId }}
class="avatar avatar-inline s20 js-no-trigger" </span>
:src="issue.assignee.avatar" </h4>
width="20" <a
height="20" class="card-assignee has-tooltip js-no-trigger"
:alt="avatarUrlTitle" :href="assigneeUrl"
/> :title="assigneeUrlTitle"
</a> v-if="issue.assignee"
</div> data-container="body"
<div class="card-footer" v-if="showLabelFooter"> >
<button <img
class="label color-label has-tooltip js-no-trigger" class="avatar avatar-inline s20 js-no-trigger"
v-for="label in issue.labels" :src="issue.assignee.avatar"
type="button" width="20"
v-if="showLabel(label)" height="20"
@click="filterByLabel(label, $event)" :alt="avatarUrlTitle"
:style="labelStyle(label)" />
:title="label.description" </a>
data-container="body">
{{ label.title }}
</button>
</div>
</div> </div>
`, <div class="card-footer" v-if="showLabelFooter">
}); <button
})(); class="label color-label has-tooltip js-no-trigger"
v-for="label in issue.labels"
type="button"
v-if="showLabel(label)"
@click="filterByLabel(label, $event)"
:style="labelStyle(label)"
:title="label.description"
data-container="body">
{{ label.title }}
</button>
</div>
</div>
`,
});

View File

@ -1,71 +1,69 @@
import Vue from 'vue'; import Vue from 'vue';
(() => { const ModalStore = gl.issueBoards.ModalStore;
const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.ModalEmptyState = Vue.extend({ gl.issueBoards.ModalEmptyState = Vue.extend({
mixins: [gl.issueBoards.ModalMixins], mixins: [gl.issueBoards.ModalMixins],
data() { data() {
return ModalStore.store; return ModalStore.store;
},
props: {
image: {
type: String,
required: true,
}, },
props: { newIssuePath: {
image: { type: String,
type: String, required: true,
required: true,
},
newIssuePath: {
type: String,
required: true,
},
}, },
computed: { },
contents() { computed: {
const obj = { contents() {
title: 'You haven\'t added any issues to your project yet', const obj = {
content: ` title: 'You haven\'t added any issues to your project yet',
An issue can be a bug, a todo or a feature request that needs to be content: `
discussed in a project. Besides, issues are searchable and filterable. An issue can be a bug, a todo or a feature request that needs to be
`, discussed in a project. Besides, issues are searchable and filterable.
}; `,
};
if (this.activeTab === 'selected') { if (this.activeTab === 'selected') {
obj.title = 'You haven\'t selected any issues yet'; obj.title = 'You haven\'t selected any issues yet';
obj.content = ` obj.content = `
Go back to <strong>Open issues</strong> and select some issues Go back to <strong>Open issues</strong> and select some issues
to add to your board. to add to your board.
`; `;
} }
return obj; return obj;
},
}, },
template: ` },
<section class="empty-state"> template: `
<div class="row"> <section class="empty-state">
<div class="col-xs-12 col-sm-6 col-sm-push-6"> <div class="row">
<aside class="svg-content" v-html="image"></aside> <div class="col-xs-12 col-sm-6 col-sm-push-6">
</div> <aside class="svg-content" v-html="image"></aside>
<div class="col-xs-12 col-sm-6 col-sm-pull-6"> </div>
<div class="text-content"> <div class="col-xs-12 col-sm-6 col-sm-pull-6">
<h4>{{ contents.title }}</h4> <div class="text-content">
<p v-html="contents.content"></p> <h4>{{ contents.title }}</h4>
<a <p v-html="contents.content"></p>
:href="newIssuePath" <a
class="btn btn-success btn-inverted" :href="newIssuePath"
v-if="activeTab === 'all'"> class="btn btn-success btn-inverted"
New issue v-if="activeTab === 'all'">
</a> New issue
<button </a>
type="button" <button
class="btn btn-default" type="button"
@click="changeTab('all')" class="btn btn-default"
v-if="activeTab === 'selected'"> @click="changeTab('all')"
Open issues v-if="activeTab === 'selected'">
</button> Open issues
</div> </button>
</div> </div>
</div> </div>
</section> </div>
`, </section>
}); `,
})(); });

View File

@ -5,80 +5,78 @@ import Vue from 'vue';
require('./lists_dropdown'); require('./lists_dropdown');
(() => { const ModalStore = gl.issueBoards.ModalStore;
const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.ModalFooter = Vue.extend({ gl.issueBoards.ModalFooter = Vue.extend({
mixins: [gl.issueBoards.ModalMixins], mixins: [gl.issueBoards.ModalMixins],
data() { data() {
return { return {
modal: ModalStore.store, modal: ModalStore.store,
state: gl.issueBoards.BoardsStore.state, state: gl.issueBoards.BoardsStore.state,
}; };
},
computed: {
submitDisabled() {
return !ModalStore.selectedCount();
}, },
computed: { submitText() {
submitDisabled() { const count = ModalStore.selectedCount();
return !ModalStore.selectedCount();
},
submitText() {
const count = ModalStore.selectedCount();
return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`; return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`;
},
}, },
methods: { },
addIssues() { methods: {
const list = this.modal.selectedList || this.state.lists[0]; addIssues() {
const selectedIssues = ModalStore.getSelectedIssues(); const list = this.modal.selectedList || this.state.lists[0];
const issueIds = selectedIssues.map(issue => issue.globalId); const selectedIssues = ModalStore.getSelectedIssues();
const issueIds = selectedIssues.map(issue => issue.globalId);
// 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(() => { }).catch(() => {
new Flash('Failed to update issues, please try again.', 'alert'); 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
selectedIssues.forEach((issue) => { selectedIssues.forEach((issue) => {
list.addIssue(issue); list.removeIssue(issue);
list.issuesSize += 1; list.issuesSize -= 1;
}); });
});
this.toggleModal(false); // Add the issues on the frontend
}, selectedIssues.forEach((issue) => {
list.addIssue(issue);
list.issuesSize += 1;
});
this.toggleModal(false);
}, },
components: { },
'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown, components: {
}, 'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown,
template: ` },
<footer template: `
class="form-actions add-issues-footer"> <footer
<div class="pull-left"> class="form-actions add-issues-footer">
<button <div class="pull-left">
class="btn btn-success"
type="button"
:disabled="submitDisabled"
@click="addIssues">
{{ submitText }}
</button>
<span class="inline add-issues-footer-to-list">
to list
</span>
<lists-dropdown></lists-dropdown>
</div>
<button <button
class="btn btn-default pull-right" class="btn btn-success"
type="button" type="button"
@click="toggleModal(false)"> :disabled="submitDisabled"
Cancel @click="addIssues">
{{ submitText }}
</button> </button>
</footer> <span class="inline add-issues-footer-to-list">
`, to list
}); </span>
})(); <lists-dropdown></lists-dropdown>
</div>
<button
class="btn btn-default pull-right"
type="button"
@click="toggleModal(false)">
Cancel
</button>
</footer>
`,
});

View File

@ -3,80 +3,78 @@ import modalFilters from './filters';
require('./tabs'); require('./tabs');
(() => { const ModalStore = gl.issueBoards.ModalStore;
const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.ModalHeader = Vue.extend({ gl.issueBoards.ModalHeader = Vue.extend({
mixins: [gl.issueBoards.ModalMixins], mixins: [gl.issueBoards.ModalMixins],
props: { props: {
projectId: { projectId: {
type: Number, type: Number,
required: true, required: true,
},
milestonePath: {
type: String,
required: true,
},
labelPath: {
type: String,
required: true,
},
}, },
data() { milestonePath: {
return ModalStore.store; type: String,
required: true,
}, },
computed: { labelPath: {
selectAllText() { type: String,
if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) { required: true,
return 'Select all'; },
} },
data() {
return ModalStore.store;
},
computed: {
selectAllText() {
if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
return 'Select all';
}
return 'Deselect all'; return 'Deselect all';
},
showSearch() {
return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
},
}, },
methods: { showSearch() {
toggleAll() { return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
this.$refs.selectAllBtn.blur(); },
},
methods: {
toggleAll() {
this.$refs.selectAllBtn.blur();
ModalStore.toggleAll(); ModalStore.toggleAll();
},
}, },
components: { },
'modal-tabs': gl.issueBoards.ModalTabs, components: {
modalFilters, 'modal-tabs': gl.issueBoards.ModalTabs,
}, modalFilters,
template: ` },
<div> template: `
<header class="add-issues-header form-actions"> <div>
<h2> <header class="add-issues-header form-actions">
Add issues <h2>
<button Add issues
type="button"
class="close"
data-dismiss="modal"
aria-label="Close"
@click="toggleModal(false)">
<span aria-hidden="true">×</span>
</button>
</h2>
</header>
<modal-tabs v-if="!loading && issuesCount > 0"></modal-tabs>
<div
class="add-issues-search append-bottom-10"
v-if="showSearch">
<modal-filters :store="filter" />
<button <button
type="button" type="button"
class="btn btn-success btn-inverted prepend-left-10" class="close"
ref="selectAllBtn" data-dismiss="modal"
@click="toggleAll"> aria-label="Close"
{{ selectAllText }} @click="toggleModal(false)">
<span aria-hidden="true">×</span>
</button> </button>
</div> </h2>
</header>
<modal-tabs v-if="!loading && issuesCount > 0"></modal-tabs>
<div
class="add-issues-search append-bottom-10"
v-if="showSearch">
<modal-filters :store="filter" />
<button
type="button"
class="btn btn-success btn-inverted prepend-left-10"
ref="selectAllBtn"
@click="toggleAll">
{{ selectAllText }}
</button>
</div> </div>
`, </div>
}); `,
})(); });

View File

@ -8,160 +8,158 @@ require('./list');
require('./footer'); require('./footer');
require('./empty_state'); require('./empty_state');
(() => { const ModalStore = gl.issueBoards.ModalStore;
const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.IssuesModal = Vue.extend({ gl.issueBoards.IssuesModal = Vue.extend({
props: { props: {
blankStateImage: { blankStateImage: {
type: String, type: String,
required: true, required: true,
},
newIssuePath: {
type: String,
required: true,
},
issueLinkBase: {
type: String,
required: true,
},
rootPath: {
type: String,
required: true,
},
projectId: {
type: Number,
required: true,
},
milestonePath: {
type: String,
required: true,
},
labelPath: {
type: String,
required: true,
},
}, },
data() { newIssuePath: {
return ModalStore.store; type: String,
required: true,
}, },
watch: { issueLinkBase: {
page() { type: String,
this.loadIssues(); required: true,
},
showAddIssuesModal() {
if (this.showAddIssuesModal && !this.issues.length) {
this.loading = true;
this.loadIssues()
.then(() => {
this.loading = false;
});
} else if (!this.showAddIssuesModal) {
this.issues = [];
this.selectedIssues = [];
this.issuesCount = false;
}
},
filter: {
handler() {
if (this.$el.tagName) {
this.page = 1;
this.filterLoading = true;
this.loadIssues(true)
.then(() => {
this.filterLoading = false;
});
}
},
deep: true,
},
}, },
methods: { rootPath: {
loadIssues(clearIssues = false) { type: String,
if (!this.showAddIssuesModal) return false; required: true,
},
projectId: {
type: Number,
required: true,
},
milestonePath: {
type: String,
required: true,
},
labelPath: {
type: String,
required: true,
},
},
data() {
return ModalStore.store;
},
watch: {
page() {
this.loadIssues();
},
showAddIssuesModal() {
if (this.showAddIssuesModal && !this.issues.length) {
this.loading = true;
return gl.boardService.getBacklog(queryData(this.filter.path, { this.loadIssues()
page: this.page, .then(() => {
per: this.perPage, this.loading = false;
})).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;
this.issues.push(issue);
}); });
} else if (!this.showAddIssuesModal) {
this.issues = [];
this.selectedIssues = [];
this.issuesCount = false;
}
},
filter: {
handler() {
if (this.$el.tagName) {
this.page = 1;
this.filterLoading = true;
this.loadingNewPage = false; this.loadIssues(true)
.then(() => {
this.filterLoading = false;
});
}
},
deep: true,
},
},
methods: {
loadIssues(clearIssues = false) {
if (!this.showAddIssuesModal) return false;
if (!this.issuesCount) { return gl.boardService.getBacklog(queryData(this.filter.path, {
this.issuesCount = data.size; page: this.page,
} per: this.perPage,
})).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;
this.issues.push(issue);
}); });
},
},
computed: {
showList() {
if (this.activeTab === 'selected') {
return this.selectedIssues.length > 0;
}
return this.issuesCount > 0; this.loadingNewPage = false;
},
showEmptyState() {
if (!this.loading && this.issuesCount === 0) {
return true;
}
return this.activeTab === 'selected' && this.selectedIssues.length === 0; if (!this.issuesCount) {
}, this.issuesCount = data.size;
}
});
}, },
created() { },
this.page = 1; computed: {
showList() {
if (this.activeTab === 'selected') {
return this.selectedIssues.length > 0;
}
return this.issuesCount > 0;
}, },
components: { showEmptyState() {
'modal-header': gl.issueBoards.ModalHeader, if (!this.loading && this.issuesCount === 0) {
'modal-list': gl.issueBoards.ModalList, return true;
'modal-footer': gl.issueBoards.ModalFooter, }
'empty-state': gl.issueBoards.ModalEmptyState,
return this.activeTab === 'selected' && this.selectedIssues.length === 0;
}, },
template: ` },
<div created() {
class="add-issues-modal" this.page = 1;
v-if="showAddIssuesModal"> },
<div class="add-issues-container"> components: {
<modal-header 'modal-header': gl.issueBoards.ModalHeader,
:project-id="projectId" 'modal-list': gl.issueBoards.ModalList,
:milestone-path="milestonePath" 'modal-footer': gl.issueBoards.ModalFooter,
:label-path="labelPath"> 'empty-state': gl.issueBoards.ModalEmptyState,
</modal-header> },
<modal-list template: `
:image="blankStateImage" <div
:issue-link-base="issueLinkBase" class="add-issues-modal"
:root-path="rootPath" v-if="showAddIssuesModal">
v-if="!loading && showList && !filterLoading"></modal-list> <div class="add-issues-container">
<empty-state <modal-header
v-if="showEmptyState" :project-id="projectId"
:image="blankStateImage" :milestone-path="milestonePath"
:new-issue-path="newIssuePath"></empty-state> :label-path="labelPath">
<section </modal-header>
class="add-issues-list text-center" <modal-list
v-if="loading || filterLoading"> :image="blankStateImage"
<div class="add-issues-list-loading"> :issue-link-base="issueLinkBase"
<i class="fa fa-spinner fa-spin"></i> :root-path="rootPath"
</div> v-if="!loading && showList && !filterLoading"></modal-list>
</section> <empty-state
<modal-footer></modal-footer> v-if="showEmptyState"
</div> :image="blankStateImage"
:new-issue-path="newIssuePath"></empty-state>
<section
class="add-issues-list text-center"
v-if="loading || filterLoading">
<div class="add-issues-list-loading">
<i class="fa fa-spinner fa-spin"></i>
</div>
</section>
<modal-footer></modal-footer>
</div> </div>
`, </div>
}); `,
})(); });

View File

@ -3,159 +3,157 @@
import Vue from 'vue'; import Vue from 'vue';
(() => { const ModalStore = gl.issueBoards.ModalStore;
const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.ModalList = Vue.extend({ gl.issueBoards.ModalList = Vue.extend({
props: { props: {
issueLinkBase: { issueLinkBase: {
type: String, type: String,
required: true, required: true,
},
rootPath: {
type: String,
required: true,
},
image: {
type: String,
required: true,
},
}, },
data() { rootPath: {
return ModalStore.store; type: String,
required: true,
}, },
watch: { image: {
activeTab() { type: String,
if (this.activeTab === 'all') { required: true,
ModalStore.purgeUnselectedIssues();
}
},
}, },
computed: { },
loopIssues() { data() {
if (this.activeTab === 'all') { return ModalStore.store;
return this.issues; },
watch: {
activeTab() {
if (this.activeTab === 'all') {
ModalStore.purgeUnselectedIssues();
}
},
},
computed: {
loopIssues() {
if (this.activeTab === 'all') {
return this.issues;
}
return this.selectedIssues;
},
groupedIssues() {
const groups = [];
this.loopIssues.forEach((issue, i) => {
const index = i % this.columns;
if (!groups[index]) {
groups.push([]);
} }
return this.selectedIssues; groups[index].push(issue);
}, });
groupedIssues() {
const groups = [];
this.loopIssues.forEach((issue, i) => {
const index = i % this.columns;
if (!groups[index]) { return groups;
groups.push([]);
}
groups[index].push(issue);
});
return groups;
},
}, },
methods: { },
scrollHandler() { methods: {
const currentPage = Math.floor(this.issues.length / this.perPage); scrollHandler() {
const currentPage = Math.floor(this.issues.length / this.perPage);
if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage
&& currentPage === this.page) { && currentPage === this.page) {
this.loadingNewPage = true; this.loadingNewPage = true;
this.page += 1; this.page += 1;
} }
},
toggleIssue(e, issue) {
if (e.target.tagName !== 'A') {
ModalStore.toggleIssue(issue);
}
},
listHeight() {
return this.$refs.list.getBoundingClientRect().height;
},
scrollHeight() {
return this.$refs.list.scrollHeight;
},
scrollTop() {
return this.$refs.list.scrollTop + this.listHeight();
},
showIssue(issue) {
if (this.activeTab === 'all') return true;
const index = ModalStore.selectedIssueIndex(issue);
return index !== -1;
},
setColumnCount() {
const breakpoint = bp.getBreakpointSize();
if (breakpoint === 'lg' || breakpoint === 'md') {
this.columns = 3;
} else if (breakpoint === 'sm') {
this.columns = 2;
} else {
this.columns = 1;
}
},
}, },
mounted() { toggleIssue(e, issue) {
this.scrollHandlerWrapper = this.scrollHandler.bind(this); if (e.target.tagName !== 'A') {
this.setColumnCountWrapper = this.setColumnCount.bind(this); ModalStore.toggleIssue(issue);
this.setColumnCount(); }
},
listHeight() {
return this.$refs.list.getBoundingClientRect().height;
},
scrollHeight() {
return this.$refs.list.scrollHeight;
},
scrollTop() {
return this.$refs.list.scrollTop + this.listHeight();
},
showIssue(issue) {
if (this.activeTab === 'all') return true;
this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper); const index = ModalStore.selectedIssueIndex(issue);
window.addEventListener('resize', this.setColumnCountWrapper);
return index !== -1;
}, },
beforeDestroy() { setColumnCount() {
this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper); const breakpoint = bp.getBreakpointSize();
window.removeEventListener('resize', this.setColumnCountWrapper);
if (breakpoint === 'lg' || breakpoint === 'md') {
this.columns = 3;
} else if (breakpoint === 'sm') {
this.columns = 2;
} else {
this.columns = 1;
}
}, },
components: { },
'issue-card-inner': gl.issueBoards.IssueCardInner, mounted() {
}, this.scrollHandlerWrapper = this.scrollHandler.bind(this);
template: ` this.setColumnCountWrapper = this.setColumnCount.bind(this);
<section this.setColumnCount();
class="add-issues-list add-issues-list-columns"
ref="list"> this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
window.addEventListener('resize', this.setColumnCountWrapper);
},
beforeDestroy() {
this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
window.removeEventListener('resize', this.setColumnCountWrapper);
},
components: {
'issue-card-inner': gl.issueBoards.IssueCardInner,
},
template: `
<section
class="add-issues-list add-issues-list-columns"
ref="list">
<div
class="empty-state add-issues-empty-state-filter text-center"
v-if="issuesCount > 0 && issues.length === 0">
<div <div
class="empty-state add-issues-empty-state-filter text-center" class="svg-content"
v-if="issuesCount > 0 && issues.length === 0"> v-html="image">
</div>
<div class="text-content">
<h4>
There are no issues to show.
</h4>
</div>
</div>
<div
v-for="group in groupedIssues"
class="add-issues-list-column">
<div
v-for="issue in group"
v-if="showIssue(issue)"
class="card-parent">
<div <div
class="svg-content" class="card"
v-html="image"> :class="{ 'is-active': issue.selected }"
</div> @click="toggleIssue($event, issue)">
<div class="text-content"> <issue-card-inner
<h4> :issue="issue"
There are no issues to show. :issue-link-base="issueLinkBase"
</h4> :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> </div>
<div </div>
v-for="group in groupedIssues" </section>
class="add-issues-list-column"> `,
<div });
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>
`,
});
})();

View File

@ -1,57 +1,55 @@
import Vue from 'vue'; import Vue from 'vue';
(() => { const ModalStore = gl.issueBoards.ModalStore;
const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.ModalFooterListsDropdown = Vue.extend({ gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
data() { data() {
return { return {
modal: ModalStore.store, modal: ModalStore.store,
state: gl.issueBoards.BoardsStore.state, state: gl.issueBoards.BoardsStore.state,
}; };
},
computed: {
selected() {
return this.modal.selectedList || this.state.lists[0];
}, },
computed: { },
selected() { destroyed() {
return this.modal.selectedList || this.state.lists[0]; this.modal.selectedList = null;
}, },
}, template: `
destroyed() { <div class="dropdown inline">
this.modal.selectedList = null; <button
}, class="dropdown-menu-toggle"
template: ` type="button"
<div class="dropdown inline"> data-toggle="dropdown"
<button aria-expanded="false">
class="dropdown-menu-toggle" <span
type="button" class="dropdown-label-box"
data-toggle="dropdown" :style="{ backgroundColor: selected.label.color }">
aria-expanded="false"> </span>
<span {{ selected.title }}
class="dropdown-label-box" <i class="fa fa-chevron-down"></i>
:style="{ backgroundColor: selected.label.color }"> </button>
</span> <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
{{ selected.title }} <ul>
<i class="fa fa-chevron-down"></i> <li
</button> v-for="list in state.lists"
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"> v-if="list.type == 'label'">
<ul> <a
<li href="#"
v-for="list in state.lists" role="button"
v-if="list.type == 'label'"> :class="{ 'is-active': list.id == selected.id }"
<a @click.prevent="modal.selectedList = list">
href="#" <span
role="button" class="dropdown-label-box"
:class="{ 'is-active': list.id == selected.id }" :style="{ backgroundColor: list.label.color }">
@click.prevent="modal.selectedList = list"> </span>
<span {{ list.title }}
class="dropdown-label-box" </a>
:style="{ backgroundColor: list.label.color }"> </li>
</span> </ul>
{{ list.title }}
</a>
</li>
</ul>
</div>
</div> </div>
`, </div>
}); `,
})(); });

View File

@ -1,48 +1,46 @@
import Vue from 'vue'; import Vue from 'vue';
(() => { const ModalStore = gl.issueBoards.ModalStore;
const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.ModalTabs = Vue.extend({ gl.issueBoards.ModalTabs = Vue.extend({
mixins: [gl.issueBoards.ModalMixins], mixins: [gl.issueBoards.ModalMixins],
data() { data() {
return ModalStore.store; return ModalStore.store;
},
computed: {
selectedCount() {
return ModalStore.selectedCount();
}, },
computed: { },
selectedCount() { destroyed() {
return ModalStore.selectedCount(); this.activeTab = 'all';
}, },
}, template: `
destroyed() { <div class="top-area prepend-top-10 append-bottom-10">
this.activeTab = 'all'; <ul class="nav-links issues-state-filters">
}, <li :class="{ 'active': activeTab == 'all' }">
template: ` <a
<div class="top-area prepend-top-10 append-bottom-10"> href="#"
<ul class="nav-links issues-state-filters"> role="button"
<li :class="{ 'active': activeTab == 'all' }"> @click.prevent="changeTab('all')">
<a Open issues
href="#" <span class="badge">
role="button" {{ issuesCount }}
@click.prevent="changeTab('all')"> </span>
Open issues </a>
<span class="badge"> </li>
{{ issuesCount }} <li :class="{ 'active': activeTab == 'selected' }">
</span> <a
</a> href="#"
</li> role="button"
<li :class="{ 'active': activeTab == 'selected' }"> @click.prevent="changeTab('selected')">
<a Selected issues
href="#" <span class="badge">
role="button" {{ selectedCount }}
@click.prevent="changeTab('selected')"> </span>
Selected issues </a>
<span class="badge"> </li>
{{ selectedCount }} </ul>
</span> </div>
</a> `,
</li> });
</ul>
</div>
`,
});
})();

View File

@ -1,76 +1,74 @@
/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var */ /* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var */
(() => { window.gl = window.gl || {};
window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {};
window.gl.issueBoards = window.gl.issueBoards || {};
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
$(document).off('created.label').on('created.label', (e, label) => { $(document).off('created.label').on('created.label', (e, label) => {
Store.new({ Store.new({
title: label.title,
position: Store.state.lists.length - 2,
list_type: 'label',
label: {
id: label.id,
title: label.title, title: label.title,
position: Store.state.lists.length - 2, color: label.color
list_type: 'label', }
label: { });
id: label.id, });
title: label.title,
color: label.color gl.issueBoards.newListDropdownInit = () => {
$('.js-new-board-list').each(function () {
const $this = $(this);
new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
$this.glDropdown({
data(term, callback) {
$.get($this.attr('data-labels'))
.then((resp) => {
callback(resp);
});
},
renderRow (label) {
const active = Store.findList('title', label.title);
const $li = $('<li />');
const $a = $('<a />', {
class: (active ? `is-active js-board-list-${active.id}` : ''),
text: label.title,
href: '#'
});
const $labelColor = $('<span />', {
class: 'dropdown-label-box',
style: `background-color: ${label.color}`
});
return $li.append($a.prepend($labelColor));
},
search: {
fields: ['title']
},
filterable: true,
selectable: true,
multiSelect: true,
clicked (label, $el, e) {
e.preventDefault();
if (!Store.findList('title', label.title)) {
Store.new({
title: label.title,
position: Store.state.lists.length - 2,
list_type: 'label',
label: {
id: label.id,
title: label.title,
color: label.color
}
});
Store.state.lists = _.sortBy(Store.state.lists, 'position');
}
} }
}); });
}); });
};
gl.issueBoards.newListDropdownInit = () => {
$('.js-new-board-list').each(function () {
const $this = $(this);
new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
$this.glDropdown({
data(term, callback) {
$.get($this.attr('data-labels'))
.then((resp) => {
callback(resp);
});
},
renderRow (label) {
const active = Store.findList('title', label.title);
const $li = $('<li />');
const $a = $('<a />', {
class: (active ? `is-active js-board-list-${active.id}` : ''),
text: label.title,
href: '#'
});
const $labelColor = $('<span />', {
class: 'dropdown-label-box',
style: `background-color: ${label.color}`
});
return $li.append($a.prepend($labelColor));
},
search: {
fields: ['title']
},
filterable: true,
selectable: true,
multiSelect: true,
clicked (label, $el, e) {
e.preventDefault();
if (!Store.findList('title', label.title)) {
Store.new({
title: label.title,
position: Store.state.lists.length - 2,
list_type: 'label',
label: {
id: label.id,
title: label.title,
color: label.color
}
});
Store.state.lists = _.sortBy(Store.state.lists, 'position');
}
}
});
});
};
})();

View File

@ -3,59 +3,57 @@
import Vue from 'vue'; import Vue from 'vue';
(() => { const Store = gl.issueBoards.BoardsStore;
const Store = gl.issueBoards.BoardsStore;
window.gl = window.gl || {}; window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {}; window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.RemoveIssueBtn = Vue.extend({ gl.issueBoards.RemoveIssueBtn = Vue.extend({
props: { props: {
issue: { issue: {
type: Object, type: Object,
required: true, required: true,
},
list: {
type: Object,
required: true,
},
}, },
methods: { list: {
removeIssue() { type: Object,
const issue = this.issue; required: true,
const lists = issue.getLists(); },
const labelIds = lists.map(list => list.label.id); },
methods: {
removeIssue() {
const issue = this.issue;
const lists = issue.getLists();
const labelIds = lists.map(list => list.label.id);
// Post the remove data // Post the remove data
gl.boardService.bulkUpdate([issue.globalId], { gl.boardService.bulkUpdate([issue.globalId], {
remove_label_ids: labelIds, remove_label_ids: labelIds,
}).catch(() => { }).catch(() => {
new Flash('Failed to remove issue from board, please try again.', 'alert'); 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) => { lists.forEach((list) => {
list.removeIssue(issue); list.addIssue(issue);
}); });
});
Store.detail.issue = {}; // Remove from the frontend store
}, lists.forEach((list) => {
list.removeIssue(issue);
});
Store.detail.issue = {};
}, },
template: ` },
<div template: `
class="block list" <div
v-if="list.type !== 'closed'"> class="block list"
<button v-if="list.type !== 'closed'">
class="btn btn-default btn-block" <button
type="button" class="btn btn-default btn-block"
@click="removeIssue"> type="button"
Remove from board @click="removeIssue">
</button> Remove from board
</div> </button>
`, </div>
}); `,
})(); });

View File

@ -1,14 +1,12 @@
(() => { const ModalStore = gl.issueBoards.ModalStore;
const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.ModalMixins = { gl.issueBoards.ModalMixins = {
methods: { methods: {
toggleModal(toggle) { toggleModal(toggle) {
ModalStore.store.showAddIssuesModal = toggle; ModalStore.store.showAddIssuesModal = toggle;
},
changeTab(tab) {
ModalStore.store.activeTab = tab;
},
}, },
}; changeTab(tab) {
})(); ModalStore.store.activeTab = tab;
},
},
};

View File

@ -1,39 +1,37 @@
/* eslint-disable no-unused-vars, no-mixed-operators, comma-dangle */ /* eslint-disable no-unused-vars, no-mixed-operators, comma-dangle */
/* global DocumentTouch */ /* global DocumentTouch */
((w) => { window.gl = window.gl || {};
window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {};
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.onStart = () => { gl.issueBoards.onStart = () => {
$('.has-tooltip').tooltip('hide') $('.has-tooltip').tooltip('hide')
.tooltip('disable'); .tooltip('disable');
document.body.classList.add('is-dragging'); document.body.classList.add('is-dragging');
};
gl.issueBoards.onEnd = () => {
$('.has-tooltip').tooltip('enable');
document.body.classList.remove('is-dragging');
};
gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch;
gl.issueBoards.getBoardSortableDefaultOptions = (obj) => {
const defaultSortOptions = {
animation: 200,
forceFallback: true,
fallbackClass: 'is-dragging',
fallbackOnBody: true,
ghostClass: 'is-ghost',
filter: '.board-delete, .btn',
delay: gl.issueBoards.touchEnabled ? 100 : 0,
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
scrollSpeed: 20,
onStart: gl.issueBoards.onStart,
onEnd: gl.issueBoards.onEnd
}; };
gl.issueBoards.onEnd = () => { Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; });
$('.has-tooltip').tooltip('enable'); return defaultSortOptions;
document.body.classList.remove('is-dragging'); };
};
gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch;
gl.issueBoards.getBoardSortableDefaultOptions = (obj) => {
const defaultSortOptions = {
animation: 200,
forceFallback: true,
fallbackClass: 'is-dragging',
fallbackOnBody: true,
ghostClass: 'is-ghost',
filter: '.board-delete, .btn',
delay: gl.issueBoards.touchEnabled ? 100 : 0,
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
scrollSpeed: 20,
onStart: gl.issueBoards.onStart,
onEnd: gl.issueBoards.onEnd
};
Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; });
return defaultSortOptions;
};
})(window);

View File

@ -3,125 +3,123 @@
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
(() => { window.gl = window.gl || {};
window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {};
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardsStore = { gl.issueBoards.BoardsStore = {
disabled: false, disabled: false,
filter: { filter: {
path: '', path: '',
}, },
state: {}, state: {},
detail: { detail: {
issue: {} issue: {}
}, },
moving: { moving: {
issue: {}, issue: {},
list: {} list: {}
}, },
create () { create () {
this.state.lists = []; this.state.lists = [];
this.filter.path = gl.utils.getUrlParamsArray().join('&'); this.filter.path = gl.utils.getUrlParamsArray().join('&');
}, },
addList (listObj) { addList (listObj) {
const list = new List(listObj); const list = new List(listObj);
this.state.lists.push(list); this.state.lists.push(list);
return list; return list;
}, },
new (listObj) { new (listObj) {
const list = this.addList(listObj); const list = this.addList(listObj);
list list
.save() .save()
.then(() => { .then(() => {
this.state.lists = _.sortBy(this.state.lists, 'position'); this.state.lists = _.sortBy(this.state.lists, 'position');
});
this.removeBlankState();
},
updateNewListDropdown (listId) {
$(`.js-board-list-${listId}`).removeClass('is-active');
},
shouldAddBlankState () {
// Decide whether to add the blank state
return !(this.state.lists.filter(list => list.type !== 'closed')[0]);
},
addBlankState () {
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
this.addList({
id: 'blank',
list_type: 'blank',
title: 'Welcome to your Issue Board!',
position: 0
}); });
this.removeBlankState();
},
updateNewListDropdown (listId) {
$(`.js-board-list-${listId}`).removeClass('is-active');
},
shouldAddBlankState () {
// Decide whether to add the blank state
return !(this.state.lists.filter(list => list.type !== 'closed')[0]);
},
addBlankState () {
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
this.state.lists = _.sortBy(this.state.lists, 'position'); this.addList({
}, id: 'blank',
removeBlankState () { list_type: 'blank',
this.removeList('blank'); title: 'Welcome to your Issue Board!',
position: 0
});
Cookies.set('issue_board_welcome_hidden', 'true', { this.state.lists = _.sortBy(this.state.lists, 'position');
expires: 365 * 10, },
path: '' removeBlankState () {
}); this.removeList('blank');
},
welcomeIsHidden () {
return Cookies.get('issue_board_welcome_hidden') === 'true';
},
removeList (id, type = 'blank') {
const list = this.findList('id', id, type);
if (!list) return; Cookies.set('issue_board_welcome_hidden', 'true', {
expires: 365 * 10,
path: ''
});
},
welcomeIsHidden () {
return Cookies.get('issue_board_welcome_hidden') === 'true';
},
removeList (id, type = 'blank') {
const list = this.findList('id', id, type);
this.state.lists = this.state.lists.filter(list => list.id !== id); if (!list) return;
},
moveList (listFrom, orderLists) {
orderLists.forEach((id, i) => {
const list = this.findList('id', parseInt(id, 10));
list.position = i; this.state.lists = this.state.lists.filter(list => list.id !== id);
}); },
listFrom.update(); moveList (listFrom, orderLists) {
}, orderLists.forEach((id, i) => {
moveIssueToList (listFrom, listTo, issue, newIndex) { const list = this.findList('id', parseInt(id, 10));
const issueTo = listTo.findIssue(issue.id);
const issueLists = issue.getLists();
const listLabels = issueLists.map(listIssue => listIssue.label);
if (!issueTo) { list.position = i;
// Add to new lists issues if it doesn't already exist });
listTo.addIssue(issue, listFrom, newIndex); listFrom.update();
} else { },
listTo.updateIssueLabel(issue, listFrom); moveIssueToList (listFrom, listTo, issue, newIndex) {
issueTo.removeLabel(listFrom.label); const issueTo = listTo.findIssue(issue.id);
} const issueLists = issue.getLists();
const listLabels = issueLists.map(listIssue => listIssue.label);
if (listTo.type === 'closed') { if (!issueTo) {
issueLists.forEach((list) => { // Add to new lists issues if it doesn't already exist
list.removeIssue(issue); listTo.addIssue(issue, listFrom, newIndex);
}); } else {
issue.removeLabels(listLabels); listTo.updateIssueLabel(issue, listFrom);
} else { issueTo.removeLabel(listFrom.label);
listFrom.removeIssue(issue);
}
},
moveIssueInList (list, issue, oldIndex, newIndex, idArray) {
const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
const afterId = parseInt(idArray[newIndex + 1], 10) || null;
list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId);
},
findList (key, val, type = 'label') {
return this.state.lists.filter((list) => {
const byType = type ? list['type'] === type : true;
return list[key] === val && byType;
})[0];
},
updateFiltersUrl () {
history.pushState(null, null, `?${this.filter.path}`);
} }
};
})(); if (listTo.type === 'closed') {
issueLists.forEach((list) => {
list.removeIssue(issue);
});
issue.removeLabels(listLabels);
} else {
listFrom.removeIssue(issue);
}
},
moveIssueInList (list, issue, oldIndex, newIndex, idArray) {
const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
const afterId = parseInt(idArray[newIndex + 1], 10) || null;
list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId);
},
findList (key, val, type = 'label') {
return this.state.lists.filter((list) => {
const byType = type ? list['type'] === type : true;
return list[key] === val && byType;
})[0];
},
updateFiltersUrl () {
history.pushState(null, null, `?${this.filter.path}`);
}
};

View File

@ -1,100 +1,98 @@
(() => { window.gl = window.gl || {};
window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {};
window.gl.issueBoards = window.gl.issueBoards || {};
class ModalStore { class ModalStore {
constructor() { constructor() {
this.store = { this.store = {
columns: 3, columns: 3,
issues: [], issues: [],
issuesCount: false, issuesCount: false,
selectedIssues: [], selectedIssues: [],
showAddIssuesModal: false, showAddIssuesModal: false,
activeTab: 'all', activeTab: 'all',
selectedList: null, selectedList: null,
searchTerm: '', searchTerm: '',
loading: false, loading: false,
loadingNewPage: false, loadingNewPage: false,
filterLoading: false, filterLoading: false,
page: 1, page: 1,
perPage: 50, perPage: 50,
filter: { filter: {
path: '', path: '',
}, },
}; };
} }
selectedCount() { selectedCount() {
return this.getSelectedIssues().length; return this.getSelectedIssues().length;
} }
toggleIssue(issueObj) { toggleIssue(issueObj) {
const issue = issueObj; const issue = issueObj;
const selected = issue.selected; const selected = issue.selected;
issue.selected = !selected; issue.selected = !selected;
if (!selected) { if (!selected) {
this.addSelectedIssue(issue); this.addSelectedIssue(issue);
} else { } else {
this.removeSelectedIssue(issue); this.removeSelectedIssue(issue);
}
}
toggleAll() {
const select = this.selectedCount() !== this.store.issues.length;
this.store.issues.forEach((issue) => {
const issueUpdate = issue;
if (issueUpdate.selected !== select) {
issueUpdate.selected = select;
if (select) {
this.addSelectedIssue(issue);
} else {
this.removeSelectedIssue(issue);
}
}
});
}
getSelectedIssues() {
return this.store.selectedIssues.filter(issue => issue.selected);
}
addSelectedIssue(issue) {
const index = this.selectedIssueIndex(issue);
if (index === -1) {
this.store.selectedIssues.push(issue);
}
}
removeSelectedIssue(issue, forcePurge = false) {
if (this.store.activeTab === 'all' || forcePurge) {
this.store.selectedIssues = this.store.selectedIssues
.filter(fIssue => fIssue.id !== issue.id);
}
}
purgeUnselectedIssues() {
this.store.selectedIssues.forEach((issue) => {
if (!issue.selected) {
this.removeSelectedIssue(issue, true);
}
});
}
selectedIssueIndex(issue) {
return this.store.selectedIssues.indexOf(issue);
}
findSelectedIssue(issue) {
return this.store.selectedIssues
.filter(filteredIssue => filteredIssue.id === issue.id)[0];
} }
} }
gl.issueBoards.ModalStore = new ModalStore(); toggleAll() {
})(); const select = this.selectedCount() !== this.store.issues.length;
this.store.issues.forEach((issue) => {
const issueUpdate = issue;
if (issueUpdate.selected !== select) {
issueUpdate.selected = select;
if (select) {
this.addSelectedIssue(issue);
} else {
this.removeSelectedIssue(issue);
}
}
});
}
getSelectedIssues() {
return this.store.selectedIssues.filter(issue => issue.selected);
}
addSelectedIssue(issue) {
const index = this.selectedIssueIndex(issue);
if (index === -1) {
this.store.selectedIssues.push(issue);
}
}
removeSelectedIssue(issue, forcePurge = false) {
if (this.store.activeTab === 'all' || forcePurge) {
this.store.selectedIssues = this.store.selectedIssues
.filter(fIssue => fIssue.id !== issue.id);
}
}
purgeUnselectedIssues() {
this.store.selectedIssues.forEach((issue) => {
if (!issue.selected) {
this.removeSelectedIssue(issue, true);
}
});
}
selectedIssueIndex(issue) {
return this.store.selectedIssues.indexOf(issue);
}
findSelectedIssue(issue) {
return this.store.selectedIssues
.filter(filteredIssue => filteredIssue.id === issue.id)[0];
}
}
gl.issueBoards.ModalStore = new ModalStore();

View File

@ -1,93 +1,90 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, guard-for-in, no-restricted-syntax, prefer-template, quotes, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, guard-for-in, no-restricted-syntax, prefer-template, quotes, max-len */
(function() { var base;
(function(w) { var w = window;
var base; if (w.gl == null) {
if (w.gl == null) { w.gl = {};
w.gl = {}; }
if ((base = w.gl).utils == null) {
base.utils = {};
}
// Returns an array containing the value(s) of the
// of the key passed as an argument
w.gl.utils.getParameterValues = function(sParam) {
var i, sPageURL, sParameterName, sURLVariables, values;
sPageURL = decodeURIComponent(window.location.search.substring(1));
sURLVariables = sPageURL.split('&');
sParameterName = void 0;
values = [];
i = 0;
while (i < sURLVariables.length) {
sParameterName = sURLVariables[i].split('=');
if (sParameterName[0] === sParam) {
values.push(sParameterName[1].replace(/\+/g, ' '));
} }
if ((base = w.gl).utils == null) { i += 1;
base.utils = {}; }
return values;
};
// @param {Object} params - url keys and value to merge
// @param {String} url
w.gl.utils.mergeUrlParams = function(params, url) {
var lastChar, newUrl, paramName, paramValue, pattern;
newUrl = decodeURIComponent(url);
for (paramName in params) {
paramValue = params[paramName];
pattern = new RegExp("\\b(" + paramName + "=).*?(&|$)");
if (paramValue == null) {
newUrl = newUrl.replace(pattern, '');
} else if (url.search(pattern) !== -1) {
newUrl = newUrl.replace(pattern, "$1" + paramValue + "$2");
} else {
newUrl = "" + newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + "=" + paramValue;
} }
// Returns an array containing the value(s) of the }
// of the key passed as an argument // Remove a trailing ampersand
w.gl.utils.getParameterValues = function(sParam) { lastChar = newUrl[newUrl.length - 1];
var i, sPageURL, sParameterName, sURLVariables, values; if (lastChar === '&') {
sPageURL = decodeURIComponent(window.location.search.substring(1)); newUrl = newUrl.slice(0, -1);
sURLVariables = sPageURL.split('&'); }
sParameterName = void 0; return newUrl;
values = []; };
i = 0; // removes parameter query string from url. returns the modified url
while (i < sURLVariables.length) { w.gl.utils.removeParamQueryString = function(url, param) {
sParameterName = sURLVariables[i].split('='); var urlVariables, variables;
if (sParameterName[0] === sParam) { url = decodeURIComponent(url);
values.push(sParameterName[1].replace(/\+/g, ' ')); urlVariables = url.split('&');
} return ((function() {
i += 1; var j, len, results;
results = [];
for (j = 0, len = urlVariables.length; j < len; j += 1) {
variables = urlVariables[j];
if (variables.indexOf(param) === -1) {
results.push(variables);
} }
return values; }
}; return results;
// @param {Object} params - url keys and value to merge })()).join('&');
// @param {String} url };
w.gl.utils.mergeUrlParams = function(params, url) { w.gl.utils.removeParams = (params) => {
var lastChar, newUrl, paramName, paramValue, pattern; const url = new URL(window.location.href);
newUrl = decodeURIComponent(url); params.forEach((param) => {
for (paramName in params) { url.search = w.gl.utils.removeParamQueryString(url.search, param);
paramValue = params[paramName]; });
pattern = new RegExp("\\b(" + paramName + "=).*?(&|$)"); return url.href;
if (paramValue == null) { };
newUrl = newUrl.replace(pattern, ''); w.gl.utils.getLocationHash = function(url) {
} else if (url.search(pattern) !== -1) { var hashIndex;
newUrl = newUrl.replace(pattern, "$1" + paramValue + "$2"); if (typeof url === 'undefined') {
} else { // Note: We can't use window.location.hash here because it's
newUrl = "" + newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + "=" + paramValue; // not consistent across browsers - Firefox will pre-decode it
} url = window.location.href;
} }
// Remove a trailing ampersand hashIndex = url.indexOf('#');
lastChar = newUrl[newUrl.length - 1]; return hashIndex === -1 ? null : url.substring(hashIndex + 1);
if (lastChar === '&') { };
newUrl = newUrl.slice(0, -1);
}
return newUrl;
};
// removes parameter query string from url. returns the modified url
w.gl.utils.removeParamQueryString = function(url, param) {
var urlVariables, variables;
url = decodeURIComponent(url);
urlVariables = url.split('&');
return ((function() {
var j, len, results;
results = [];
for (j = 0, len = urlVariables.length; j < len; j += 1) {
variables = urlVariables[j];
if (variables.indexOf(param) === -1) {
results.push(variables);
}
}
return results;
})()).join('&');
};
w.gl.utils.removeParams = (params) => {
const url = new URL(window.location.href);
params.forEach((param) => {
url.search = w.gl.utils.removeParamQueryString(url.search, param);
});
return url.href;
};
w.gl.utils.getLocationHash = function(url) {
var hashIndex;
if (typeof url === 'undefined') {
// Note: We can't use window.location.hash here because it's
// not consistent across browsers - Firefox will pre-decode it
url = window.location.href;
}
hashIndex = url.indexOf('#');
return hashIndex === -1 ? null : url.substring(hashIndex + 1);
};
w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href); w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href);
w.gl.utils.visitUrl = (url) => { w.gl.utils.visitUrl = (url) => {
document.location.href = url; document.location.href = url;
}; };
})(window);
}).call(window);