Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Greiling <mike@pixelcog.com>2017-02-03 22:17:03 +0300
committerMike Greiling <mike@pixelcog.com>2017-02-03 22:17:03 +0300
commit04dc2b76d7e382a6b6369576753cfc20928c99b8 (patch)
treeb9062e9886f1913a634e505755e284d7073f2eaa /app/assets/javascripts/boards
parent152b292d0b566547875a44470c76e9a43cb28a36 (diff)
parent538a6875d3b53f909e9b5b6ef396a457cbcae760 (diff)
Merge branch 'master' into go-go-gadget-webpack
* master: (181 commits) Fixed adding to list bug Remove unnecessary queries for .atom and .json in Dashboard::ProjectsController#index Fixed modal lists dropdown not updating when list is deleted Fixed remove btn error after creating new issue in list Removed duplicated test Removed Masonry, instead uses groups of data Uses mixins for repeated functions Fixed up specs Props use objects with required & type values Removes labels instead of closing issue when clicking remove button Fixed JS lint errors Fixed issue card spec Added webkit CSS properties Fixed bug with empty state showing after search Fixed users href path being incorrect Fixed bug where 2 un-selected issues would stay on selected tab Fixed DB schema Changed how components are added in objects Added remove button Add optional id property to the issue schema Fixed issue link href Disabled add issues button if no lists exist ...
Diffstat (limited to 'app/assets/javascripts/boards')
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js.es634
-rw-r--r--app/assets/javascripts/boards/components/board.js.es63
-rw-r--r--app/assets/javascripts/boards/components/board_card.js.es634
-rw-r--r--app/assets/javascripts/boards/components/board_list.js.es61
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.js.es61
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js.es611
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.js.es6111
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.js.es670
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.js.es683
-rw-r--r--app/assets/javascripts/boards/components/modal/header.js.es670
-rw-r--r--app/assets/javascripts/boards/components/modal/index.js.es6136
-rw-r--r--app/assets/javascripts/boards/components/modal/list.js.es6142
-rw-r--r--app/assets/javascripts/boards/components/modal/lists_dropdown.js.es656
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.js.es647
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.js.es659
-rw-r--r--app/assets/javascripts/boards/mixins/modal_mixins.js.es614
-rw-r--r--app/assets/javascripts/boards/models/issue.js.es63
-rw-r--r--app/assets/javascripts/boards/models/list.js.es62
-rw-r--r--app/assets/javascripts/boards/services/board_service.js.es629
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js.es69
-rw-r--r--app/assets/javascripts/boards/stores/modal_store.js.es696
21 files changed, 968 insertions, 43 deletions
diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6
index 8c7f59652ca..1053d167131 100644
--- a/app/assets/javascripts/boards/boards_bundle.js.es6
+++ b/app/assets/javascripts/boards/boards_bundle.js.es6
@@ -15,11 +15,13 @@ requireAll(require.context('./filters', true, /^\.\/.*\.(js|es6)$/));
require('./components/board');
require('./components/board_sidebar');
require('./components/new_list_dropdown');
+require('./components/modal/index');
require('./vue_resource_interceptor');
$(() => {
const $boardApp = document.getElementById('board-app');
const Store = gl.issueBoards.BoardsStore;
+ const ModalStore = gl.issueBoards.ModalStore;
window.gl = window.gl || {};
@@ -33,7 +35,8 @@ $(() => {
el: $boardApp,
components: {
'board': gl.issueBoards.Board,
- 'board-sidebar': gl.issueBoards.BoardSidebar
+ 'board-sidebar': gl.issueBoards.BoardSidebar,
+ 'board-add-issues-modal': gl.issueBoards.IssuesModal,
},
data: {
state: Store.state,
@@ -42,6 +45,8 @@ $(() => {
boardId: $boardApp.dataset.boardId,
disabled: $boardApp.dataset.disabled === 'true',
issueLinkBase: $boardApp.dataset.issueLinkBase,
+ rootPath: $boardApp.dataset.rootPath,
+ bulkUpdatePath: $boardApp.dataset.bulkUpdatePath,
detailIssue: Store.detail
},
computed: {
@@ -50,7 +55,7 @@ $(() => {
},
},
created () {
- gl.boardService = new BoardService(this.endpoint, this.boardId);
+ gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId);
},
mounted () {
Store.disabled = this.disabled;
@@ -61,8 +66,6 @@ $(() => {
if (list.type === 'done') {
list.position = Infinity;
- } else if (list.type === 'backlog') {
- list.position = -1;
}
});
@@ -83,4 +86,27 @@ $(() => {
gl.issueBoards.newListDropdownInit();
}
});
+
+ gl.IssueBoardsModalAddBtn = new Vue({
+ mixins: [gl.issueBoards.ModalMixins],
+ el: '#js-add-issues-btn',
+ data: {
+ modal: ModalStore.store,
+ store: Store.state,
+ },
+ computed: {
+ disabled() {
+ return Store.shouldAddBlankState();
+ },
+ },
+ template: `
+ <button
+ class="btn btn-create pull-right prepend-left-10 has-tooltip"
+ type="button"
+ :disabled="disabled"
+ @click="toggleModal(true)">
+ Add issues
+ </button>
+ `,
+ });
});
diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6
index d0dbbb17d3e..18324de18b3 100644
--- a/app/assets/javascripts/boards/components/board.js.es6
+++ b/app/assets/javascripts/boards/components/board.js.es6
@@ -22,7 +22,8 @@ require('./board_list');
props: {
list: Object,
disabled: Boolean,
- issueLinkBase: String
+ issueLinkBase: String,
+ rootPath: String,
},
data () {
return {
diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6
index 5fc50280811..0ea66bd027c 100644
--- a/app/assets/javascripts/boards/components/board_card.js.es6
+++ b/app/assets/javascripts/boards/components/board_card.js.es6
@@ -1,6 +1,8 @@
/* eslint-disable comma-dangle, space-before-function-paren, dot-notation */
/* global Vue */
+require('./issue_card_inner');
+
(() => {
const Store = gl.issueBoards.BoardsStore;
@@ -9,12 +11,16 @@
gl.issueBoards.BoardCard = Vue.extend({
template: '#js-board-list-card',
+ components: {
+ 'issue-card-inner': gl.issueBoards.IssueCardInner,
+ },
props: {
list: Object,
issue: Object,
issueLinkBase: String,
disabled: Boolean,
- index: Number
+ index: Number,
+ rootPath: String,
},
data () {
return {
@@ -28,31 +34,6 @@
}
},
methods: {
- filterByLabel (label, e) {
- let labelToggleText = label.title;
- const labelIndex = Store.state.filters['label_name'].indexOf(label.title);
- $(e.target).tooltip('hide');
-
- if (labelIndex === -1) {
- Store.state.filters['label_name'].push(label.title);
- $('.labels-filter').prepend(`<input type="hidden" name="label_name[]" value="${label.title}" />`);
- } else {
- Store.state.filters['label_name'].splice(labelIndex, 1);
- labelToggleText = Store.state.filters['label_name'][0];
- $(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove();
- }
-
- const selectedLabels = Store.state.filters['label_name'];
- if (selectedLabels.length === 0) {
- labelToggleText = 'Label';
- } else if (selectedLabels.length > 1) {
- labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`;
- }
-
- $('.labels-filter .dropdown-toggle-text').text(labelToggleText);
-
- Store.updateFiltersUrl();
- },
mouseDown () {
this.showDetail = true;
},
@@ -71,6 +52,7 @@
Store.detail.issue = {};
} else {
Store.detail.issue = this.issue;
+ Store.detail.list = this.list;
}
}
}
diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6
index 57059cef444..60b0a30af3f 100644
--- a/app/assets/javascripts/boards/components/board_list.js.es6
+++ b/app/assets/javascripts/boards/components/board_list.js.es6
@@ -23,6 +23,7 @@ require('./board_new_issue');
issues: Array,
loading: Boolean,
issueLinkBase: String,
+ rootPath: String,
},
data () {
return {
diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6
index 2386d3a613c..b5c14a198ba 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.js.es6
+++ b/app/assets/javascripts/boards/components/board_new_issue.js.es6
@@ -37,6 +37,7 @@
$(this.$refs.submitButton).enable();
Store.detail.issue = issue;
+ Store.detail.list = this.list;
})
.catch(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js.es6
index 75dfcb66bb0..dfc6eed785c 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js.es6
+++ b/app/assets/javascripts/boards/components/board_sidebar.js.es6
@@ -5,6 +5,8 @@
/* global LabelsSelect */
/* global Sidebar */
+require('./sidebar/remove_issue');
+
(() => {
const Store = gl.issueBoards.BoardsStore;
@@ -18,7 +20,8 @@
data() {
return {
detail: Store.detail,
- issue: {}
+ issue: {},
+ list: {},
};
},
computed: {
@@ -36,6 +39,7 @@
}
this.issue = this.detail.issue;
+ this.list = this.detail.list;
},
deep: true
},
@@ -60,6 +64,9 @@
new LabelsSelect();
new Sidebar();
gl.Subscription.bindAll('.subscription');
- }
+ },
+ components: {
+ removeBtn: gl.issueBoards.RemoveIssueBtn,
+ },
});
})();
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js.es6 b/app/assets/javascripts/boards/components/issue_card_inner.js.es6
new file mode 100644
index 00000000000..22a8b971ff8
--- /dev/null
+++ b/app/assets/javascripts/boards/components/issue_card_inner.js.es6
@@ -0,0 +1,111 @@
+/* global Vue */
+(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.IssueCardInner = Vue.extend({
+ props: {
+ issue: {
+ type: Object,
+ required: true,
+ },
+ issueLinkBase: {
+ type: String,
+ required: true,
+ },
+ list: {
+ type: Object,
+ required: false,
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ showLabel(label) {
+ if (!this.list) return true;
+
+ return !this.list.label || label.id !== this.list.label.id;
+ },
+ filterByLabel(label, e) {
+ let labelToggleText = label.title;
+ const labelIndex = Store.state.filters.label_name.indexOf(label.title);
+ $(e.currentTarget).tooltip('hide');
+
+ if (labelIndex === -1) {
+ Store.state.filters.label_name.push(label.title);
+ $('.labels-filter').prepend(`<input type="hidden" name="label_name[]" value="${label.title}" />`);
+ } else {
+ Store.state.filters.label_name.splice(labelIndex, 1);
+ labelToggleText = Store.state.filters.label_name[0];
+ $(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove();
+ }
+
+ const selectedLabels = Store.state.filters.label_name;
+ if (selectedLabels.length === 0) {
+ labelToggleText = 'Label';
+ } else if (selectedLabels.length > 1) {
+ labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`;
+ }
+
+ $('.labels-filter .dropdown-toggle-text').text(labelToggleText);
+
+ Store.updateFiltersUrl();
+ },
+ labelStyle(label) {
+ return {
+ backgroundColor: label.color,
+ color: label.textColor,
+ };
+ },
+ },
+ template: `
+ <div>
+ <h4 class="card-title">
+ <i
+ class="fa fa-eye-slash confidential-icon"
+ v-if="issue.confidential"></i>
+ <a
+ :href="issueLinkBase + '/' + issue.id"
+ :title="issue.title">
+ {{ issue.title }}
+ </a>
+ </h4>
+ <div class="card-footer">
+ <span
+ class="card-number"
+ v-if="issue.id">
+ #{{ issue.id }}
+ </span>
+ <a
+ class="card-assignee has-tooltip"
+ :href="rootPath + issue.assignee.username"
+ :title="'Assigned to ' + issue.assignee.name"
+ v-if="issue.assignee"
+ data-container="body">
+ <img
+ class="avatar avatar-inline s20"
+ :src="issue.assignee.avatar"
+ width="20"
+ height="20"
+ :alt="'Avatar for ' + issue.assignee.name" />
+ </a>
+ <button
+ class="label color-label has-tooltip"
+ 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>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js.es6 b/app/assets/javascripts/boards/components/modal/empty_state.js.es6
new file mode 100644
index 00000000000..9538f5b69e9
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/empty_state.js.es6
@@ -0,0 +1,70 @@
+/* global Vue */
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.ModalEmptyState = Vue.extend({
+ mixins: [gl.issueBoards.ModalMixins],
+ data() {
+ return ModalStore.store;
+ },
+ props: {
+ image: {
+ type: String,
+ required: true,
+ },
+ newIssuePath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ contents() {
+ const obj = {
+ title: 'You haven\'t added any issues to your project yet',
+ content: `
+ 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') {
+ obj.title = 'You haven\'t selected any issues yet';
+ obj.content = `
+ Go back to <strong>All issues</strong> and select some issues
+ to add to your board.
+ `;
+ }
+
+ return obj;
+ },
+ },
+ template: `
+ <section class="empty-state">
+ <div class="row">
+ <div class="col-xs-12 col-sm-6 col-sm-push-6">
+ <aside class="svg-content" v-html="image"></aside>
+ </div>
+ <div class="col-xs-12 col-sm-6 col-sm-pull-6">
+ <div class="text-content">
+ <h4>{{ contents.title }}</h4>
+ <p v-html="contents.content"></p>
+ <a
+ :href="newIssuePath"
+ class="btn btn-success btn-inverted"
+ v-if="activeTab === 'all'">
+ New issue
+ </a>
+ <button
+ type="button"
+ class="btn btn-default"
+ @click="changeTab('all')"
+ v-if="activeTab === 'selected'">
+ All issues
+ </button>
+ </div>
+ </div>
+ </div>
+ </section>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js.es6
new file mode 100644
index 00000000000..1cbc422c961
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/footer.js.es6
@@ -0,0 +1,83 @@
+/* eslint-disable no-new */
+/* global Vue */
+/* global Flash */
+
+require('./lists_dropdown');
+
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.ModalFooter = Vue.extend({
+ mixins: [gl.issueBoards.ModalMixins],
+ data() {
+ return {
+ modal: ModalStore.store,
+ state: gl.issueBoards.BoardsStore.state,
+ };
+ },
+ computed: {
+ submitDisabled() {
+ return !ModalStore.selectedCount();
+ },
+ submitText() {
+ const count = ModalStore.selectedCount();
+
+ return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`;
+ },
+ },
+ methods: {
+ addIssues() {
+ const list = this.modal.selectedList || this.state.lists[0];
+ const selectedIssues = ModalStore.getSelectedIssues();
+ const issueIds = selectedIssues.map(issue => issue.globalId);
+
+ // 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
+ selectedIssues.forEach((issue) => {
+ list.addIssue(issue);
+ list.issuesSize += 1;
+ });
+
+ this.toggleModal(false);
+ },
+ },
+ components: {
+ 'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown,
+ },
+ template: `
+ <footer
+ class="form-actions add-issues-footer">
+ <div class="pull-left">
+ <button
+ 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
+ class="btn btn-default pull-right"
+ type="button"
+ @click="toggleModal(false)">
+ Cancel
+ </button>
+ </footer>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6
new file mode 100644
index 00000000000..ab903722ba4
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/header.js.es6
@@ -0,0 +1,70 @@
+/* global Vue */
+
+require('./tabs');
+
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.ModalHeader = Vue.extend({
+ mixins: [gl.issueBoards.ModalMixins],
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ selectAllText() {
+ if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
+ return 'Select all';
+ }
+
+ return 'Deselect all';
+ },
+ showSearch() {
+ return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
+ },
+ },
+ methods: {
+ toggleAll() {
+ this.$refs.selectAllBtn.blur();
+
+ ModalStore.toggleAll();
+ },
+ },
+ components: {
+ 'modal-tabs': gl.issueBoards.ModalTabs,
+ },
+ template: `
+ <div>
+ <header class="add-issues-header form-actions">
+ <h2>
+ Add issues
+ <button
+ 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">
+ <input
+ placeholder="Search issues..."
+ class="form-control"
+ type="search"
+ v-model="searchTerm" />
+ <button
+ type="button"
+ class="btn btn-success btn-inverted prepend-left-10"
+ ref="selectAllBtn"
+ @click="toggleAll">
+ {{ selectAllText }}
+ </button>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6
new file mode 100644
index 00000000000..d367b7e4246
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/index.js.es6
@@ -0,0 +1,136 @@
+/* global Vue */
+/* global ListIssue */
+
+require('./header');
+require('./list');
+require('./footer');
+require('./empty_state');
+
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.IssuesModal = Vue.extend({
+ props: {
+ blankStateImage: {
+ type: String,
+ required: true,
+ },
+ newIssuePath: {
+ type: String,
+ required: true,
+ },
+ issueLinkBase: {
+ type: String,
+ required: true,
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return ModalStore.store;
+ },
+ watch: {
+ page() {
+ this.loadIssues();
+ },
+ searchTerm() {
+ this.searchOperation();
+ },
+ 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;
+ }
+ },
+ },
+ methods: {
+ searchOperation: _.debounce(function searchOperationDebounce() {
+ this.loadIssues(true);
+ }, 500),
+ loadIssues(clearIssues = false) {
+ return gl.boardService.getBacklog({
+ search: this.searchTerm,
+ 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);
+ });
+
+ this.loadingNewPage = false;
+
+ if (!this.issuesCount) {
+ this.issuesCount = data.size;
+ }
+ });
+ },
+ },
+ computed: {
+ showList() {
+ if (this.activeTab === 'selected') {
+ return this.selectedIssues.length > 0;
+ }
+
+ 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.ModalHeader,
+ 'modal-list': gl.issueBoards.ModalList,
+ 'modal-footer': gl.issueBoards.ModalFooter,
+ 'empty-state': gl.issueBoards.ModalEmptyState,
+ },
+ template: `
+ <div
+ class="add-issues-modal"
+ v-if="showAddIssuesModal">
+ <div class="add-issues-container">
+ <modal-header></modal-header>
+ <modal-list
+ :issue-link-base="issueLinkBase"
+ :root-path="rootPath"
+ v-if="!loading && showList"></modal-list>
+ <empty-state
+ v-if="showEmptyState"
+ :image="blankStateImage"
+ :new-issue-path="newIssuePath"></empty-state>
+ <section
+ class="add-issues-list text-center"
+ v-if="loading">
+ <div class="add-issues-list-loading">
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>
+ </section>
+ <modal-footer></modal-footer>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6
new file mode 100644
index 00000000000..d0901219216
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/list.js.es6
@@ -0,0 +1,142 @@
+/* global Vue */
+/* global ListIssue */
+/* global bp */
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.ModalList = Vue.extend({
+ props: {
+ issueLinkBase: {
+ type: String,
+ required: true,
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return ModalStore.store;
+ },
+ 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([]);
+ }
+
+ 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);
+ }
+ },
+ 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() {
+ this.scrollHandlerWrapper = this.scrollHandler.bind(this);
+ this.setColumnCountWrapper = this.setColumnCount.bind(this);
+ this.setColumnCount();
+
+ 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
+ v-for="group in groupedIssues"
+ 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>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 b/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6
new file mode 100644
index 00000000000..3c05120a2da
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6
@@ -0,0 +1,56 @@
+/* global Vue */
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
+ data() {
+ return {
+ modal: ModalStore.store,
+ state: gl.issueBoards.BoardsStore.state,
+ };
+ },
+ computed: {
+ selected() {
+ return this.modal.selectedList || this.state.lists[0];
+ },
+ },
+ destroyed() {
+ this.modal.selectedList = null;
+ },
+ template: `
+ <div class="dropdown inline">
+ <button
+ class="dropdown-menu-toggle"
+ type="button"
+ data-toggle="dropdown"
+ aria-expanded="false">
+ <span
+ class="dropdown-label-box"
+ :style="{ backgroundColor: selected.label.color }">
+ </span>
+ {{ selected.title }}
+ <i class="fa fa-chevron-down"></i>
+ </button>
+ <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
+ <ul>
+ <li
+ v-for="list in state.lists"
+ v-if="list.type == 'label'">
+ <a
+ href="#"
+ role="button"
+ :class="{ 'is-active': list.id == selected.id }"
+ @click.prevent="modal.selectedList = list">
+ <span
+ class="dropdown-label-box"
+ :style="{ backgroundColor: list.label.color }">
+ </span>
+ {{ list.title }}
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/tabs.js.es6 b/app/assets/javascripts/boards/components/modal/tabs.js.es6
new file mode 100644
index 00000000000..e8cb43f3503
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/tabs.js.es6
@@ -0,0 +1,47 @@
+/* global Vue */
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.ModalTabs = Vue.extend({
+ mixins: [gl.issueBoards.ModalMixins],
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ selectedCount() {
+ return ModalStore.selectedCount();
+ },
+ },
+ destroyed() {
+ this.activeTab = 'all';
+ },
+ template: `
+ <div class="top-area prepend-top-10 append-bottom-10">
+ <ul class="nav-links issues-state-filters">
+ <li :class="{ 'active': activeTab == 'all' }">
+ <a
+ href="#"
+ role="button"
+ @click.prevent="changeTab('all')">
+ All issues
+ <span class="badge">
+ {{ issuesCount }}
+ </span>
+ </a>
+ </li>
+ <li :class="{ 'active': activeTab == 'selected' }">
+ <a
+ href="#"
+ role="button"
+ @click.prevent="changeTab('selected')">
+ Selected issues
+ <span class="badge">
+ {{ selectedCount }}
+ </span>
+ </a>
+ </li>
+ </ul>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 b/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6
new file mode 100644
index 00000000000..e74935e1cb0
--- /dev/null
+++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6
@@ -0,0 +1,59 @@
+/* eslint-disable no-new */
+/* global Vue */
+/* global Flash */
+(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.RemoveIssueBtn = Vue.extend({
+ props: {
+ issue: {
+ type: Object,
+ required: true,
+ },
+ list: {
+ type: Object,
+ required: true,
+ },
+ },
+ methods: {
+ removeIssue() {
+ const issue = this.issue;
+ const lists = issue.getLists();
+ const labelIds = lists.map(list => list.label.id);
+
+ // Post the remove data
+ 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(issue);
+ });
+
+ Store.detail.issue = {};
+ },
+ },
+ template: `
+ <div
+ class="block list"
+ v-if="list.type !== 'done'">
+ <button
+ class="btn btn-default btn-block"
+ type="button"
+ @click="removeIssue">
+ Remove from board
+ </button>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/mixins/modal_mixins.js.es6 b/app/assets/javascripts/boards/mixins/modal_mixins.js.es6
new file mode 100644
index 00000000000..d378b7d4baf
--- /dev/null
+++ b/app/assets/javascripts/boards/mixins/modal_mixins.js.es6
@@ -0,0 +1,14 @@
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.ModalMixins = {
+ methods: {
+ toggleModal(toggle) {
+ ModalStore.store.showAddIssuesModal = toggle;
+ },
+ changeTab(tab) {
+ ModalStore.store.activeTab = tab;
+ },
+ },
+ };
+})();
diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6
index 31531c3ee34..2d0a295ae4d 100644
--- a/app/assets/javascripts/boards/models/issue.js.es6
+++ b/app/assets/javascripts/boards/models/issue.js.es6
@@ -6,12 +6,15 @@
class ListIssue {
constructor (obj) {
+ this.globalId = obj.id;
this.id = obj.iid;
this.title = obj.title;
this.confidential = obj.confidential;
this.dueDate = obj.due_date;
this.subscribed = obj.subscribed;
this.labels = [];
+ this.selected = false;
+ this.assignee = false;
if (obj.assignee) {
this.assignee = new ListUser(obj.assignee);
diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6
index 3dd5f273057..5152be56b66 100644
--- a/app/assets/javascripts/boards/models/list.js.es6
+++ b/app/assets/javascripts/boards/models/list.js.es6
@@ -9,7 +9,7 @@ class List {
this.position = obj.position;
this.title = obj.title;
this.type = obj.list_type;
- this.preset = ['backlog', 'done', 'blank'].indexOf(this.type) > -1;
+ this.preset = ['done', 'blank'].indexOf(this.type) > -1;
this.filters = gl.issueBoards.BoardsStore.state.filters;
this.page = 1;
this.loading = true;
diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6
index ea55158306b..065e90518df 100644
--- a/app/assets/javascripts/boards/services/board_service.js.es6
+++ b/app/assets/javascripts/boards/services/board_service.js.es6
@@ -2,7 +2,13 @@
/* global Vue */
class BoardService {
- constructor (root, boardId) {
+ constructor (root, bulkUpdatePath, boardId) {
+ this.boards = Vue.resource(`${root}{/id}.json`, {}, {
+ issues: {
+ method: 'GET',
+ url: `${root}/${boardId}/issues.json`
+ }
+ });
this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, {
generate: {
method: 'POST',
@@ -10,7 +16,12 @@ class BoardService {
}
});
this.issue = Vue.resource(`${root}/${boardId}/issues{/id}`, {});
- this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {});
+ this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {}, {
+ bulkUpdate: {
+ method: 'POST',
+ url: bulkUpdatePath,
+ },
+ });
Vue.http.interceptors.push((request, next) => {
request.headers['X-CSRF-Token'] = $.rails.csrfToken();
@@ -65,6 +76,20 @@ class BoardService {
issue
});
}
+
+ getBacklog(data) {
+ return this.boards.issues(data);
+ }
+
+ bulkUpdate(issueIds, extraData = {}) {
+ const data = {
+ update: Object.assign(extraData, {
+ issuable_ids: issueIds.join(','),
+ }),
+ };
+
+ return this.issues.bulkUpdate(data);
+ }
}
window.BoardService = BoardService;
diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6
index cdf1b09c0a4..50842ecbaaa 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js.es6
+++ b/app/assets/javascripts/boards/stores/boards_store.js.es6
@@ -34,15 +34,10 @@
},
new (listObj) {
const list = this.addList(listObj);
- const backlogList = this.findList('type', 'backlog', 'backlog');
list
.save()
.then(() => {
- // Remove any new issues from the backlog
- // as they will be visible in the new list
- list.issues.forEach(backlogList.removeIssue.bind(backlogList));
-
this.state.lists = _.sortBy(this.state.lists, 'position');
});
this.removeBlankState();
@@ -52,7 +47,7 @@
},
shouldAddBlankState () {
// Decide whether to add the blank state
- return !(this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'done')[0]);
+ return !(this.state.lists.filter(list => list.type !== 'done')[0]);
},
addBlankState () {
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
@@ -102,7 +97,7 @@
listTo.addIssue(issue, listFrom, newIndex);
}
- if (listTo.type === 'done' && listFrom.type !== 'backlog') {
+ if (listTo.type === 'done') {
issueLists.forEach((list) => {
list.removeIssue(issue);
});
diff --git a/app/assets/javascripts/boards/stores/modal_store.js.es6 b/app/assets/javascripts/boards/stores/modal_store.js.es6
new file mode 100644
index 00000000000..73518b42b84
--- /dev/null
+++ b/app/assets/javascripts/boards/stores/modal_store.js.es6
@@ -0,0 +1,96 @@
+(() => {
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ class ModalStore {
+ constructor() {
+ this.store = {
+ columns: 3,
+ issues: [],
+ issuesCount: false,
+ selectedIssues: [],
+ showAddIssuesModal: false,
+ activeTab: 'all',
+ selectedList: null,
+ searchTerm: '',
+ loading: false,
+ loadingNewPage: false,
+ page: 1,
+ perPage: 50,
+ };
+ }
+
+ selectedCount() {
+ return this.getSelectedIssues().length;
+ }
+
+ toggleIssue(issueObj) {
+ const issue = issueObj;
+ const selected = issue.selected;
+
+ issue.selected = !selected;
+
+ if (!selected) {
+ this.addSelectedIssue(issue);
+ } else {
+ 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();
+})();