diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index 6f9eb465dc2d20eb1b526cc5e7e0f6233bdf3faa..12d68256598dead060939af14660c721c4ab8838 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -50,8 +50,7 @@ export default { return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id; }, multiSelectVisible() { - const ids = this.multiSelect.list.map(issue => issue.id); - return ids.indexOf(this.issue.id) !== -1; + return this.multiSelect.list.findIndex(issue => issue.id === this.issue.id) > -1; }, canMultiSelect() { return gon.features && gon.features.multiSelectBoard; @@ -71,12 +70,13 @@ export default { // If CMD or CTRL is clicked const isMultiSelect = this.canMultiSelect && (e.ctrlKey || e.metaKey); - if ( - boardsStore.detail.issue && - boardsStore.detail.issue.id === this.issue.id && - !isMultiSelect - ) { + + if (boardsStore.detail.issue && boardsStore.detail.issue.id === this.issue.id) { eventHub.$emit('clearDetailIssue', isMultiSelect); + + if (isMultiSelect) { + eventHub.$emit('newDetailIssue', this.issue, isMultiSelect); + } } else { eventHub.$emit('newDetailIssue', this.issue, isMultiSelect); boardsStore.setListDetail(this.list); diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index 430db41aeaae0126e5d14b1daf56c7b1cd5b6074..38c008e86c4c2c695d759844d553c5b7a7fbca2a 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -104,6 +104,7 @@ export default { multiSelectOpts.multiDrag = true; multiSelectOpts.selectedClass = 'js-multi-select'; multiSelectOpts.animation = 500; + // multiSelectOpts.multiDragKey = 'shift' } const options = getBoardSortableDefaultOptions({ @@ -166,30 +167,27 @@ export default { const { list } = card; - let issue = list.findIssue(Number(e.item.dataset.issueId)); - - if (e.items && e.items.length) { - issue = []; - e.items.forEach(item => { - issue.push(list.findIssue(Number(item.dataset.issueId))); - }); - } + const issue = list.findIssue(Number(e.item.dataset.issueId)); boardsStore.startMoving(list, issue); sortableStart(); }, onAdd: e => { - if (e.items && e.items.length) { - // Not using e.newIndex here instead taking - // min of all the newIndicies - // Whenever the we select multiple items and drag - const newIndex = Math.min(...e.newIndicies.map(obj => obj.index)); - + const { items = [], newIndicies = [] } = e; + if (items.length) { + // Not using e.newIndex here instead taking a min of all + // the newIndicies. Basically we have to find that during + // a drop which is the index we're going to start putting + // all the dropped elements from. + console.log(newIndicies.map(obj => obj.index)) + const newIndex = Math.min(...newIndicies.map(obj => obj.index).filter(i => i !== -1)); + console.log(newIndex); + const issues = items.map(item => boardsStore.moving.list.findIssue(Number(item.dataset.issueId))); boardsStore.moveMultipleIssuesToList( boardsStore.moving.list, this.list, - boardsStore.moving.issue, + issues, newIndex, ); @@ -221,15 +219,20 @@ export default { onUpdate: e => { const sortedArray = this.sortable.toArray().filter(id => id !== '-1'); - if (e.items && e.items.length) { - const newIndex = Math.min(...e.newIndicies.map(obj => obj.index)); + const { items = [], newIndicies = [], oldIndicies = [] } = e; + if (items.length) { + const newIndex = Math.min(...newIndicies.map(obj => obj.index)); + const issues = items.map(item => boardsStore.moving.list.findIssue(Number(item.dataset.issueId))); boardsStore.moveMultipleIssuesInList( this.list, - boardsStore.moving.issue, - e.oldIndex, + issues, + oldIndicies.map(obj => obj.index), newIndex, sortedArray, ); + e.items.forEach(el => { + Sortable.utils.deselect(el); + }); boardsStore.clearMultiSelect(); return; } @@ -245,6 +248,29 @@ export default { onMove(e) { return !e.related.classList.contains('board-list-count'); }, + onSelect(e) { + const { + item: { classList }, + } = e; + + if ( + classList && + classList.contains('js-multi-select') && + !classList.contains('multi-select') + ) { + Sortable.utils.deselect(e.item); + } + }, + onDeselect: (e) => { + const { + item: { dataset, classList }, + } = e; + + if (classList && classList.contains('multi-select') && !classList.contains('js-multi-select')) { + const issue = this.list.findIssue(Number(dataset.issueId)); + boardsStore.toggleMultiSelect(issue); + } + }, }); this.sortable = Sortable.create(this.$refs.list, options); @@ -289,6 +315,11 @@ export default { this.loadNextPage(); } }, + foo(issues) { + console.log(this.list.title); + console.log(issues.map(issue => issue.id)); + return issues; + } }, }; @@ -316,9 +347,10 @@ export default { class="board-list w-100 h-100 list-unstyled mb-0 p-1 js-board-list" > { clearDetailIssue(multiSelect = false) { if (multiSelect) { boardsStore.clearMultiSelect(); - return; } boardsStore.clearDetailIssue(); }, diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index dc44f6325513d33546a1b452695f80f7f0d8fdd7..bde00d5de942ef687d8876c7e863c9fd934a804f 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -5,6 +5,7 @@ import { __ } from '~/locale'; import ListLabel from './label'; import ListAssignee from './assignee'; import { urlParamsToObject } from '~/lib/utils/common_utils'; +import flash from '~/flash'; import boardsStore from '../stores/boards_store'; import ListMilestone from './milestone'; @@ -193,7 +194,7 @@ class List { this.issues.splice(newIndex, 0, ...issues); } else { - Array.prototype.push.apply(this.issues, issues); + this.issues.push(...issues); } if (this.label) { @@ -276,15 +277,15 @@ class List { }); } - moveMultipleIssues(issues, oldIndex, newIndex, moveBeforeId, moveAfterId) { - this.issues.splice(oldIndex, issues.length); + moveMultipleIssues(issues, oldIndicies, newIndex, moveBeforeId, moveAfterId) { + oldIndicies.reverse().forEach(index => { + this.issues.splice(index, 1); + }); this.issues.splice(newIndex, 0, ...issues); gl.boardService .moveMultipleIssues(issues.map(issue => issue.id), null, null, moveBeforeId, moveAfterId) - .catch(() => { - // TODO: handle request error - }); + .catch(() => flash(__('Something went wrong on our end.'))); } updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId) { @@ -304,15 +305,27 @@ class List { moveBeforeId, moveAfterId, ) - .catch(() => { - // TODO: handle error - }); + .catch(() => flash(__('Something went wrong on our end.'))); } findIssue(id) { return this.issues.find(issue => issue.id === id); } + removeMultipleIssues(removeIssues) { + const ids = removeIssues.map(issue => issue.id); + debugger; + this.issues = this.issues.filter(issue => { + const matchesRemove = ids.findIndex(id => id === issue.id) > -1; + if (matchesRemove) { + this.issuesSize -= 1; + issue.removeLabel(this.label); + } + return !matchesRemove; + }); + console.log(this.issues); + } + removeIssue(removeIssue) { this.issues = this.issues.filter(issue => { const matchesRemove = removeIssue.id === issue.id; diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 0c65178e1fd993309d5ee64b2ad32f86e9951abe..5425bd644292f97804df5294f2a0b6c3b28bf796 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -173,9 +173,14 @@ const boardsStore = { currentList.removeIssue(issue); }); }); - + if (this.shouldRemoveIssue(listFrom, listTo)) { + listFrom.removeMultipleIssues(issues); + } listTo.addMultipleIssues(issues, listFrom, newIndex); } else { + if (this.shouldRemoveIssue(listFrom, listTo)) { + listFrom.removeMultipleIssues(issues); + } // Add to new lists issues if it doesn't already exist listTo.addMultipleIssues(issues, listFrom, newIndex); } @@ -214,10 +219,6 @@ const boardsStore = { list.removeIssue(issue); }); }); - } else if (this.shouldRemoveIssue(listFrom, listTo)) { - issues.forEach(issue => { - listFrom.removeIssue(issue); - }); } }, @@ -282,11 +283,11 @@ const boardsStore = { list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId); }, - moveMultipleIssuesInList(list, issues, oldIndex, newIndex, idArray) { + moveMultipleIssuesInList(list, issues, oldIndicies, newIndex, idArray) { const beforeId = parseInt(idArray[newIndex - 1], 10) || null; const afterId = parseInt(idArray[newIndex + issues.length], 10) || null; - list.moveMultipleIssues(issues, oldIndex, newIndex, beforeId, afterId); + list.moveMultipleIssues(issues, oldIndicies, newIndex, beforeId, afterId); }, findList(key, val, type = 'label') { const filteredList = this.state.lists.filter(list => { diff --git a/package.json b/package.json index 93ae31ae81bc290d03b8ea4f9690b33155ddfe29..2646034a79c403f8f445aa8969fbc938a2af7d43 100644 --- a/package.json +++ b/package.json @@ -114,7 +114,7 @@ "select2": "3.5.2-browserify", "sha1": "^1.1.1", "smooshpack": "^0.0.54", - "sortablejs": "1.10.0-rc3", + "sortablejs": "1.10.0", "sql.js": "^0.4.0", "stickyfilljs": "^2.0.5", "style-loader": "^0.23.1", diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js index 4cbf0b533f6fe0b97b65b2ffb51aeec2fc30ad0b..de7973e2215b5558a455444adb38d3afaa392cb0 100644 --- a/spec/javascripts/boards/boards_store_spec.js +++ b/spec/javascripts/boards/boards_store_spec.js @@ -387,32 +387,34 @@ describe('Store', () => { }); describe('toggleMultiSelect', () => { + let basicIssueObj; + + beforeAll(() => { + basicIssueObj = { id: 987654 }; + }); + afterEach(() => { boardsStore.clearMultiSelect(); }); it('adds issue when not present', () => { - const issue = { id: 987654 }; - - boardsStore.toggleMultiSelect(issue); + boardsStore.toggleMultiSelect(basicIssueObj); const selectedIds = boardsStore.multiSelect.list.map(x => x.id); - expect(selectedIds.includes(issue.id)).toEqual(true); + expect(selectedIds.includes(basicIssueObj.id)).toEqual(true); }); it('removes issue when issue is present', () => { - const issue = { id: 987654 }; - - boardsStore.toggleMultiSelect(issue); + boardsStore.toggleMultiSelect(basicIssueObj); let selectedIds = boardsStore.multiSelect.list.map(x => x.id); - expect(selectedIds.includes(issue.id)).toEqual(true); + expect(selectedIds.includes(basicIssueObj.id)).toEqual(true); - boardsStore.toggleMultiSelect(issue); + boardsStore.toggleMultiSelect(basicIssueObj); selectedIds = boardsStore.multiSelect.list.map(x => x.id); - expect(selectedIds.includes(issue.id)).toEqual(false); + expect(selectedIds.includes(basicIssueObj.id)).toEqual(false); }); }); @@ -455,22 +457,19 @@ describe('Store', () => { describe('moveMultipleIssuesInList', () => { it('moves multiple issues in list', done => { - const issue1 = new ListIssue({ + const issueObj = { title: 'Issue #1', id: 12345, iid: 2, confidential: false, labels: [], assignees: [], - }); - + }; + const issue1 = new ListIssue(issueObj); const issue2 = new ListIssue({ + ...issueObj, title: 'Issue #2', id: 12346, - iid: 2, - confidential: false, - labels: [], - assignees: [], }); const list = boardsStore.addList(listObj); @@ -482,9 +481,9 @@ describe('Store', () => { expect(list.issues.length).toBe(3); expect(list.issues[0].id).not.toBe(issue2.id); - boardsStore.moveMultipleIssuesInList(list, [issue1, issue2], 0, 1, [1, 12345, 12346]); + boardsStore.moveMultipleIssuesInList(list, [issue1, issue2], [0], 1, [1, 12345, 12346]); - expect(list.issues[0].id).toBe(issue2.id); + expect(list.issues[0].id).toBe(issue1.id); expect(gl.boardService.moveMultipleIssues).toHaveBeenCalledWith( [issue1.id, issue2.id], diff --git a/yarn.lock b/yarn.lock index d9ada77fe384591d10e80a2729aaafd11e1a3ce0..b070dc7b411e9765809800fd3b370904abf382f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11105,10 +11105,10 @@ sort-keys@^2.0.0: dependencies: is-plain-obj "^1.0.0" -sortablejs@1.10.0-rc3: - version "1.10.0-rc3" - resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.10.0-rc3.tgz#2fe63463a38b5cd12ec914fc3e03583048496f42" - integrity sha512-uw8vZqwI3nkIeAqdrP6N/GDxFW3dY7yz3/rK0GLLoe8aJ2RZALmo6mAwOi+uA7RYuqfz2lm7AACr4ms6gXcb6w== +sortablejs@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.10.0.tgz#0ebc054acff2486569194a2f975b2b145dd5e7d6" + integrity sha512-+e0YakK1BxgEZpf9l9UiFaiQ8ZOBn1p/4qkkXr8QDVmYyCrUDTyDRRGm0AgW4E4cD0wtgxJ6yzIRkSPUwqhuhg== source-list-map@^2.0.0: version "2.0.0"