summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/boards
diff options
context:
space:
mode:
authorMario de la Ossa <mdelaossa@gitlab.com>2018-06-07 20:54:24 +0000
committerSean McGivern <sean@mcgivern.me.uk>2018-06-07 20:54:24 +0000
commitcf41aaba5ab9fb1d229807f77b2b77585d3550b0 (patch)
tree37405b49c29180d4fbf53efb3117163505ca0309 /app/assets/javascripts/boards
parentd4357afde55436a0fa3702d8d11bb8ae8a214381 (diff)
downloadgitlab-ce-cf41aaba5ab9fb1d229807f77b2b77585d3550b0.tar.gz
Backport of "Add assignee lists to boards"
Diffstat (limited to 'app/assets/javascripts/boards')
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue41
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue4
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js1
-rw-r--r--app/assets/javascripts/boards/index.js2
-rw-r--r--app/assets/javascripts/boards/models/assignee.js12
-rw-r--r--app/assets/javascripts/boards/models/list.js87
-rw-r--r--app/assets/javascripts/boards/services/board_service.js10
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js24
8 files changed, 122 insertions, 59 deletions
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 84a7f277227..0692c96e767 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -87,10 +87,46 @@ export default {
mounted() {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
scroll: true,
- group: 'issues',
disabled: this.disabled,
filter: '.board-list-count, .is-disabled',
dataIdAttr: 'data-issue-id',
+ group: {
+ name: 'issues',
+ /**
+ * Dynamically determine between which containers
+ * items can be moved or copied as
+ * Assignee lists (EE feature) require this behavior
+ */
+ pull: (to, from, dragEl, e) => {
+ // As per Sortable's docs, `to` should provide
+ // reference to exact sortable container on which
+ // we're trying to drag element, but either it is
+ // a library's bug or our markup structure is too complex
+ // that `to` never points to correct container
+ // See https://github.com/RubaXa/Sortable/issues/1037
+ //
+ // So we use `e.target` which is always accurate about
+ // which element we're currently dragging our card upon
+ // So from there, we can get reference to actual container
+ // and thus the container type to enable Copy or Move
+ if (e.target) {
+ const containerEl = e.target.closest('.js-board-list') || e.target.querySelector('.js-board-list');
+ const toBoardType = containerEl.dataset.boardType;
+
+ if (toBoardType) {
+ const fromBoardType = this.list.type;
+
+ if ((fromBoardType === 'assignee' && toBoardType === 'label') ||
+ (fromBoardType === 'label' && toBoardType === 'assignee')) {
+ return 'clone';
+ }
+ }
+ }
+
+ return true;
+ },
+ revertClone: true,
+ },
onStart: (e) => {
const card = this.$refs.issue[e.oldIndex];
@@ -179,10 +215,11 @@ export default {
:list="list"
v-if="list.type !== 'closed' && showIssueForm"/>
<ul
- class="board-list"
+ class="board-list js-board-list"
v-show="!loading"
ref="list"
:data-board="list.id"
+ :data-board-type="list.type"
:class="{ 'is-smaller': showIssueForm }">
<board-card
v-for="(issue, index) in issues"
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index e8dfd95f7ae..297c9eff38c 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -49,11 +49,12 @@ export default {
this.error = false;
const labels = this.list.label ? [this.list.label] : [];
+ const assignees = this.list.assignee ? [this.list.assignee] : [];
const issue = new ListIssue({
title: this.title,
labels,
subscribed: true,
- assignees: [],
+ assignees,
project_id: this.selectedProject.id,
});
@@ -141,4 +142,3 @@ export default {
</div>
</div>
</template>
-
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js
index 71f49319c36..6dcd4aaec43 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js
@@ -56,6 +56,7 @@ gl.issueBoards.newListDropdownInit = () => {
filterable: true,
selectable: true,
multiSelect: true,
+ containerSelector: '.js-tab-container-labels .dropdown-page-one .dropdown-content',
clicked (options) {
const { e } = options;
const label = options.selectedObj;
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 29ab13b8e0b..cdad8d238e3 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -7,6 +7,7 @@ import Vue from 'vue';
import Flash from '~/flash';
import { __ } from '~/locale';
import '~/vue_shared/models/label';
+import '~/vue_shared/models/assignee';
import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub';
@@ -15,7 +16,6 @@ import './models/issue';
import './models/list';
import './models/milestone';
import './models/project';
-import './models/assignee';
import './stores/boards_store';
import ModalStore from './stores/modal_store';
import BoardService from './services/board_service';
diff --git a/app/assets/javascripts/boards/models/assignee.js b/app/assets/javascripts/boards/models/assignee.js
deleted file mode 100644
index 05dd449e4fd..00000000000
--- a/app/assets/javascripts/boards/models/assignee.js
+++ /dev/null
@@ -1,12 +0,0 @@
-/* eslint-disable no-unused-vars */
-
-class ListAssignee {
- constructor(user, defaultAvatar) {
- this.id = user.id;
- this.name = user.name;
- this.username = user.username;
- this.avatar = user.avatar_url || defaultAvatar;
- }
-}
-
-window.ListAssignee = ListAssignee;
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index 7144f4190e7..a79dd62e2e4 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -1,12 +1,14 @@
/* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len, no-unused-vars */
/* global ListIssue */
-/* global ListLabel */
+
+import ListLabel from '~/vue_shared/models/label';
+import ListAssignee from '~/vue_shared/models/assignee';
import queryData from '../utils/query_data';
const PER_PAGE = 20;
class List {
- constructor (obj, defaultAvatar) {
+ constructor(obj, defaultAvatar) {
this.id = obj.id;
this._uid = this.guid();
this.position = obj.position;
@@ -24,6 +26,9 @@ class List {
if (obj.label) {
this.label = new ListLabel(obj.label);
+ } else if (obj.user) {
+ this.assignee = new ListAssignee(obj.user);
+ this.title = this.assignee.name;
}
if (this.type !== 'blank' && this.id) {
@@ -34,14 +39,25 @@ class List {
}
guid() {
- const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
+ const s4 = () =>
+ Math.floor((1 + Math.random()) * 0x10000)
+ .toString(16)
+ .substring(1);
return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`;
}
- save () {
+ save() {
+ const entity = this.label || this.assignee;
+ let entityType = '';
+ if (this.label) {
+ entityType = 'label_id';
+ } else {
+ entityType = 'assignee_id';
+ }
+
return gl.boardService.createList(this.label.id)
.then(res => res.data)
- .then((data) => {
+ .then(data => {
this.id = data.id;
this.type = data.list_type;
this.position = data.position;
@@ -50,25 +66,23 @@ class List {
});
}
- destroy () {
+ destroy() {
const index = gl.issueBoards.BoardsStore.state.lists.indexOf(this);
gl.issueBoards.BoardsStore.state.lists.splice(index, 1);
gl.issueBoards.BoardsStore.updateNewListDropdown(this.id);
- gl.boardService.destroyList(this.id)
- .catch(() => {
- // TODO: handle request error
- });
+ gl.boardService.destroyList(this.id).catch(() => {
+ // TODO: handle request error
+ });
}
- update () {
- gl.boardService.updateList(this.id, this.position)
- .catch(() => {
- // TODO: handle request error
- });
+ update() {
+ gl.boardService.updateList(this.id, this.position).catch(() => {
+ // TODO: handle request error
+ });
}
- nextPage () {
+ nextPage() {
if (this.issuesSize > this.issues.length) {
if (this.issues.length / PER_PAGE >= 1) {
this.page += 1;
@@ -78,7 +92,7 @@ class List {
}
}
- getIssues (emptyIssues = true) {
+ getIssues(emptyIssues = true) {
const data = queryData(gl.issueBoards.BoardsStore.filter.path, { page: this.page });
if (this.label && data.label_name) {
@@ -89,7 +103,8 @@ class List {
this.loading = true;
}
- return gl.boardService.getIssuesForList(this.id, data)
+ return gl.boardService
+ .getIssuesForList(this.id, data)
.then(res => res.data)
.then((data) => {
this.loading = false;
@@ -103,11 +118,12 @@ class List {
});
}
- newIssue (issue) {
+ newIssue(issue) {
this.addIssue(issue, null, 0);
this.issuesSize += 1;
- return gl.boardService.newIssue(this.id, issue)
+ return gl.boardService
+ .newIssue(this.id, issue)
.then(res => res.data)
.then((data) => {
issue.id = data.id;
@@ -123,13 +139,13 @@ class List {
});
}
- createIssues (data) {
- data.forEach((issueObj) => {
+ createIssues(data) {
+ data.forEach(issueObj => {
this.addIssue(new ListIssue(issueObj, this.defaultAvatar));
});
}
- addIssue (issue, listFrom, newIndex) {
+ addIssue(issue, listFrom, newIndex) {
let moveBeforeId = null;
let moveAfterId = null;
@@ -152,6 +168,13 @@ class List {
issue.addLabel(this.label);
}
+ if (this.assignee) {
+ if (listFrom && listFrom.type === 'assignee') {
+ issue.removeAssignee(listFrom.assignee);
+ }
+ issue.addAssignee(this.assignee);
+ }
+
if (listFrom) {
this.issuesSize += 1;
@@ -160,29 +183,29 @@ class List {
}
}
- moveIssue (issue, oldIndex, newIndex, moveBeforeId, moveAfterId) {
+ moveIssue(issue, oldIndex, newIndex, moveBeforeId, moveAfterId) {
this.issues.splice(oldIndex, 1);
this.issues.splice(newIndex, 0, issue);
- gl.boardService.moveIssue(issue.id, null, null, moveBeforeId, moveAfterId)
- .catch(() => {
- // TODO: handle request error
- });
+ gl.boardService.moveIssue(issue.id, null, null, moveBeforeId, moveAfterId).catch(() => {
+ // TODO: handle request error
+ });
}
updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId) {
- gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId)
+ gl.boardService
+ .moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId)
.catch(() => {
// TODO: handle request error
});
}
- findIssue (id) {
+ findIssue(id) {
return this.issues.find(issue => issue.id === id);
}
- removeIssue (removeIssue) {
- this.issues = this.issues.filter((issue) => {
+ removeIssue(removeIssue) {
+ this.issues = this.issues.filter(issue => {
const matchesRemove = removeIssue.id === issue.id;
if (matchesRemove) {
diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js
index 7c90597f77c..029b0971f2c 100644
--- a/app/assets/javascripts/boards/services/board_service.js
+++ b/app/assets/javascripts/boards/services/board_service.js
@@ -30,11 +30,13 @@ export default class BoardService {
return axios.post(this.listsEndpointGenerate, {});
}
- createList(labelId) {
+ createList(entityId, entityType) {
+ const list = {
+ [entityType]: entityId,
+ };
+
return axios.post(this.listsEndpoint, {
- list: {
- label_id: labelId,
- },
+ list,
});
}
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 20e78edf2a2..7dc83843e9b 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -103,8 +103,15 @@ gl.issueBoards.BoardsStore = {
const listLabels = issueLists.map(listIssue => listIssue.label);
if (!issueTo) {
- // Add to new lists issues if it doesn't already exist
- listTo.addIssue(issue, listFrom, newIndex);
+ // Check if target list assignee is already present in this issue
+ if ((listTo.type === 'assignee' && listFrom.type === 'assignee') &&
+ issue.findAssignee(listTo.assignee)) {
+ const targetIssue = listTo.findIssue(issue.id);
+ targetIssue.removeAssignee(listFrom.assignee);
+ } else {
+ // Add to new lists issues if it doesn't already exist
+ listTo.addIssue(issue, listFrom, newIndex);
+ }
} else {
listTo.updateIssueLabel(issue, listFrom);
issueTo.removeLabel(listFrom.label);
@@ -115,7 +122,11 @@ gl.issueBoards.BoardsStore = {
list.removeIssue(issue);
});
issue.removeLabels(listLabels);
- } else {
+ } else if (listTo.type === 'backlog' && listFrom.type === 'assignee') {
+ issue.removeAssignee(listFrom.assignee);
+ listFrom.removeIssue(issue);
+ } else if ((listTo.type !== 'label' && listFrom.type === 'assignee') ||
+ (listTo.type !== 'assignee' && listFrom.type === 'label')) {
listFrom.removeIssue(issue);
}
},
@@ -126,11 +137,12 @@ gl.issueBoards.BoardsStore = {
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;
+ const filteredList = this.state.lists.filter((list) => {
+ const byType = type ? (list.type === type) || (list.type === 'assignee') : true;
return list[key] === val && byType;
- })[0];
+ });
+ return filteredList[0];
},
updateFiltersUrl () {
history.pushState(null, null, `?${this.filter.path}`);