diff options
Diffstat (limited to 'app/assets/javascripts/boards/components')
22 files changed, 596 insertions, 576 deletions
diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index 3cffd91716a..a2355d7fd5c 100644 --- a/app/assets/javascripts/boards/components/board.js +++ b/app/assets/javascripts/boards/components/board.js @@ -1,11 +1,10 @@ -/* eslint-disable comma-dangle, space-before-function-paren, one-var */ +/* eslint-disable comma-dangle */ -import $ from 'jquery'; -import Sortable from 'vendor/Sortable'; +import Sortable from 'sortablejs'; import Vue from 'vue'; import AccessorUtilities from '../../lib/utils/accessor'; import boardList from './board_list.vue'; -import boardBlankState from './board_blank_state'; +import BoardBlankState from './board_blank_state.vue'; import './board_delete'; const Store = gl.issueBoards.BoardsStore; @@ -14,17 +13,28 @@ window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; gl.issueBoards.Board = Vue.extend({ - template: '#js-board-template', components: { boardList, 'board-delete': gl.issueBoards.BoardDelete, - boardBlankState, + BoardBlankState, }, props: { - list: Object, - disabled: Boolean, - issueLinkBase: String, - rootPath: String, + list: { + type: Object, + default: () => ({}), + }, + disabled: { + type: Boolean, + required: true, + }, + issueLinkBase: { + type: String, + required: true, + }, + rootPath: { + type: String, + required: true, + }, boardId: { type: String, required: true, @@ -46,56 +56,8 @@ gl.issueBoards.Board = Vue.extend({ }); }, deep: true, - }, - detailIssue: { - handler () { - if (!Object.keys(this.detailIssue.issue).length) return; - - const issue = this.list.findIssue(this.detailIssue.issue.id); - - if (issue) { - const offsetLeft = this.$el.offsetLeft; - const boardsList = document.querySelectorAll('.boards-list')[0]; - const left = boardsList.scrollLeft - offsetLeft; - let right = (offsetLeft + this.$el.offsetWidth); - - if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) { - // -290 here because width of boardsList is animating so therefore - // getting the width here is incorrect - // 290 is the width of the sidebar - right -= (boardsList.offsetWidth - 290); - } else { - 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; - }, - toggleExpanded(e) { - if (this.list.isExpandable && !e.target.classList.contains('js-no-trigger-collapse')) { - this.list.isExpanded = !this.list.isExpanded; - - if (AccessorUtilities.isLocalStorageAccessSafe()) { - localStorage.setItem(`boards.${this.boardId}.${this.list.type}.expanded`, this.list.isExpanded); - } - } - }, - }, mounted () { this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({ disabled: this.disabled, @@ -125,4 +87,19 @@ gl.issueBoards.Board = Vue.extend({ this.list.isExpanded = !isCollapsed; } }, + methods: { + showNewIssueForm() { + this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm; + }, + toggleExpanded(e) { + if (this.list.isExpandable && !e.target.classList.contains('js-no-trigger-collapse')) { + this.list.isExpanded = !this.list.isExpanded; + + if (AccessorUtilities.isLocalStorageAccessSafe()) { + localStorage.setItem(`boards.${this.boardId}.${this.list.type}.expanded`, this.list.isExpanded); + } + } + }, + }, + template: '#js-board-template', }); diff --git a/app/assets/javascripts/boards/components/board_blank_state.js b/app/assets/javascripts/boards/components/board_blank_state.vue index 72db626d3c7..286529b4d13 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.js +++ b/app/assets/javascripts/boards/components/board_blank_state.vue @@ -1,42 +1,11 @@ +<script> /* global ListLabel */ - import _ from 'underscore'; import Cookies from 'js-cookie'; const Store = gl.issueBoards.BoardsStore; export default { - template: ` - <div class="board-blank-state"> - <p> - Add the following default lists to your Issue Board with one click: - </p> - <ul class="board-blank-state-list"> - <li v-for="label in predefinedLabels"> - <span - class="label-color" - :style="{ backgroundColor: label.color }"> - </span> - {{ label.title }} - </li> - </ul> - <p> - Starting out with the default set of lists will get you right on the way to making the most of your board. - </p> - <button - class="btn btn-create btn-inverted btn-block" - type="button" - @click.stop="addDefaultLists"> - Add default lists - </button> - <button - class="btn btn-default btn-block" - type="button" - @click.stop="clearBlankState"> - Nevermind, I'll use my own - </button> - </div> - `, data() { return { predefinedLabels: [ @@ -89,3 +58,41 @@ export default { clearBlankState: Store.removeBlankState.bind(Store), }, }; + +</script> + +<template> + <div class="board-blank-state"> + <p> + Add the following default lists to your Issue Board with one click: + </p> + <ul class="board-blank-state-list"> + <li + v-for="(label, index) in predefinedLabels" + :key="index" + > + <span + :style="{ backgroundColor: label.color }" + class="label-color"> + </span> + {{ label.title }} + </li> + </ul> + <p> + Starting out with the default set of lists will get you + right on the way to making the most of your board. + </p> + <button + class="btn btn-create btn-inverted btn-block" + type="button" + @click.stop="addDefaultLists"> + Add default lists + </button> + <button + class="btn btn-default btn-block" + type="button" + @click.stop="clearBlankState"> + Nevermind, I'll use my own + </button> + </div> +</template> diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index 84885ca9306..b7d3574bc80 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -77,7 +77,6 @@ export default { <template> <li - class="card" :class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, @@ -85,6 +84,7 @@ export default { }" :index="index" :data-issue-id="issue.id" + class="board-card" @mousedown="mouseDown" @mousemove="mouseMove" @mouseup="showIssue($event)"> diff --git a/app/assets/javascripts/boards/components/board_delete.js b/app/assets/javascripts/boards/components/board_delete.js index 7be98825fda..c5945e8098d 100644 --- a/app/assets/javascripts/boards/components/board_delete.js +++ b/app/assets/javascripts/boards/components/board_delete.js @@ -1,4 +1,4 @@ -/* eslint-disable comma-dangle, space-before-function-paren, no-alert */ +/* eslint-disable comma-dangle, no-alert */ import $ from 'jquery'; import Vue from 'vue'; @@ -8,13 +8,16 @@ window.gl.issueBoards = window.gl.issueBoards || {}; gl.issueBoards.BoardDelete = Vue.extend({ props: { - list: Object + list: { + type: Object, + default: () => ({}), + }, }, methods: { deleteBoard () { $(this.$el).tooltip('hide'); - if (confirm('Are you sure you want to delete this list?')) { + if (window.confirm('Are you sure you want to delete this list?')) { this.list.destroy(); } } diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index 0d03c1c419c..5c7565234d8 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -1,5 +1,5 @@ <script> -import Sortable from 'vendor/Sortable'; +import Sortable from 'sortablejs'; import boardNewIssue from './board_new_issue.vue'; import boardCard from './board_card.vue'; import eventHub from '../eventhub'; @@ -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]; @@ -169,21 +205,22 @@ export default { <template> <div class="board-list-component"> <div + v-if="loading" class="board-list-loading text-center" - aria-label="Loading issues" - v-if="loading"> + aria-label="Loading issues"> <loading-icon /> </div> <board-new-issue + v-if="list.type !== 'closed' && showIssueForm" :group-id="groupId" - :list="list" - v-if="list.type !== 'closed' && showIssueForm"/> + :list="list"/> <ul - class="board-list" v-show="!loading" ref="list" :data-board="list.id" - :class="{ 'is-smaller': showIssueForm }"> + :data-board-type="list.type" + :class="{ 'is-smaller': showIssueForm }" + class="board-list js-board-list"> <board-card v-for="(issue, index) in issues" ref="issue" @@ -196,8 +233,8 @@ export default { :disabled="disabled" :key="issue.id" /> <li - class="board-list-count text-center" v-if="showCount" + class="board-list-count text-center" data-issue-id="-1"> <loading-icon v-show="list.loadingMore" diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue index 8d84c1735b8..ec23b1e7c11 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, }); @@ -92,29 +93,29 @@ export default { <template> <div class="board-new-issue-form"> - <div class="card"> + <div class="board-card"> <form @submit="submit($event)"> <div - class="flash-container" v-if="error" + class="flash-container" > <div class="flash-alert"> An error occurred. Please try again. </div> </div> <label - class="label-light" :for="list.id + '-title'" + class="label-light" > Title </label> <input + ref="input" + v-model="title" + :id="list.id + '-title'" class="form-control" type="text" - v-model="title" - ref="input" autocomplete="off" - :id="list.id + '-title'" /> <project-select v-if="groupId" @@ -122,15 +123,15 @@ export default { /> <div class="clearfix prepend-top-10"> <button - class="btn btn-success pull-left" - type="submit" - :disabled="disabled" ref="submit-button" + :disabled="disabled" + class="btn btn-success float-left" + type="submit" > Submit issue </button> <button - class="btn btn-default pull-right" + class="btn btn-default float-right" type="button" @click="cancel" > @@ -141,4 +142,3 @@ export default { </div> </div> </template> - diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index a44969272a1..371be109229 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -1,4 +1,4 @@ -/* eslint-disable comma-dangle, space-before-function-paren, no-new */ +/* eslint-disable comma-dangle, no-new */ import $ from 'jquery'; import Vue from 'vue'; @@ -6,13 +6,13 @@ import Flash from '../../flash'; import { __ } from '../../locale'; import Sidebar from '../../right_sidebar'; import eventHub from '../../sidebar/event_hub'; -import assigneeTitle from '../../sidebar/components/assignees/assignee_title.vue'; -import assignees from '../../sidebar/components/assignees/assignees.vue'; +import AssigneeTitle from '../../sidebar/components/assignees/assignee_title.vue'; +import Assignees from '../../sidebar/components/assignees/assignees.vue'; import DueDateSelectors from '../../due_date_select'; -import './sidebar/remove_issue'; +import RemoveBtn from './sidebar/remove_issue.vue'; import IssuableContext from '../../issuable_context'; import LabelsSelect from '../../labels_select'; -import subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue'; +import Subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue'; import MilestoneSelect from '../../milestone_select'; const Store = gl.issueBoards.BoardsStore; @@ -21,8 +21,17 @@ window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; gl.issueBoards.BoardSidebar = Vue.extend({ + components: { + AssigneeTitle, + Assignees, + RemoveBtn, + Subscriptions, + }, props: { - currentUser: Object + currentUser: { + type: Object, + default: () => ({}), + }, }, data() { return { @@ -60,14 +69,30 @@ gl.issueBoards.BoardSidebar = Vue.extend({ this.issue = this.detail.issue; this.list = this.detail.list; - - this.$nextTick(() => { - this.endpoint = this.$refs.assigneeDropdown.dataset.issueUpdate; - }); }, deep: true }, }, + created () { + // Get events from glDropdown + eventHub.$on('sidebar.removeAssignee', this.removeAssignee); + eventHub.$on('sidebar.addAssignee', this.addAssignee); + eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees); + eventHub.$on('sidebar.saveAssignees', this.saveAssignees); + }, + beforeDestroy() { + eventHub.$off('sidebar.removeAssignee', this.removeAssignee); + eventHub.$off('sidebar.addAssignee', this.addAssignee); + eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees); + eventHub.$off('sidebar.saveAssignees', this.saveAssignees); + }, + mounted () { + new IssuableContext(this.currentUser); + new MilestoneSelect(); + new DueDateSelectors(); + new LabelsSelect(); + new Sidebar(); + }, methods: { closeSidebar () { this.detail.issue = {}; @@ -91,7 +116,7 @@ gl.issueBoards.BoardSidebar = Vue.extend({ saveAssignees () { this.loadingAssignees = true; - gl.issueBoards.BoardsStore.detail.issue.update(this.endpoint) + gl.issueBoards.BoardsStore.detail.issue.update() .then(() => { this.loadingAssignees = false; }) @@ -101,30 +126,4 @@ gl.issueBoards.BoardSidebar = Vue.extend({ }); }, }, - created () { - // Get events from glDropdown - eventHub.$on('sidebar.removeAssignee', this.removeAssignee); - eventHub.$on('sidebar.addAssignee', this.addAssignee); - eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees); - eventHub.$on('sidebar.saveAssignees', this.saveAssignees); - }, - beforeDestroy() { - eventHub.$off('sidebar.removeAssignee', this.removeAssignee); - eventHub.$off('sidebar.addAssignee', this.addAssignee); - eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees); - eventHub.$off('sidebar.saveAssignees', this.saveAssignees); - }, - mounted () { - new IssuableContext(this.currentUser); - new MilestoneSelect(); - new DueDateSelectors(); - new LabelsSelect(); - new Sidebar(); - }, - components: { - assigneeTitle, - assignees, - removeBtn: gl.issueBoards.RemoveIssueBtn, - subscriptions, - }, }); diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js index 8aee5b23c76..f7d7b910e2f 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js +++ b/app/assets/javascripts/boards/components/issue_card_inner.js @@ -9,6 +9,9 @@ window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; gl.issueBoards.IssueCardInner = Vue.extend({ + components: { + UserAvatarLink, + }, props: { issue: { type: Object, @@ -35,6 +38,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({ groupId: { type: Number, required: false, + default: null, }, }, data() { @@ -44,9 +48,6 @@ gl.issueBoards.IssueCardInner = Vue.extend({ maxCounter: 99, }; }, - components: { - UserAvatarLink, - }, computed: { numberOverLimit() { return this.issue.assignees.length - this.limitBeforeCounter; @@ -68,15 +69,6 @@ gl.issueBoards.IssueCardInner = Vue.extend({ return this.issue.assignees.length > this.numberOverLimit; }, - cardUrl() { - let baseUrl = this.issueLinkBase; - - if (this.groupId && this.issue.project) { - baseUrl = this.issueLinkBase.replace(':project_path', this.issue.project.path); - } - - return `${baseUrl}/${this.issue.iid}`; - }, issueId() { if (this.issue.iid) { return `#${this.issue.iid}`; @@ -144,8 +136,8 @@ gl.issueBoards.IssueCardInner = Vue.extend({ }, template: ` <div> - <div class="card-header"> - <h4 class="card-title"> + <div class="board-card-header"> + <h4 class="board-card-title"> <i class="fa fa-eye-slash confidential-icon" v-if="issue.confidential" @@ -153,16 +145,16 @@ gl.issueBoards.IssueCardInner = Vue.extend({ /> <a class="js-no-trigger" - :href="cardUrl" + :href="issue.path" :title="issue.title">{{ issue.title }}</a> <span - class="card-number" + class="board-card-number" v-if="issueId" > - <template v-if="groupId && issue.project">{{issue.project.path}}</template>{{ issueId }} + {{ issue.referencePath }} </span> </h4> - <div class="card-assignee"> + <div class="board-card-assignee"> <user-avatar-link v-for="(assignee, index) in issue.assignees" :key="assignee.id" @@ -184,11 +176,11 @@ gl.issueBoards.IssueCardInner = Vue.extend({ </div> </div> <div - class="card-footer" + class="board-card-footer" v-if="showLabelFooter" > <button - class="label color-label has-tooltip" + class="badge color-label has-tooltip" v-for="label in issue.labels" type="button" v-if="showLabel(label)" diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js b/app/assets/javascripts/boards/components/modal/empty_state.js deleted file mode 100644 index e571b11a83d..00000000000 --- a/app/assets/javascripts/boards/components/modal/empty_state.js +++ /dev/null @@ -1,69 +0,0 @@ -import Vue from 'vue'; - -const ModalStore = gl.issueBoards.ModalStore; - -gl.issueBoards.ModalEmptyState = Vue.extend({ - mixins: [gl.issueBoards.ModalMixins], - data() { - return ModalStore.store; - }, - props: { - newIssuePath: { - type: String, - required: true, - }, - emptyStateSvg: { - 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>Open 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"><img :src="emptyStateSvg"/></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'"> - Open issues - </button> - </div> - </div> - </div> - </section> - `, -}); diff --git a/app/assets/javascripts/boards/components/modal/empty_state.vue b/app/assets/javascripts/boards/components/modal/empty_state.vue new file mode 100644 index 00000000000..dbd69f84526 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/empty_state.vue @@ -0,0 +1,73 @@ +<script> +import ModalStore from '../../stores/modal_store'; +import modalMixin from '../../mixins/modal_mixins'; + +export default { + mixins: [modalMixin], + props: { + newIssuePath: { + type: String, + required: true, + }, + emptyStateSvg: { + type: String, + required: true, + }, + }, + data() { + return ModalStore.store; + }, + 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>Open issues</strong> and select some issues + to add to your board. + `; + } + + return obj; + }, + }, +}; +</script> + +<template> + <section class="empty-state"> + <div class="row"> + <div class="col-12 col-md-6 order-md-last"> + <aside class="svg-content"><img :src="emptyStateSvg"/></aside> + </div> + <div class="col-12 col-md-6 order-md-first"> + <div class="text-content"> + <h4>{{ contents.title }}</h4> + <p v-html="contents.content"></p> + <a + v-if="activeTab === 'all'" + :href="newIssuePath" + class="btn btn-success btn-inverted" + > + New issue + </a> + <button + v-if="activeTab === 'selected'" + class="btn btn-default" + type="button" + @click="changeTab('all')" + > + Open issues + </button> + </div> + </div> + </div> + </section> +</template> diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.vue index 03cd7ef65cb..e0dac6003f1 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js +++ b/app/assets/javascripts/boards/components/modal/footer.vue @@ -1,13 +1,16 @@ -import Vue from 'vue'; +<script> import Flash from '../../../flash'; import { __ } from '../../../locale'; -import './lists_dropdown'; +import ListsDropdown from './lists_dropdown.vue'; import { pluralize } from '../../../lib/utils/text_utility'; +import ModalStore from '../../stores/modal_store'; +import modalMixin from '../../mixins/modal_mixins'; -const ModalStore = gl.issueBoards.ModalStore; - -gl.issueBoards.ModalFooter = Vue.extend({ - mixins: [gl.issueBoards.ModalMixins], +export default { + components: { + ListsDropdown, + }, + mixins: [modalMixin], data() { return { modal: ModalStore.store, @@ -52,31 +55,32 @@ gl.issueBoards.ModalFooter = Vue.extend({ 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> +}; +</script> +<template> + <footer + class="form-actions add-issues-footer" + > + <div class="float-left"> <button - class="btn btn-default pull-right" + :disabled="submitDisabled" + class="btn btn-success" type="button" - @click="toggleModal(false)"> - Cancel + @click="addIssues" + > + {{ submitText }} </button> - </footer> - `, -}); + <span class="inline add-issues-footer-to-list"> + to list + </span> + <lists-dropdown/> + </div> + <button + class="btn btn-default float-right" + type="button" + @click="toggleModal(false)" + > + Cancel + </button> + </footer> +</template> diff --git a/app/assets/javascripts/boards/components/modal/header.js b/app/assets/javascripts/boards/components/modal/header.js index 31f59d295bf..cc9848058ca 100644 --- a/app/assets/javascripts/boards/components/modal/header.js +++ b/app/assets/javascripts/boards/components/modal/header.js @@ -1,11 +1,15 @@ import Vue from 'vue'; import modalFilters from './filters'; -import './tabs'; - -const ModalStore = gl.issueBoards.ModalStore; +import modalTabs from './tabs.vue'; +import ModalStore from '../../stores/modal_store'; +import modalMixin from '../../mixins/modal_mixins'; gl.issueBoards.ModalHeader = Vue.extend({ - mixins: [gl.issueBoards.ModalMixins], + components: { + modalTabs, + modalFilters, + }, + mixins: [modalMixin], props: { projectId: { type: Number, @@ -42,10 +46,6 @@ gl.issueBoards.ModalHeader = Vue.extend({ ModalStore.toggleAll(); }, }, - components: { - 'modal-tabs': gl.issueBoards.ModalTabs, - modalFilters, - }, template: ` <div> <header class="add-issues-header form-actions"> diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js index d825ff38587..983061f52ae 100644 --- a/app/assets/javascripts/boards/components/modal/index.js +++ b/app/assets/javascripts/boards/components/modal/index.js @@ -5,12 +5,18 @@ import queryData from '~/boards/utils/query_data'; import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import './header'; import './list'; -import './footer'; -import './empty_state'; - -const ModalStore = gl.issueBoards.ModalStore; +import ModalFooter from './footer.vue'; +import EmptyState from './empty_state.vue'; +import ModalStore from '../../stores/modal_store'; gl.issueBoards.IssuesModal = Vue.extend({ + components: { + EmptyState, + 'modal-header': gl.issueBoards.ModalHeader, + 'modal-list': gl.issueBoards.ModalList, + ModalFooter, + loadingIcon, + }, props: { newIssuePath: { type: String, @@ -44,6 +50,22 @@ gl.issueBoards.IssuesModal = Vue.extend({ data() { return ModalStore.store; }, + 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; + }, + }, watch: { page() { this.loadIssues(); @@ -81,6 +103,9 @@ gl.issueBoards.IssuesModal = Vue.extend({ deep: true, }, }, + created() { + this.page = 1; + }, methods: { loadIssues(clearIssues = false) { if (!this.showAddIssuesModal) return false; @@ -113,32 +138,6 @@ gl.issueBoards.IssuesModal = Vue.extend({ }); }, }, - 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; - }, - }, - created() { - this.page = 1; - }, - components: { - 'modal-header': gl.issueBoards.ModalHeader, - 'modal-list': gl.issueBoards.ModalList, - 'modal-footer': gl.issueBoards.ModalFooter, - 'empty-state': gl.issueBoards.ModalEmptyState, - loadingIcon, - }, template: ` <div class="add-issues-modal" diff --git a/app/assets/javascripts/boards/components/modal/list.js b/app/assets/javascripts/boards/components/modal/list.js index 7c62134b3a3..11061c72a7b 100644 --- a/app/assets/javascripts/boards/components/modal/list.js +++ b/app/assets/javascripts/boards/components/modal/list.js @@ -1,11 +1,11 @@ -/* global ListIssue */ - import Vue from 'vue'; import bp from '../../../breakpoints'; - -const ModalStore = gl.issueBoards.ModalStore; +import ModalStore from '../../stores/modal_store'; gl.issueBoards.ModalList = Vue.extend({ + components: { + 'issue-card-inner': gl.issueBoards.IssueCardInner, + }, props: { issueLinkBase: { type: String, @@ -23,13 +23,6 @@ gl.issueBoards.ModalList = Vue.extend({ data() { return ModalStore.store; }, - watch: { - activeTab() { - if (this.activeTab === 'all') { - ModalStore.purgeUnselectedIssues(); - } - }, - }, computed: { loopIssues() { if (this.activeTab === 'all') { @@ -53,12 +46,34 @@ gl.issueBoards.ModalList = Vue.extend({ return groups; }, }, + watch: { + activeTab() { + if (this.activeTab === 'all') { + ModalStore.purgeUnselectedIssues(); + } + }, + }, + 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); + }, methods: { scrollHandler() { const currentPage = Math.floor(this.issues.length / this.perPage); - if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage - && currentPage === this.page) { + if ( + this.scrollTop() > this.scrollHeight() - 100 && + !this.loadingNewPage && + currentPage === this.page + ) { this.loadingNewPage = true; this.page += 1; } @@ -96,21 +111,6 @@ gl.issueBoards.ModalList = Vue.extend({ } }, }, - 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" @@ -134,9 +134,9 @@ gl.issueBoards.ModalList = Vue.extend({ <div v-for="issue in group" v-if="showIssue(issue)" - class="card-parent"> + class="board-card-parent"> <div - class="card" + class="board-card" :class="{ 'is-active': issue.selected }" @click="toggleIssue($event, issue)"> <issue-card-inner diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js b/app/assets/javascripts/boards/components/modal/lists_dropdown.js deleted file mode 100644 index 4684ea76647..00000000000 --- a/app/assets/javascripts/boards/components/modal/lists_dropdown.js +++ /dev/null @@ -1,55 +0,0 @@ -import Vue from '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[1]; - }, - }, - 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/lists_dropdown.vue b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue new file mode 100644 index 00000000000..6a5a39099bd --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue @@ -0,0 +1,56 @@ +<script> +import ModalStore from '../../stores/modal_store'; + +export default { + data() { + return { + modal: ModalStore.store, + state: gl.issueBoards.BoardsStore.state, + }; + }, + computed: { + selected() { + return this.modal.selectedList || this.state.lists[1]; + }, + }, + destroyed() { + this.modal.selectedList = null; + }, +}; +</script> +<template> + <div class="dropdown inline"> + <button + class="dropdown-menu-toggle" + type="button" + data-toggle="dropdown" + aria-expanded="false"> + <span + :style="{ backgroundColor: selected.label.color }" + class="dropdown-label-box"> + </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, i) in state.lists" + v-if="list.type == 'label'" + :key="i"> + <a + :class="{ 'is-active': list.id == selected.id }" + href="#" + role="button" + @click.prevent="modal.selectedList = list"> + <span + :style="{ backgroundColor: list.label.color }" + class="dropdown-label-box"> + </span> + {{ list.title }} + </a> + </li> + </ul> + </div> + </div> +</template> diff --git a/app/assets/javascripts/boards/components/modal/tabs.js b/app/assets/javascripts/boards/components/modal/tabs.js deleted file mode 100644 index 3e5d08e3d75..00000000000 --- a/app/assets/javascripts/boards/components/modal/tabs.js +++ /dev/null @@ -1,46 +0,0 @@ -import Vue from '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')"> - Open 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/modal/tabs.vue b/app/assets/javascripts/boards/components/modal/tabs.vue new file mode 100644 index 00000000000..d926b080094 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/tabs.vue @@ -0,0 +1,49 @@ +<script> + import ModalStore from '../../stores/modal_store'; + import modalMixin from '../../mixins/modal_mixins'; + + export default { + mixins: [modalMixin], + data() { + return ModalStore.store; + }, + computed: { + selectedCount() { + return ModalStore.selectedCount(); + }, + }, + destroyed() { + this.activeTab = 'all'; + }, + }; +</script> +<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')" + > + Open issues + <span class="badge badge-pill"> + {{ issuesCount }} + </span> + </a> + </li> + <li :class="{ 'active': activeTab == 'selected' }"> + <a + href="#" + role="button" + @click.prevent="changeTab('selected')" + > + Selected issues + <span class="badge badge-pill"> + {{ selectedCount }} + </span> + </a> + </li> + </ul> + </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..448ab9ed135 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, no-new, space-before-function-paren, one-var, promise/catch-or-return, max-len */ +/* eslint-disable func-names, no-new, promise/catch-or-return */ import $ from 'jquery'; import axios from '~/lib/utils/axios_utils'; @@ -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/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue index 371774098b9..eb335f352d3 100644 --- a/app/assets/javascripts/boards/components/project_select.vue +++ b/app/assets/javascripts/boards/components/project_select.vue @@ -1,71 +1,69 @@ <script> - /* global ListIssue */ +import $ from 'jquery'; +import _ from 'underscore'; +import eventHub from '../eventhub'; +import loadingIcon from '../../vue_shared/components/loading_icon.vue'; +import Api from '../../api'; - import $ from 'jquery'; - import _ from 'underscore'; - import eventHub from '../eventhub'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; - import Api from '../../api'; - - export default { - name: 'BoardProjectSelect', - components: { - loadingIcon, - }, - props: { - groupId: { - type: Number, - required: true, - default: 0, - }, +export default { + name: 'BoardProjectSelect', + components: { + loadingIcon, + }, + props: { + groupId: { + type: Number, + required: true, + default: 0, }, - data() { - return { - loading: true, - selectedProject: {}, - }; + }, + data() { + return { + loading: true, + selectedProject: {}, + }; + }, + computed: { + selectedProjectName() { + return this.selectedProject.name || 'Select a project'; }, - computed: { - selectedProjectName() { - return this.selectedProject.name || 'Select a project'; + }, + mounted() { + $(this.$refs.projectsDropdown).glDropdown({ + filterable: true, + filterRemote: true, + search: { + fields: ['name_with_namespace'], }, - }, - mounted() { - $(this.$refs.projectsDropdown).glDropdown({ - filterable: true, - filterRemote: true, - search: { - fields: ['name_with_namespace'], - }, - clicked: ({ $el, e }) => { - e.preventDefault(); - this.selectedProject = { - id: $el.data('project-id'), - name: $el.data('project-name'), - }; - eventHub.$emit('setSelectedProject', this.selectedProject); - }, - selectable: true, - data: (term, callback) => { - this.loading = true; - return Api.groupProjects(this.groupId, term, (projects) => { - this.loading = false; - callback(projects); - }); - }, - renderRow(project) { - return ` + clicked: ({ $el, e }) => { + e.preventDefault(); + this.selectedProject = { + id: $el.data('project-id'), + name: $el.data('project-name'), + }; + eventHub.$emit('setSelectedProject', this.selectedProject); + }, + selectable: true, + data: (term, callback) => { + this.loading = true; + return Api.groupProjects(this.groupId, term, projects => { + this.loading = false; + callback(projects); + }); + }, + renderRow(project) { + return ` <li> <a href='#' class='dropdown-menu-link' data-project-id="${project.id}" data-project-name="${project.name}"> ${_.escape(project.name)} </a> </li> `; - }, - text: project => project.name, - }); - }, - }; + }, + text: project => project.name, + }); + }, +}; </script> <template> diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js deleted file mode 100644 index 09c683ff621..00000000000 --- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js +++ /dev/null @@ -1,77 +0,0 @@ -import Vue from 'vue'; -import Flash from '../../../flash'; -import { __ } from '../../../locale'; - -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, - }, - issueUpdate: { - type: String, - required: true, - }, - }, - computed: { - updateUrl() { - return this.issueUpdate.replace(':project_path', this.issue.project.path); - }, - }, - methods: { - removeIssue() { - const issue = this.issue; - const lists = issue.getLists(); - const listLabelIds = lists.map(list => list.label.id); - - let labelIds = issue.labels - .map(label => label.id) - .filter(id => !listLabelIds.includes(id)); - if (labelIds.length === 0) { - labelIds = ['']; - } - - const data = { - issue: { - label_ids: labelIds, - }, - }; - - // Post the remove data - Vue.http.patch(this.updateUrl, data).catch(() => { - Flash(__('Failed to remove issue from board, please try again.')); - - 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"> - <button - class="btn btn-default btn-block" - type="button" - @click="removeIssue"> - Remove from board - </button> - </div> - `, -}); diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue new file mode 100644 index 00000000000..55278626ffc --- /dev/null +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue @@ -0,0 +1,72 @@ +<script> + import Vue from 'vue'; + import Flash from '../../../flash'; + import { __ } from '../../../locale'; + + const Store = gl.issueBoards.BoardsStore; + + export default { + props: { + issue: { + type: Object, + required: true, + }, + list: { + type: Object, + required: true, + }, + }, + computed: { + updateUrl() { + return this.issue.path; + }, + }, + methods: { + removeIssue() { + const { issue } = this; + const lists = issue.getLists(); + const listLabelIds = lists.map(list => list.label.id); + + let labelIds = issue.labels.map(label => label.id).filter(id => !listLabelIds.includes(id)); + if (labelIds.length === 0) { + labelIds = ['']; + } + + const data = { + issue: { + label_ids: labelIds, + }, + }; + + // Post the remove data + Vue.http.patch(this.updateUrl, data).catch(() => { + Flash(__('Failed to remove issue from board, please try again.')); + + lists.forEach(list => { + list.addIssue(issue); + }); + }); + + // Remove from the frontend store + lists.forEach(list => { + list.removeIssue(issue); + }); + + Store.detail.issue = {}; + }, + }, + }; +</script> +<template> + <div + class="block list" + > + <button + class="btn btn-default btn-block" + type="button" + @click="removeIssue" + > + Remove from board + </button> + </div> +</template> |