diff options
Diffstat (limited to 'app/assets/javascripts/boards')
17 files changed, 219 insertions, 362 deletions
diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 8b53e2aa344..52c227e6b15 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -2,6 +2,9 @@ /* global Vue */ /* global BoardService */ +import FilteredSearchBoards from './filtered_search_boards'; +import eventHub from './eventhub'; + window.Vue = require('vue'); window.Vue.use(require('vue-resource')); require('./models/issue'); @@ -60,6 +63,14 @@ $(() => { }, created () { gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId); + + this.filterManager = new FilteredSearchBoards(Store.filter, true); + + // Listen for updateTokens event + eventHub.$on('updateTokens', this.updateTokens); + }, + beforeDestroy() { + eventHub.$off('updateTokens', this.updateTokens); }, mounted () { Store.disabled = this.disabled; @@ -78,11 +89,16 @@ $(() => { Store.addBlankState(); this.loading = false; }); - } + }, + methods: { + updateTokens() { + this.filterManager.updateTokens(); + } + }, }); gl.IssueBoardsSearch = new Vue({ - el: document.getElementById('js-boards-search'), + el: document.getElementById('js-add-list'), data: { filters: Store.state.filters }, diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index 18324de18b3..67c0c419713 100644 --- a/app/assets/javascripts/boards/components/board.js +++ b/app/assets/javascripts/boards/components/board.js @@ -2,7 +2,8 @@ /* global Vue */ /* global Sortable */ -require('./board_blank_state'); +import boardBlankState from './board_blank_state'; + require('./board_delete'); require('./board_list'); @@ -17,7 +18,7 @@ require('./board_list'); components: { 'board-list': gl.issueBoards.BoardList, 'board-delete': gl.issueBoards.BoardDelete, - 'board-blank-state': gl.issueBoards.BoardBlankState + boardBlankState, }, props: { list: Object, @@ -28,16 +29,16 @@ require('./board_list'); data () { return { detailIssue: Store.detail, - filters: Store.state.filters, + filter: Store.filter, }; }, watch: { - filters: { - handler () { + filter: { + handler() { this.list.page = 1; this.list.getIssues(true); }, - deep: true + deep: true, }, detailIssue: { handler () { diff --git a/app/assets/javascripts/boards/components/board_blank_state.js b/app/assets/javascripts/boards/components/board_blank_state.js index d76314c1892..52893d4642b 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.js +++ b/app/assets/javascripts/boards/components/board_blank_state.js @@ -1,53 +1,84 @@ -/* eslint-disable space-before-function-paren, comma-dangle */ -/* global Vue */ /* global ListLabel */ +/* global Cookies */ +const Store = gl.issueBoards.BoardsStore; -(() => { - 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: [ + new ListLabel({ title: 'To Do', color: '#F0AD4E' }), + new ListLabel({ title: 'Doing', color: '#5CB85C' }), + ], + }; + }, + methods: { + addDefaultLists() { + this.clearBlankState(); - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - - gl.issueBoards.BoardBlankState = Vue.extend({ - data () { - return { - predefinedLabels: [ - new ListLabel({ title: 'To Do', color: '#F0AD4E' }), - new ListLabel({ title: 'Doing', color: '#5CB85C' }) - ] - }; - }, - methods: { - addDefaultLists () { - this.clearBlankState(); - - this.predefinedLabels.forEach((label, i) => { - Store.addList({ + this.predefinedLabels.forEach((label, i) => { + Store.addList({ + title: label.title, + position: i, + list_type: 'label', + label: { title: label.title, - position: i, - list_type: 'label', - label: { - title: label.title, - color: label.color - } - }); + color: label.color, + }, }); + }); - Store.state.lists = _.sortBy(Store.state.lists, 'position'); + Store.state.lists = _.sortBy(Store.state.lists, 'position'); - // Save the labels - gl.boardService.generateDefaultLists() - .then((resp) => { - resp.json().forEach((listObj) => { - const list = Store.findList('title', listObj.title); + // Save the labels + gl.boardService.generateDefaultLists() + .then((resp) => { + resp.json().forEach((listObj) => { + const list = Store.findList('title', listObj.title); - list.id = listObj.id; - list.label.id = listObj.label.id; - list.getIssues(); - }); + list.id = listObj.id; + list.label.id = listObj.label.id; + list.getIssues(); }); - }, - clearBlankState: Store.removeBlankState.bind(Store) - } - }); -})(); + }) + .catch(() => { + Store.removeList(undefined, 'label'); + Cookies.remove('issue_board_welcome_hidden', { + path: '', + }); + Store.addBlankState(); + }); + }, + clearBlankState: Store.removeBlankState.bind(Store), + }, +}; diff --git a/app/assets/javascripts/boards/components/board_card.js b/app/assets/javascripts/boards/components/board_card.js index 795b3cf2ec0..4b72090df31 100644 --- a/app/assets/javascripts/boards/components/board_card.js +++ b/app/assets/javascripts/boards/components/board_card.js @@ -17,7 +17,8 @@ export default { :list="list" :issue="issue" :issue-link-base="issueLinkBase" - :root-path="rootPath" /> + :root-path="rootPath" + :update-filters="true" /> </li> `, components: { diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js index 22a8b971ff8..69e30cec4c5 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js +++ b/app/assets/javascripts/boards/components/issue_card_inner.js @@ -1,4 +1,6 @@ /* global Vue */ +import eventHub from '../eventhub'; + (() => { const Store = gl.issueBoards.BoardsStore; @@ -23,6 +25,11 @@ type: String, required: true, }, + updateFilters: { + type: Boolean, + required: false, + default: false, + }, }, methods: { showLabel(label) { @@ -31,29 +38,25 @@ 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); + if (!this.updateFilters) return; + + const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&'); + const labelTitle = encodeURIComponent(label.title); + const param = `label_name[]=${labelTitle}`; + const labelIndex = filterPath.indexOf(param); $(e.currentTarget).tooltip('hide'); if (labelIndex === -1) { - Store.state.filters.label_name.push(label.title); - $('.labels-filter').prepend(`<input type="hidden" name="label_name[]" value="${label.title}" />`); + filterPath.push(param); } 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(); + filterPath.splice(labelIndex, 1); } - 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); + gl.issueBoards.BoardsStore.filter.path = filterPath.join('&'); Store.updateFiltersUrl(); + + eventHub.$emit('updateTokens'); }, labelStyle(label) { return { diff --git a/app/assets/javascripts/boards/components/modal/filters.js b/app/assets/javascripts/boards/components/modal/filters.js index 6de06811d94..bd394a2318c 100644 --- a/app/assets/javascripts/boards/components/modal/filters.js +++ b/app/assets/javascripts/boards/components/modal/filters.js @@ -1,49 +1,24 @@ -/* global Vue */ -const userFilter = require('./filters/user'); -const milestoneFilter = require('./filters/milestone'); -const labelFilter = require('./filters/label'); +import FilteredSearchBoards from '../../filtered_search_boards'; +import FilteredSearchContainer from '../../../filtered_search/container'; -module.exports = Vue.extend({ +export default { name: 'modal-filters', props: { - projectId: { - type: Number, - required: true, - }, - milestonePath: { - type: String, - required: true, - }, - labelPath: { - type: String, + store: { + type: Object, required: true, }, }, - destroyed() { - gl.issueBoards.ModalStore.setDefaultFilter(); + mounted() { + FilteredSearchContainer.container = this.$el; + + this.filteredSearch = new FilteredSearchBoards(this.store); + this.filteredSearch.removeTokens(); }, - components: { - userFilter, - milestoneFilter, - labelFilter, + beforeDestroy() { + this.filteredSearch.cleanup(); + FilteredSearchContainer.container = document; + this.store.path = ''; }, - template: ` - <div class="modal-filters"> - <user-filter - dropdown-class-name="dropdown-menu-author" - toggle-class-name="js-user-search js-author-search" - toggle-label="Author" - field-name="author_id" - :project-id="projectId"></user-filter> - <user-filter - dropdown-class-name="dropdown-menu-author" - toggle-class-name="js-assignee-search" - toggle-label="Assignee" - field-name="assignee_id" - :null-user="true" - :project-id="projectId"></user-filter> - <milestone-filter :milestone-path="milestonePath"></milestone-filter> - <label-filter :label-path="labelPath"></label-filter> - </div> - `, -}); + template: '#js-board-modal-filter', +}; diff --git a/app/assets/javascripts/boards/components/modal/filters/label.js b/app/assets/javascripts/boards/components/modal/filters/label.js deleted file mode 100644 index 4fc8f72a145..00000000000 --- a/app/assets/javascripts/boards/components/modal/filters/label.js +++ /dev/null @@ -1,54 +0,0 @@ -/* eslint-disable no-new */ -/* global Vue */ -/* global LabelsSelect */ -module.exports = Vue.extend({ - name: 'filter-label', - props: { - labelPath: { - type: String, - required: true, - }, - }, - mounted() { - new LabelsSelect(this.$refs.dropdown); - }, - template: ` - <div class="dropdown"> - <button - class="dropdown-menu-toggle js-label-select js-multiselect js-extra-options" - type="button" - data-toggle="dropdown" - data-show-any="true" - data-show-no="true" - :data-labels="labelPath" - ref="dropdown"> - <span class="dropdown-toggle-text"> - Label - </span> - <i class="fa fa-chevron-down"></i> - </button> - <div class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable"> - <div class="dropdown-title"> - Filter by label - <button - class="dropdown-title-button dropdown-menu-close" - aria-label="Close" - type="button"> - <i class="fa fa-times dropdown-menu-close-icon"></i> - </button> - </div> - <div class="dropdown-input"> - <input - type="search" - class="dropdown-input-field" - placeholder="Search" - autocomplete="off" /> - <i class="fa fa-search dropdown-input-search"></i> - <i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i> - </div> - <div class="dropdown-content"></div> - <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div> - </div> - </div> - `, -}); diff --git a/app/assets/javascripts/boards/components/modal/filters/milestone.js b/app/assets/javascripts/boards/components/modal/filters/milestone.js deleted file mode 100644 index d555599d300..00000000000 --- a/app/assets/javascripts/boards/components/modal/filters/milestone.js +++ /dev/null @@ -1,55 +0,0 @@ -/* eslint-disable no-new */ -/* global Vue */ -/* global MilestoneSelect */ -module.exports = Vue.extend({ - name: 'filter-milestone', - props: { - milestonePath: { - type: String, - required: true, - }, - }, - mounted() { - new MilestoneSelect(null, this.$refs.dropdown); - }, - template: ` - <div class="dropdown"> - <button - class="dropdown-menu-toggle js-milestone-select" - type="button" - data-toggle="dropdown" - data-show-any="true" - data-show-upcoming="true" - data-field-name="milestone_title" - :data-milestones="milestonePath" - ref="dropdown"> - <span class="dropdown-toggle-text"> - Milestone - </span> - <i class="fa fa-chevron-down"></i> - </button> - <div class="dropdown-menu dropdown-select dropdown-menu-selectable dropdown-menu-milestone"> - <div class="dropdown-title"> - <span>Filter by milestone</span> - <button - class="dropdown-title-button dropdown-menu-close" - aria-label="Close" - type="button"> - <i class="fa fa-times dropdown-menu-close-icon"></i> - </button> - </div> - <div class="dropdown-input"> - <input - type="search" - class="dropdown-input-field" - placeholder="Search milestones" - autocomplete="off" /> - <i class="fa fa-search dropdown-input-search"></i> - <i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i> - </div> - <div class="dropdown-content"></div> - <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div> - </div> - </div> - `, -}); diff --git a/app/assets/javascripts/boards/components/modal/filters/user.js b/app/assets/javascripts/boards/components/modal/filters/user.js deleted file mode 100644 index 8523028c29c..00000000000 --- a/app/assets/javascripts/boards/components/modal/filters/user.js +++ /dev/null @@ -1,96 +0,0 @@ -/* eslint-disable no-new */ -/* global Vue */ -/* global UsersSelect */ -module.exports = Vue.extend({ - name: 'filter-user', - props: { - toggleClassName: { - type: String, - required: true, - }, - dropdownClassName: { - type: String, - required: false, - default: '', - }, - toggleLabel: { - type: String, - required: true, - }, - fieldName: { - type: String, - required: true, - }, - nullUser: { - type: Boolean, - required: false, - default: false, - }, - projectId: { - type: Number, - required: true, - }, - }, - mounted() { - new UsersSelect(null, this.$refs.dropdown); - }, - computed: { - currentUsername() { - return gon.current_username; - }, - dropdownTitle() { - return `Filter by ${this.toggleLabel.toLowerCase()}`; - }, - inputPlaceholder() { - return `Search ${this.toggleLabel.toLowerCase()}`; - }, - }, - template: ` - <div class="dropdown"> - <button - class="dropdown-menu-toggle js-user-search" - :class="toggleClassName" - type="button" - data-toggle="dropdown" - data-current-user="true" - :data-any-user="'Any ' + toggleLabel" - :data-null-user="nullUser" - :data-field-name="fieldName" - :data-project-id="projectId" - :data-first-user="currentUsername" - ref="dropdown"> - <span class="dropdown-toggle-text"> - {{ toggleLabel }} - </span> - <i class="fa fa-chevron-down"></i> - </button> - <div - class="dropdown-menu dropdown-select dropdown-menu-user dropdown-menu-selectable" - :class="dropdownClassName"> - <div class="dropdown-title"> - {{ dropdownTitle }} - <button - class="dropdown-title-button dropdown-menu-close" - aria-label="Close" - type="button"> - <i class="fa fa-times dropdown-menu-close-icon"></i> - </button> - </div> - <div class="dropdown-input"> - <input - type="search" - class="dropdown-input-field" - autocomplete="off" - :placeholder="inputPlaceholder" /> - <i class="fa fa-search dropdown-input-search"></i> - <i - role="button" - class="fa fa-times dropdown-input-clear js-dropdown-input-clear"> - </i> - </div> - <div class="dropdown-content"></div> - <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div> - </div> - </div> - `, -}); diff --git a/app/assets/javascripts/boards/components/modal/header.js b/app/assets/javascripts/boards/components/modal/header.js index 70c088f9054..116e29cd177 100644 --- a/app/assets/javascripts/boards/components/modal/header.js +++ b/app/assets/javascripts/boards/components/modal/header.js @@ -1,6 +1,7 @@ -/* global Vue */ +import Vue from 'vue'; +import modalFilters from './filters'; + require('./tabs'); -const modalFilters = require('./filters'); (() => { const ModalStore = gl.issueBoards.ModalStore; @@ -66,16 +67,7 @@ const modalFilters = require('./filters'); <div class="add-issues-search append-bottom-10" v-if="showSearch"> - <modal-filters - :project-id="projectId" - :milestone-path="milestonePath" - :label-path="labelPath"> - </modal-filters> - <input - placeholder="Search issues..." - class="form-control" - type="search" - v-model="searchTerm" /> + <modal-filters :store="filter" /> <button type="button" class="btn btn-success btn-inverted prepend-left-10" diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js index f290cd13763..1b66c8b922d 100644 --- a/app/assets/javascripts/boards/components/modal/index.js +++ b/app/assets/javascripts/boards/components/modal/index.js @@ -1,5 +1,6 @@ /* global Vue */ /* global ListIssue */ +import queryData from '../../utils/query_data'; require('./header'); require('./list'); @@ -47,9 +48,6 @@ require('./empty_state'); page() { this.loadIssues(); }, - searchTerm() { - this.searchOperation(); - }, showAddIssuesModal() { if (this.showAddIssuesModal && !this.issues.length) { this.loading = true; @@ -72,19 +70,13 @@ require('./empty_state'); }, }, methods: { - searchOperation: _.debounce(function searchOperationDebounce() { - this.loadIssues(true); - }, 500), loadIssues(clearIssues = false) { if (!this.showAddIssuesModal) return false; - const queryData = Object.assign({}, this.filter, { - search: this.searchTerm, + return gl.boardService.getBacklog(queryData(this.filter.path, { page: this.page, per: this.perPage, - }); - - return gl.boardService.getBacklog(queryData).then((res) => { + })).then((res) => { const data = res.json(); if (clearIssues) { diff --git a/app/assets/javascripts/boards/eventhub.js b/app/assets/javascripts/boards/eventhub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/boards/eventhub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js new file mode 100644 index 00000000000..101732309ea --- /dev/null +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -0,0 +1,41 @@ +/* eslint-disable class-methods-use-this */ +import FilteredSearchContainer from '../filtered_search/container'; + +export default class FilteredSearchBoards extends gl.FilteredSearchManager { + constructor(store, updateUrl = false) { + super('boards'); + + this.store = store; + this.updateUrl = updateUrl; + + // Issue boards is slightly different, we handle all the requests async + // instead or reloading the page, we just re-fire the list ajax requests + this.isHandledAsync = true; + } + + updateObject(path) { + this.store.path = path.substr(1); + + if (this.updateUrl) { + gl.issueBoards.BoardsStore.updateFiltersUrl(); + } + } + + removeTokens() { + const tokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token'); + + // Remove all the tokens as they will be replaced by the search manager + [].forEach.call(tokens, (el) => { + el.parentNode.removeChild(el); + }); + } + + updateTokens() { + this.removeTokens(); + + this.loadSearchParamsFromURL(); + + // Get the placeholder back if search is empty + this.filteredSearchInput.dispatchEvent(new Event('input')); + } +} diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index f237567208c..f18ad2a0fac 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -1,6 +1,7 @@ /* 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 queryData from '../utils/query_data'; class List { constructor (obj) { @@ -10,7 +11,6 @@ class List { this.title = obj.title; this.type = obj.list_type; this.preset = ['done', 'blank'].indexOf(this.type) > -1; - this.filters = gl.issueBoards.BoardsStore.state.filters; this.page = 1; this.loading = true; this.loadingMore = false; @@ -65,12 +65,9 @@ class List { } getIssues (emptyIssues = true) { - const filters = this.filters; - const data = { page: this.page }; + const data = queryData(gl.issueBoards.BoardsStore.filter.path, { page: this.page }); - Object.keys(filters).forEach((key) => { data[key] = filters[key]; }); - - if (this.label) { + if (this.label && data.label_name) { data.label_name = data.label_name.filter(label => label !== this.label.title); } diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 3866c6bbfc6..28ecb322df7 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -8,6 +8,9 @@ gl.issueBoards.BoardsStore = { disabled: false, + filter: { + path: '', + }, state: {}, detail: { issue: {} @@ -18,13 +21,7 @@ }, create () { this.state.lists = []; - this.state.filters = { - author_id: gl.utils.getParameterValues('author_id')[0], - assignee_id: gl.utils.getParameterValues('assignee_id')[0], - milestone_title: gl.utils.getParameterValues('milestone_title')[0], - label_name: gl.utils.getParameterValues('label_name[]'), - search: '' - }; + this.filter.path = gl.utils.getUrlParamsArray().join('&'); }, addList (listObj) { const list = new List(listObj); @@ -123,7 +120,7 @@ })[0]; }, updateFiltersUrl () { - history.pushState(null, null, `?${$.param(this.state.filters)}`); + history.pushState(null, null, `?${this.filter.path}`); } }; })(); diff --git a/app/assets/javascripts/boards/stores/modal_store.js b/app/assets/javascripts/boards/stores/modal_store.js index 15fc6c79e8d..7ee266a831f 100644 --- a/app/assets/javascripts/boards/stores/modal_store.js +++ b/app/assets/javascripts/boards/stores/modal_store.js @@ -17,17 +17,9 @@ loadingNewPage: false, page: 1, perPage: 50, - }; - - this.setDefaultFilter(); - } - - setDefaultFilter() { - this.store.filter = { - author_id: '', - assignee_id: '', - milestone_title: '', - label_name: [], + filter: { + path: '', + }, }; } diff --git a/app/assets/javascripts/boards/utils/query_data.js b/app/assets/javascripts/boards/utils/query_data.js new file mode 100644 index 00000000000..2cd3c146f11 --- /dev/null +++ b/app/assets/javascripts/boards/utils/query_data.js @@ -0,0 +1,21 @@ +export default (path, extraData) => path.split('&').reduce((dataParam, filterParam) => { + if (filterParam === '') return dataParam; + + const data = dataParam; + const paramSplit = filterParam.split('='); + const paramKeyNormalized = paramSplit[0].replace('[]', ''); + const isArray = paramSplit[0].indexOf('[]'); + const value = decodeURIComponent(paramSplit[1]).replace(/\+/g, ' '); + + if (isArray !== -1) { + if (!data[paramKeyNormalized]) { + data[paramKeyNormalized] = []; + } + + data[paramKeyNormalized].push(value); + } else { + data[paramKeyNormalized] = value; + } + + return data; +}, extraData); |