diff options
author | Filipa Lacerda <filipa@gitlab.com> | 2017-02-03 19:51:41 +0000 |
---|---|---|
committer | Filipa Lacerda <filipa@gitlab.com> | 2017-02-03 19:51:41 +0000 |
commit | fdbdd45d22c47c5b14b9ddc59d1a309bd2d7d9ce (patch) | |
tree | fcb5af6d573826d15fd1aad82304bddd2ba687d6 /app | |
parent | 183772c4488a82364425f5bc39e551988a2ac8ec (diff) | |
parent | d76b9291f75031c729760e3e00d9f7f1aa01644d (diff) | |
download | gitlab-ce-fdbdd45d22c47c5b14b9ddc59d1a309bd2d7d9ce.tar.gz |
Merge branch 'master' into fe-commit-mr-pipelines
* master: (65 commits)
Fixed eslint test failure
Fixed adding to list bug
Remove unnecessary queries for .atom and .json in Dashboard::ProjectsController#index
Fixed modal lists dropdown not updating when list is deleted
Fixed remove btn error after creating new issue in list
Removed duplicated test
Removed Masonry, instead uses groups of data
Uses mixins for repeated functions
Fixed up specs
Props use objects with required & type values
Removes labels instead of closing issue when clicking remove button
Fixed JS lint errors
Fixed issue card spec
Added webkit CSS properties
Fixed bug with empty state showing after search Fixed users href path being incorrect
Fixed bug where 2 un-selected issues would stay on selected tab
Fixed DB schema Changed how components are added in objects
Added remove button
Add optional id property to the issue schema
Fixed issue link href
...
Diffstat (limited to 'app')
80 files changed, 1231 insertions, 176 deletions
diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js index 993f427c9fb..424dc719c78 100644 --- a/app/assets/javascripts/admin.js +++ b/app/assets/javascripts/admin.js @@ -1,5 +1,4 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, max-len */ -/* global Turbolinks */ (function() { this.Admin = (function() { @@ -42,10 +41,10 @@ return $('.change-owner-link').show(); }); $('li.project_member').bind('ajax:success', function() { - return Turbolinks.visit(location.href); + return gl.utils.refreshCurrentPage(); }); $('li.group_member').bind('ajax:success', function() { - return Turbolinks.visit(location.href); + return gl.utils.refreshCurrentPage(); }); showBlacklistType = function() { if ($("input[name='blacklist_type']:checked").val() === 'file') { diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 4849aab50f4..ad95c1b9dfb 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -24,9 +24,7 @@ /*= require jquery.waitforimages */ /*= require jquery.atwho */ /*= require jquery.scrollTo */ -/*= require jquery.turbolinks */ /*= require js.cookie */ -/*= require turbolinks */ /*= require autosave */ /*= require bootstrap/affix */ /*= require bootstrap/alert */ @@ -64,7 +62,7 @@ /*= require es6-promise.auto */ (function () { - document.addEventListener('page:fetch', function () { + document.addEventListener('beforeunload', function () { // Unbind scroll events $(document).off('scroll'); // Close any open tooltips diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index 5b53cfe59cd..00f472edbde 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -14,10 +14,12 @@ //= require ./components/board_sidebar //= require ./components/new_list_dropdown //= require vue_shared/vue_resource_interceptor +//= require ./components/modal/index $(() => { const $boardApp = document.getElementById('board-app'); const Store = gl.issueBoards.BoardsStore; + const ModalStore = gl.issueBoards.ModalStore; window.gl = window.gl || {}; @@ -31,7 +33,8 @@ $(() => { el: $boardApp, components: { 'board': gl.issueBoards.Board, - 'board-sidebar': gl.issueBoards.BoardSidebar + 'board-sidebar': gl.issueBoards.BoardSidebar, + 'board-add-issues-modal': gl.issueBoards.IssuesModal, }, data: { state: Store.state, @@ -40,6 +43,8 @@ $(() => { boardId: $boardApp.dataset.boardId, disabled: $boardApp.dataset.disabled === 'true', issueLinkBase: $boardApp.dataset.issueLinkBase, + rootPath: $boardApp.dataset.rootPath, + bulkUpdatePath: $boardApp.dataset.bulkUpdatePath, detailIssue: Store.detail }, computed: { @@ -48,7 +53,7 @@ $(() => { }, }, created () { - gl.boardService = new BoardService(this.endpoint, this.boardId); + gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId); }, mounted () { Store.disabled = this.disabled; @@ -59,8 +64,6 @@ $(() => { if (list.type === 'done') { list.position = Infinity; - } else if (list.type === 'backlog') { - list.position = -1; } }); @@ -81,4 +84,27 @@ $(() => { gl.issueBoards.newListDropdownInit(); } }); + + gl.IssueBoardsModalAddBtn = new Vue({ + mixins: [gl.issueBoards.ModalMixins], + el: '#js-add-issues-btn', + data: { + modal: ModalStore.store, + store: Store.state, + }, + computed: { + disabled() { + return Store.shouldAddBlankState(); + }, + }, + template: ` + <button + class="btn btn-create pull-right prepend-left-10 has-tooltip" + type="button" + :disabled="disabled" + @click="toggleModal(true)"> + Add issues + </button> + `, + }); }); diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6 index a32881116d5..d6148ae748a 100644 --- a/app/assets/javascripts/boards/components/board.js.es6 +++ b/app/assets/javascripts/boards/components/board.js.es6 @@ -22,7 +22,8 @@ props: { list: Object, disabled: Boolean, - issueLinkBase: String + issueLinkBase: String, + rootPath: String, }, data () { return { diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6 index 5fc50280811..032b93da021 100644 --- a/app/assets/javascripts/boards/components/board_card.js.es6 +++ b/app/assets/javascripts/boards/components/board_card.js.es6 @@ -1,4 +1,5 @@ /* eslint-disable comma-dangle, space-before-function-paren, dot-notation */ +//= require ./issue_card_inner /* global Vue */ (() => { @@ -9,12 +10,16 @@ gl.issueBoards.BoardCard = Vue.extend({ template: '#js-board-list-card', + components: { + 'issue-card-inner': gl.issueBoards.IssueCardInner, + }, props: { list: Object, issue: Object, issueLinkBase: String, disabled: Boolean, - index: Number + index: Number, + rootPath: String, }, data () { return { @@ -28,31 +33,6 @@ } }, methods: { - filterByLabel (label, e) { - let labelToggleText = label.title; - const labelIndex = Store.state.filters['label_name'].indexOf(label.title); - $(e.target).tooltip('hide'); - - if (labelIndex === -1) { - Store.state.filters['label_name'].push(label.title); - $('.labels-filter').prepend(`<input type="hidden" name="label_name[]" value="${label.title}" />`); - } else { - Store.state.filters['label_name'].splice(labelIndex, 1); - labelToggleText = Store.state.filters['label_name'][0]; - $(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove(); - } - - const selectedLabels = Store.state.filters['label_name']; - if (selectedLabels.length === 0) { - labelToggleText = 'Label'; - } else if (selectedLabels.length > 1) { - labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`; - } - - $('.labels-filter .dropdown-toggle-text').text(labelToggleText); - - Store.updateFiltersUrl(); - }, mouseDown () { this.showDetail = true; }, @@ -71,6 +51,7 @@ Store.detail.issue = {}; } else { Store.detail.issue = this.issue; + Store.detail.list = this.list; } } } diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6 index 630fe084175..6906a910a2f 100644 --- a/app/assets/javascripts/boards/components/board_list.js.es6 +++ b/app/assets/javascripts/boards/components/board_list.js.es6 @@ -23,6 +23,7 @@ issues: Array, loading: Boolean, issueLinkBase: String, + rootPath: String, }, data () { return { diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6 index 2386d3a613c..b5c14a198ba 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.js.es6 +++ b/app/assets/javascripts/boards/components/board_new_issue.js.es6 @@ -37,6 +37,7 @@ $(this.$refs.submitButton).enable(); Store.detail.issue = issue; + Store.detail.list = this.list; }) .catch(() => { // Need this because our jQuery very kindly disables buttons on ALL form submissions diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js.es6 index 75dfcb66bb0..126ccdb4978 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js.es6 +++ b/app/assets/javascripts/boards/components/board_sidebar.js.es6 @@ -4,6 +4,7 @@ /* global MilestoneSelect */ /* global LabelsSelect */ /* global Sidebar */ +//= require ./sidebar/remove_issue (() => { const Store = gl.issueBoards.BoardsStore; @@ -18,7 +19,8 @@ data() { return { detail: Store.detail, - issue: {} + issue: {}, + list: {}, }; }, computed: { @@ -36,6 +38,7 @@ } this.issue = this.detail.issue; + this.list = this.detail.list; }, deep: true }, @@ -60,6 +63,9 @@ new LabelsSelect(); new Sidebar(); gl.Subscription.bindAll('.subscription'); - } + }, + components: { + removeBtn: gl.issueBoards.RemoveIssueBtn, + }, }); })(); diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js.es6 b/app/assets/javascripts/boards/components/issue_card_inner.js.es6 new file mode 100644 index 00000000000..22a8b971ff8 --- /dev/null +++ b/app/assets/javascripts/boards/components/issue_card_inner.js.es6 @@ -0,0 +1,111 @@ +/* global Vue */ +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.IssueCardInner = Vue.extend({ + props: { + issue: { + type: Object, + required: true, + }, + issueLinkBase: { + type: String, + required: true, + }, + list: { + type: Object, + required: false, + }, + rootPath: { + type: String, + required: true, + }, + }, + methods: { + showLabel(label) { + if (!this.list) return true; + + return !this.list.label || label.id !== this.list.label.id; + }, + filterByLabel(label, e) { + let labelToggleText = label.title; + const labelIndex = Store.state.filters.label_name.indexOf(label.title); + $(e.currentTarget).tooltip('hide'); + + if (labelIndex === -1) { + Store.state.filters.label_name.push(label.title); + $('.labels-filter').prepend(`<input type="hidden" name="label_name[]" value="${label.title}" />`); + } else { + Store.state.filters.label_name.splice(labelIndex, 1); + labelToggleText = Store.state.filters.label_name[0]; + $(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove(); + } + + const selectedLabels = Store.state.filters.label_name; + if (selectedLabels.length === 0) { + labelToggleText = 'Label'; + } else if (selectedLabels.length > 1) { + labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`; + } + + $('.labels-filter .dropdown-toggle-text').text(labelToggleText); + + Store.updateFiltersUrl(); + }, + labelStyle(label) { + return { + backgroundColor: label.color, + color: label.textColor, + }; + }, + }, + template: ` + <div> + <h4 class="card-title"> + <i + class="fa fa-eye-slash confidential-icon" + v-if="issue.confidential"></i> + <a + :href="issueLinkBase + '/' + issue.id" + :title="issue.title"> + {{ issue.title }} + </a> + </h4> + <div class="card-footer"> + <span + class="card-number" + v-if="issue.id"> + #{{ issue.id }} + </span> + <a + class="card-assignee has-tooltip" + :href="rootPath + issue.assignee.username" + :title="'Assigned to ' + issue.assignee.name" + v-if="issue.assignee" + data-container="body"> + <img + class="avatar avatar-inline s20" + :src="issue.assignee.avatar" + width="20" + height="20" + :alt="'Avatar for ' + issue.assignee.name" /> + </a> + <button + class="label color-label has-tooltip" + v-for="label in issue.labels" + type="button" + v-if="showLabel(label)" + @click="filterByLabel(label, $event)" + :style="labelStyle(label)" + :title="label.description" + data-container="body"> + {{ label.title }} + </button> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js.es6 b/app/assets/javascripts/boards/components/modal/empty_state.js.es6 new file mode 100644 index 00000000000..9538f5b69e9 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/empty_state.js.es6 @@ -0,0 +1,70 @@ +/* global Vue */ +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.ModalEmptyState = Vue.extend({ + mixins: [gl.issueBoards.ModalMixins], + data() { + return ModalStore.store; + }, + props: { + image: { + type: String, + required: true, + }, + newIssuePath: { + type: String, + required: true, + }, + }, + computed: { + contents() { + const obj = { + title: 'You haven\'t added any issues to your project yet', + content: ` + An issue can be a bug, a todo or a feature request that needs to be + discussed in a project. Besides, issues are searchable and filterable. + `, + }; + + if (this.activeTab === 'selected') { + obj.title = 'You haven\'t selected any issues yet'; + obj.content = ` + Go back to <strong>All issues</strong> and select some issues + to add to your board. + `; + } + + return obj; + }, + }, + template: ` + <section class="empty-state"> + <div class="row"> + <div class="col-xs-12 col-sm-6 col-sm-push-6"> + <aside class="svg-content" v-html="image"></aside> + </div> + <div class="col-xs-12 col-sm-6 col-sm-pull-6"> + <div class="text-content"> + <h4>{{ contents.title }}</h4> + <p v-html="contents.content"></p> + <a + :href="newIssuePath" + class="btn btn-success btn-inverted" + v-if="activeTab === 'all'"> + New issue + </a> + <button + type="button" + class="btn btn-default" + @click="changeTab('all')" + v-if="activeTab === 'selected'"> + All issues + </button> + </div> + </div> + </div> + </section> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js.es6 new file mode 100644 index 00000000000..a71d71106b4 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/footer.js.es6 @@ -0,0 +1,81 @@ +/* eslint-disable no-new */ +//= require ./lists_dropdown +/* global Vue */ +/* global Flash */ +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.ModalFooter = Vue.extend({ + mixins: [gl.issueBoards.ModalMixins], + data() { + return { + modal: ModalStore.store, + state: gl.issueBoards.BoardsStore.state, + }; + }, + computed: { + submitDisabled() { + return !ModalStore.selectedCount(); + }, + submitText() { + const count = ModalStore.selectedCount(); + + return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`; + }, + }, + methods: { + addIssues() { + const list = this.modal.selectedList || this.state.lists[0]; + const selectedIssues = ModalStore.getSelectedIssues(); + const issueIds = selectedIssues.map(issue => issue.globalId); + + // Post the data to the backend + gl.boardService.bulkUpdate(issueIds, { + add_label_ids: [list.label.id], + }).catch(() => { + new Flash('Failed to update issues, please try again.', 'alert'); + + selectedIssues.forEach((issue) => { + list.removeIssue(issue); + list.issuesSize -= 1; + }); + }); + + // Add the issues on the frontend + selectedIssues.forEach((issue) => { + list.addIssue(issue); + list.issuesSize += 1; + }); + + this.toggleModal(false); + }, + }, + components: { + 'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown, + }, + template: ` + <footer + class="form-actions add-issues-footer"> + <div class="pull-left"> + <button + class="btn btn-success" + type="button" + :disabled="submitDisabled" + @click="addIssues"> + {{ submitText }} + </button> + <span class="inline add-issues-footer-to-list"> + to list + </span> + <lists-dropdown></lists-dropdown> + </div> + <button + class="btn btn-default pull-right" + type="button" + @click="toggleModal(false)"> + Cancel + </button> + </footer> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6 new file mode 100644 index 00000000000..dbbcd73f1fe --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/header.js.es6 @@ -0,0 +1,68 @@ +/* global Vue */ +//= require ./tabs +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.ModalHeader = Vue.extend({ + mixins: [gl.issueBoards.ModalMixins], + data() { + return ModalStore.store; + }, + computed: { + selectAllText() { + if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) { + return 'Select all'; + } + + return 'Deselect all'; + }, + showSearch() { + return this.activeTab === 'all' && !this.loading && this.issuesCount > 0; + }, + }, + methods: { + toggleAll() { + this.$refs.selectAllBtn.blur(); + + ModalStore.toggleAll(); + }, + }, + components: { + 'modal-tabs': gl.issueBoards.ModalTabs, + }, + template: ` + <div> + <header class="add-issues-header form-actions"> + <h2> + Add issues + <button + type="button" + class="close" + data-dismiss="modal" + aria-label="Close" + @click="toggleModal(false)"> + <span aria-hidden="true">×</span> + </button> + </h2> + </header> + <modal-tabs v-if="!loading && issuesCount > 0"></modal-tabs> + <div + class="add-issues-search append-bottom-10" + v-if="showSearch"> + <input + placeholder="Search issues..." + class="form-control" + type="search" + v-model="searchTerm" /> + <button + type="button" + class="btn btn-success btn-inverted prepend-left-10" + ref="selectAllBtn" + @click="toggleAll"> + {{ selectAllText }} + </button> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6 new file mode 100644 index 00000000000..666f4e16793 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/index.js.es6 @@ -0,0 +1,134 @@ +/* global Vue */ +/* global ListIssue */ +//= require ./header +//= require ./list +//= require ./footer +//= require ./empty_state +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.IssuesModal = Vue.extend({ + props: { + blankStateImage: { + type: String, + required: true, + }, + newIssuePath: { + type: String, + required: true, + }, + issueLinkBase: { + type: String, + required: true, + }, + rootPath: { + type: String, + required: true, + }, + }, + data() { + return ModalStore.store; + }, + watch: { + page() { + this.loadIssues(); + }, + searchTerm() { + this.searchOperation(); + }, + showAddIssuesModal() { + if (this.showAddIssuesModal && !this.issues.length) { + this.loading = true; + + this.loadIssues() + .then(() => { + this.loading = false; + }); + } else if (!this.showAddIssuesModal) { + this.issues = []; + this.selectedIssues = []; + this.issuesCount = false; + } + }, + }, + methods: { + searchOperation: _.debounce(function searchOperationDebounce() { + this.loadIssues(true); + }, 500), + loadIssues(clearIssues = false) { + return gl.boardService.getBacklog({ + search: this.searchTerm, + page: this.page, + per: this.perPage, + }).then((res) => { + const data = res.json(); + + if (clearIssues) { + this.issues = []; + } + + data.issues.forEach((issueObj) => { + const issue = new ListIssue(issueObj); + const foundSelectedIssue = ModalStore.findSelectedIssue(issue); + issue.selected = !!foundSelectedIssue; + + this.issues.push(issue); + }); + + this.loadingNewPage = false; + + if (!this.issuesCount) { + this.issuesCount = data.size; + } + }); + }, + }, + computed: { + showList() { + if (this.activeTab === 'selected') { + return this.selectedIssues.length > 0; + } + + return this.issuesCount > 0; + }, + showEmptyState() { + if (!this.loading && this.issuesCount === 0) { + return true; + } + + return this.activeTab === 'selected' && this.selectedIssues.length === 0; + }, + }, + components: { + 'modal-header': gl.issueBoards.ModalHeader, + 'modal-list': gl.issueBoards.ModalList, + 'modal-footer': gl.issueBoards.ModalFooter, + 'empty-state': gl.issueBoards.ModalEmptyState, + }, + template: ` + <div + class="add-issues-modal" + v-if="showAddIssuesModal"> + <div class="add-issues-container"> + <modal-header></modal-header> + <modal-list + :issue-link-base="issueLinkBase" + :root-path="rootPath" + v-if="!loading && showList"></modal-list> + <empty-state + v-if="showEmptyState" + :image="blankStateImage" + :new-issue-path="newIssuePath"></empty-state> + <section + class="add-issues-list text-center" + v-if="loading"> + <div class="add-issues-list-loading"> + <i class="fa fa-spinner fa-spin"></i> + </div> + </section> + <modal-footer></modal-footer> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6 new file mode 100644 index 00000000000..d0901219216 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/list.js.es6 @@ -0,0 +1,142 @@ +/* global Vue */ +/* global ListIssue */ +/* global bp */ +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.ModalList = Vue.extend({ + props: { + issueLinkBase: { + type: String, + required: true, + }, + rootPath: { + type: String, + required: true, + }, + }, + data() { + return ModalStore.store; + }, + watch: { + activeTab() { + if (this.activeTab === 'all') { + ModalStore.purgeUnselectedIssues(); + } + }, + }, + computed: { + loopIssues() { + if (this.activeTab === 'all') { + return this.issues; + } + + return this.selectedIssues; + }, + groupedIssues() { + const groups = []; + this.loopIssues.forEach((issue, i) => { + const index = i % this.columns; + + if (!groups[index]) { + groups.push([]); + } + + groups[index].push(issue); + }); + + return groups; + }, + }, + methods: { + scrollHandler() { + const currentPage = Math.floor(this.issues.length / this.perPage); + + if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage + && currentPage === this.page) { + this.loadingNewPage = true; + this.page += 1; + } + }, + toggleIssue(e, issue) { + if (e.target.tagName !== 'A') { + ModalStore.toggleIssue(issue); + } + }, + listHeight() { + return this.$refs.list.getBoundingClientRect().height; + }, + scrollHeight() { + return this.$refs.list.scrollHeight; + }, + scrollTop() { + return this.$refs.list.scrollTop + this.listHeight(); + }, + showIssue(issue) { + if (this.activeTab === 'all') return true; + + const index = ModalStore.selectedIssueIndex(issue); + + return index !== -1; + }, + setColumnCount() { + const breakpoint = bp.getBreakpointSize(); + + if (breakpoint === 'lg' || breakpoint === 'md') { + this.columns = 3; + } else if (breakpoint === 'sm') { + this.columns = 2; + } else { + this.columns = 1; + } + }, + }, + mounted() { + this.scrollHandlerWrapper = this.scrollHandler.bind(this); + this.setColumnCountWrapper = this.setColumnCount.bind(this); + this.setColumnCount(); + + this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper); + window.addEventListener('resize', this.setColumnCountWrapper); + }, + beforeDestroy() { + this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper); + window.removeEventListener('resize', this.setColumnCountWrapper); + }, + components: { + 'issue-card-inner': gl.issueBoards.IssueCardInner, + }, + template: ` + <section + class="add-issues-list add-issues-list-columns" + ref="list"> + <div + v-for="group in groupedIssues" + class="add-issues-list-column"> + <div + v-for="issue in group" + v-if="showIssue(issue)" + class="card-parent"> + <div + class="card" + :class="{ 'is-active': issue.selected }" + @click="toggleIssue($event, issue)"> + <issue-card-inner + :issue="issue" + :issue-link-base="issueLinkBase" + :root-path="rootPath"> + </issue-card-inner> + <span + :aria-label="'Issue #' + issue.id + ' selected'" + aria-checked="true" + v-if="issue.selected" + class="issue-card-selected text-center"> + <i class="fa fa-check"></i> + </span> + </div> + </div> + </div> + </section> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 b/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 new file mode 100644 index 00000000000..3c05120a2da --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 @@ -0,0 +1,56 @@ +/* global Vue */ +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.ModalFooterListsDropdown = Vue.extend({ + data() { + return { + modal: ModalStore.store, + state: gl.issueBoards.BoardsStore.state, + }; + }, + computed: { + selected() { + return this.modal.selectedList || this.state.lists[0]; + }, + }, + destroyed() { + this.modal.selectedList = null; + }, + template: ` + <div class="dropdown inline"> + <button + class="dropdown-menu-toggle" + type="button" + data-toggle="dropdown" + aria-expanded="false"> + <span + class="dropdown-label-box" + :style="{ backgroundColor: selected.label.color }"> + </span> + {{ selected.title }} + <i class="fa fa-chevron-down"></i> + </button> + <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"> + <ul> + <li + v-for="list in state.lists" + v-if="list.type == 'label'"> + <a + href="#" + role="button" + :class="{ 'is-active': list.id == selected.id }" + @click.prevent="modal.selectedList = list"> + <span + class="dropdown-label-box" + :style="{ backgroundColor: list.label.color }"> + </span> + {{ list.title }} + </a> + </li> + </ul> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/tabs.js.es6 b/app/assets/javascripts/boards/components/modal/tabs.js.es6 new file mode 100644 index 00000000000..e8cb43f3503 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/tabs.js.es6 @@ -0,0 +1,47 @@ +/* global Vue */ +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.ModalTabs = Vue.extend({ + mixins: [gl.issueBoards.ModalMixins], + data() { + return ModalStore.store; + }, + computed: { + selectedCount() { + return ModalStore.selectedCount(); + }, + }, + destroyed() { + this.activeTab = 'all'; + }, + template: ` + <div class="top-area prepend-top-10 append-bottom-10"> + <ul class="nav-links issues-state-filters"> + <li :class="{ 'active': activeTab == 'all' }"> + <a + href="#" + role="button" + @click.prevent="changeTab('all')"> + All issues + <span class="badge"> + {{ issuesCount }} + </span> + </a> + </li> + <li :class="{ 'active': activeTab == 'selected' }"> + <a + href="#" + role="button" + @click.prevent="changeTab('selected')"> + Selected issues + <span class="badge"> + {{ selectedCount }} + </span> + </a> + </li> + </ul> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 b/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 new file mode 100644 index 00000000000..e74935e1cb0 --- /dev/null +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 @@ -0,0 +1,59 @@ +/* eslint-disable no-new */ +/* global Vue */ +/* global Flash */ +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.RemoveIssueBtn = Vue.extend({ + props: { + issue: { + type: Object, + required: true, + }, + list: { + type: Object, + required: true, + }, + }, + methods: { + removeIssue() { + const issue = this.issue; + const lists = issue.getLists(); + const labelIds = lists.map(list => list.label.id); + + // Post the remove data + gl.boardService.bulkUpdate([issue.globalId], { + remove_label_ids: labelIds, + }).catch(() => { + new Flash('Failed to remove issue from board, please try again.', 'alert'); + + lists.forEach((list) => { + list.addIssue(issue); + }); + }); + + // Remove from the frontend store + lists.forEach((list) => { + list.removeIssue(issue); + }); + + Store.detail.issue = {}; + }, + }, + template: ` + <div + class="block list" + v-if="list.type !== 'done'"> + <button + class="btn btn-default btn-block" + type="button" + @click="removeIssue"> + Remove from board + </button> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/mixins/modal_mixins.js.es6 b/app/assets/javascripts/boards/mixins/modal_mixins.js.es6 new file mode 100644 index 00000000000..d378b7d4baf --- /dev/null +++ b/app/assets/javascripts/boards/mixins/modal_mixins.js.es6 @@ -0,0 +1,14 @@ +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.ModalMixins = { + methods: { + toggleModal(toggle) { + ModalStore.store.showAddIssuesModal = toggle; + }, + changeTab(tab) { + ModalStore.store.activeTab = tab; + }, + }, + }; +})(); diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6 index 31531c3ee34..2d0a295ae4d 100644 --- a/app/assets/javascripts/boards/models/issue.js.es6 +++ b/app/assets/javascripts/boards/models/issue.js.es6 @@ -6,12 +6,15 @@ class ListIssue { constructor (obj) { + this.globalId = obj.id; this.id = obj.iid; this.title = obj.title; this.confidential = obj.confidential; this.dueDate = obj.due_date; this.subscribed = obj.subscribed; this.labels = []; + this.selected = false; + this.assignee = false; if (obj.assignee) { this.assignee = new ListUser(obj.assignee); diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6 index 3dd5f273057..5152be56b66 100644 --- a/app/assets/javascripts/boards/models/list.js.es6 +++ b/app/assets/javascripts/boards/models/list.js.es6 @@ -9,7 +9,7 @@ class List { this.position = obj.position; this.title = obj.title; this.type = obj.list_type; - this.preset = ['backlog', 'done', 'blank'].indexOf(this.type) > -1; + this.preset = ['done', 'blank'].indexOf(this.type) > -1; this.filters = gl.issueBoards.BoardsStore.state.filters; this.page = 1; this.loading = true; diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6 index ea55158306b..065e90518df 100644 --- a/app/assets/javascripts/boards/services/board_service.js.es6 +++ b/app/assets/javascripts/boards/services/board_service.js.es6 @@ -2,7 +2,13 @@ /* global Vue */ class BoardService { - constructor (root, boardId) { + constructor (root, bulkUpdatePath, boardId) { + this.boards = Vue.resource(`${root}{/id}.json`, {}, { + issues: { + method: 'GET', + url: `${root}/${boardId}/issues.json` + } + }); this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, { generate: { method: 'POST', @@ -10,7 +16,12 @@ class BoardService { } }); this.issue = Vue.resource(`${root}/${boardId}/issues{/id}`, {}); - this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {}); + this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {}, { + bulkUpdate: { + method: 'POST', + url: bulkUpdatePath, + }, + }); Vue.http.interceptors.push((request, next) => { request.headers['X-CSRF-Token'] = $.rails.csrfToken(); @@ -65,6 +76,20 @@ class BoardService { issue }); } + + getBacklog(data) { + return this.boards.issues(data); + } + + bulkUpdate(issueIds, extraData = {}) { + const data = { + update: Object.assign(extraData, { + issuable_ids: issueIds.join(','), + }), + }; + + return this.issues.bulkUpdate(data); + } } window.BoardService = BoardService; diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6 index cdf1b09c0a4..50842ecbaaa 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js.es6 +++ b/app/assets/javascripts/boards/stores/boards_store.js.es6 @@ -34,15 +34,10 @@ }, new (listObj) { const list = this.addList(listObj); - const backlogList = this.findList('type', 'backlog', 'backlog'); list .save() .then(() => { - // Remove any new issues from the backlog - // as they will be visible in the new list - list.issues.forEach(backlogList.removeIssue.bind(backlogList)); - this.state.lists = _.sortBy(this.state.lists, 'position'); }); this.removeBlankState(); @@ -52,7 +47,7 @@ }, shouldAddBlankState () { // Decide whether to add the blank state - return !(this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'done')[0]); + return !(this.state.lists.filter(list => list.type !== 'done')[0]); }, addBlankState () { if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return; @@ -102,7 +97,7 @@ listTo.addIssue(issue, listFrom, newIndex); } - if (listTo.type === 'done' && listFrom.type !== 'backlog') { + if (listTo.type === 'done') { issueLists.forEach((list) => { list.removeIssue(issue); }); diff --git a/app/assets/javascripts/boards/stores/modal_store.js.es6 b/app/assets/javascripts/boards/stores/modal_store.js.es6 new file mode 100644 index 00000000000..73518b42b84 --- /dev/null +++ b/app/assets/javascripts/boards/stores/modal_store.js.es6 @@ -0,0 +1,96 @@ +(() => { + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + class ModalStore { + constructor() { + this.store = { + columns: 3, + issues: [], + issuesCount: false, + selectedIssues: [], + showAddIssuesModal: false, + activeTab: 'all', + selectedList: null, + searchTerm: '', + loading: false, + loadingNewPage: false, + page: 1, + perPage: 50, + }; + } + + selectedCount() { + return this.getSelectedIssues().length; + } + + toggleIssue(issueObj) { + const issue = issueObj; + const selected = issue.selected; + + issue.selected = !selected; + + if (!selected) { + this.addSelectedIssue(issue); + } else { + this.removeSelectedIssue(issue); + } + } + + toggleAll() { + const select = this.selectedCount() !== this.store.issues.length; + + this.store.issues.forEach((issue) => { + const issueUpdate = issue; + + if (issueUpdate.selected !== select) { + issueUpdate.selected = select; + + if (select) { + this.addSelectedIssue(issue); + } else { + this.removeSelectedIssue(issue); + } + } + }); + } + + getSelectedIssues() { + return this.store.selectedIssues.filter(issue => issue.selected); + } + + addSelectedIssue(issue) { + const index = this.selectedIssueIndex(issue); + + if (index === -1) { + this.store.selectedIssues.push(issue); + } + } + + removeSelectedIssue(issue, forcePurge = false) { + if (this.store.activeTab === 'all' || forcePurge) { + this.store.selectedIssues = this.store.selectedIssues + .filter(fIssue => fIssue.id !== issue.id); + } + } + + purgeUnselectedIssues() { + this.store.selectedIssues.forEach((issue) => { + if (!issue.selected) { + this.removeSelectedIssue(issue, true); + } + }); + } + + selectedIssueIndex(issue) { + return this.store.selectedIssues.indexOf(issue); + } + + findSelectedIssue(issue) { + return this.store.selectedIssues + .filter(filteredIssue => filteredIssue.id === issue.id)[0]; + } + } + + gl.issueBoards.ModalStore = new ModalStore(); +})(); diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js index eae062a3aa3..f8dac1ff56e 100644 --- a/app/assets/javascripts/breakpoints.js +++ b/app/assets/javascripts/breakpoints.js @@ -43,6 +43,7 @@ BreakpointInstance.prototype.getBreakpointSize = function() { var $visibleDevice; $visibleDevice = this.visibleDevice; + // TODO: Consider refactoring in light of turbolinks removal. // the page refreshed via turbolinks if (!$visibleDevice().length) { this.setup(); diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 0df84234520..0152be88b48 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -1,6 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, quotes, yoda, no-else-return, consistent-return, comma-dangle, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top */ /* global Breakpoints */ -/* global Turbolinks */ (function() { var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; @@ -127,7 +126,7 @@ pageUrl += DOWN_BUILD_TRACE; } - return Turbolinks.visit(pageUrl); + return gl.utils.visitUrl(pageUrl); } }; })(this) diff --git a/app/assets/javascripts/diff.js.es6 b/app/assets/javascripts/diff.js.es6 index 35a029194d0..8741674b2d1 100644 --- a/app/assets/javascripts/diff.js.es6 +++ b/app/assets/javascripts/diff.js.es6 @@ -4,6 +4,7 @@ (() => { const UNFOLD_COUNT = 20; + let isBound = false; class Diff { constructor() { @@ -17,10 +18,12 @@ $('.content-wrapper .container-fluid').removeClass('container-limited'); } - $(document) - .off('click', '.js-unfold, .diff-line-num a') - .on('click', '.js-unfold', this.handleClickUnfold.bind(this)) - .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this)); + if (!isBound) { + $(document) + .on('click', '.js-unfold', this.handleClickUnfold.bind(this)) + .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this)); + isBound = true; + } this.openAnchoredDiff(); } diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 index 00e1c28692f..547989a6ff5 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 @@ -9,7 +9,7 @@ this.setupMapping(); this.cleanupWrapper = this.cleanup.bind(this); - document.addEventListener('page:fetch', this.cleanupWrapper); + document.addEventListener('beforeunload', this.cleanupWrapper); } cleanup() { @@ -20,7 +20,7 @@ this.setupMapping(); - document.removeEventListener('page:fetch', this.cleanupWrapper); + document.removeEventListener('beforeunload', this.cleanupWrapper); } setupMapping() { diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 029564ffc61..4e02ab7c8c1 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -1,5 +1,3 @@ -/* global Turbolinks */ - (() => { class FilteredSearchManager { constructor() { @@ -15,13 +13,13 @@ this.dropdownManager.setDropdown(); this.cleanupWrapper = this.cleanup.bind(this); - document.addEventListener('page:fetch', this.cleanupWrapper); + document.addEventListener('beforeunload', this.cleanupWrapper); } } cleanup() { this.unbindEvents(); - document.removeEventListener('page:fetch', this.cleanupWrapper); + document.removeEventListener('beforeunload', this.cleanupWrapper); } bindEvents() { @@ -200,7 +198,9 @@ paths.push(`search=${sanitized}`); } - Turbolinks.visit(`?scope=all&utf8=✓&${paths.join('&')}`); + const parameterizedUrl = `?scope=all&utf8=✓&${paths.join('&')}`; + + gl.utils.visitUrl(parameterizedUrl); } getUsernameParams() { diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 5c86e98567a..d9101b55c7f 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -1,6 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */ /* global fuzzaldrinPlus */ -/* global Turbolinks */ (function() { var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, @@ -723,7 +722,7 @@ if ($el.length) { var href = $el.attr('href'); if (href && href !== '#') { - Turbolinks.visit(href); + gl.utils.visitUrl(href); } else { $el.first().trigger('click'); } diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6 index f63d700fd65..8df86f68218 100644 --- a/app/assets/javascripts/issuable.js.es6 +++ b/app/assets/javascripts/issuable.js.es6 @@ -1,6 +1,5 @@ /* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, wrap-iife, max-len */ /* global Issuable */ -/* global Turbolinks */ ((global) => { var issuable_created; @@ -119,7 +118,7 @@ issuesUrl = formAction; issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&'); issuesUrl += formData; - return Turbolinks.visit(issuesUrl); + return gl.utils.visitUrl(issuesUrl); }; })(this), initResetFilters: function() { @@ -130,7 +129,7 @@ const baseIssuesUrl = target.href; $form.attr('action', baseIssuesUrl); - Turbolinks.visit(baseIssuesUrl); + gl.utils.visitUrl(baseIssuesUrl); }); }, initChecks: function() { diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 index e810ee85bd3..2955bda1a36 100644 --- a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 +++ b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 @@ -95,7 +95,6 @@ const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`; history.replaceState({ - turbolinks: true, url: newState, }, document.title, newState); return newState; diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 6bb575059b7..d9370db0cf2 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -161,6 +161,9 @@ gl.text.humanize = function(string) { return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1); }; + gl.text.pluralize = function(str, count) { + return str + (count > 1 || count === 0 ? 's' : ''); + }; return gl.text.truncate = function(string, maxLength) { return string.substr(0, (maxLength - 3)) + '...'; }; diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js.es6 index 8e15bf0735c..a1558b371f0 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js.es6 @@ -76,5 +76,11 @@ hashIndex = url.indexOf('#'); return hashIndex === -1 ? null : url.substring(hashIndex + 1); }; + + w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href); + + w.gl.utils.visitUrl = (url) => { + document.location.href = url; + }; })(window); }).call(this); diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js index 2f147704c22..78e338033e3 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -171,7 +171,6 @@ // This method is stubbed in tests. LineHighlighter.prototype.__setLocationHash__ = function(value) { return history.pushState({ - turbolinks: false, url: value // We're using pushState instead of assigning location.hash directly to // prevent the page from scrolling on the hashchange event diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js index ea9bfb4860a..1b0d0768db8 100644 --- a/app/assets/javascripts/logo.js +++ b/app/assets/javascripts/logo.js @@ -1,14 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback */ -/* global Turbolinks */ (function() { - Turbolinks.enableProgressBar(); - - $(document).on('page:fetch', function() { + window.addEventListener('beforeunload', function() { $('.tanuki-logo').addClass('animate'); }); - - $(document).on('page:change', function() { - $('.tanuki-logo').removeClass('animate'); - }); }).call(this); diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 index dabba9e1fa9..e8b4b9bf73a 100644 --- a/app/assets/javascripts/merge_request_tabs.js.es6 +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -179,12 +179,13 @@ // Ensure parameters and hash come along for the ride newState += location.search + location.hash; + // TODO: Consider refactoring in light of turbolinks removal. + // Replace the current history state with the new one without breaking // Turbolinks' history. // // See https://github.com/rails/turbolinks/issues/363 window.history.replaceState({ - turbolinks: true, url: newState, }, document.title, newState); diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index fa782ebbedf..2c19029d175 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -2,7 +2,6 @@ /* global notify */ /* global notifyPermissions */ /* global merge_request_widget */ -/* global Turbolinks */ ((global) => { var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; }; @@ -69,13 +68,13 @@ } MergeRequestWidget.prototype.clearEventListeners = function() { - return $(document).off('page:change.merge_request'); + return $(document).off('DOMContentLoaded'); }; MergeRequestWidget.prototype.addEventListeners = function() { var allowedPages; allowedPages = ['show', 'commits', 'pipelines', 'changes']; - $(document).on('page:change.merge_request', (function(_this) { + $(document).on('DOMContentLoaded', (function(_this) { return function() { var page; page = $('body').data('page').split(':').last(); diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index 67f8804666d..71719917d0c 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -1,6 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */ /* global Cookies */ -/* global Turbolinks */ /* global ProjectSelect */ (function() { @@ -58,8 +57,8 @@ }; Project.prototype.initRefSwitcher = function() { - var refListItem = document.createElement('li'), - refLink = document.createElement('a'); + var refListItem = document.createElement('li'); + var refLink = document.createElement('a'); refLink.href = '#'; @@ -118,7 +117,7 @@ var $form = $dropdown.closest('form'); var action = $form.attr('action'); var divider = action.indexOf('?') < 0 ? '?' : '&'; - Turbolinks.visit(action + '' + divider + '' + $form.serialize()); + gl.utils.visitUrl(action + '' + divider + '' + $form.serialize()); } } }); diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js index 6614d8952cd..d7943959238 100644 --- a/app/assets/javascripts/project_import.js +++ b/app/assets/javascripts/project_import.js @@ -1,11 +1,10 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, max-len */ -/* global Turbolinks */ (function() { this.ProjectImport = (function() { function ProjectImport() { setTimeout(function() { - return Turbolinks.visit(location.href); + return gl.utils.visitUrl(location.href); }, 5000); } diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js index 0caf8ba4344..bdbad93ad04 100644 --- a/app/assets/javascripts/render_gfm.js +++ b/app/assets/javascripts/render_gfm.js @@ -9,7 +9,7 @@ this.find('.js-render-math').renderMath(); }; - $(document).on('ready page:load', function() { + $(document).on('ready load', function() { return $('body').renderGFM(); }); }).call(this); diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js index c56ee429b8e..c6d9b007ad1 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/shortcuts.js @@ -1,6 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */ /* global Mousetrap */ -/* global Turbolinks */ /* global findFileURL */ (function() { @@ -23,7 +22,7 @@ Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], this.toggleMarkdownPreview); if (typeof findFileURL !== "undefined" && findFileURL !== null) { Mousetrap.bind('t', function() { - return Turbolinks.visit(findFileURL); + return gl.utils.visitUrl(findFileURL); }); } } diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index 4dcc5ebe28f..3501974a8c9 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -1,6 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators */ /* global Mousetrap */ -/* global Turbolinks */ /* global ShortcutsNavigation */ /* global sidebar */ @@ -80,7 +79,7 @@ ShortcutsIssuable.prototype.editIssue = function() { var $editBtn; $editBtn = $('.issuable-edit'); - return Turbolinks.visit($editBtn.attr('href')); + return gl.utils.visitUrl($editBtn.attr('href')); }; ShortcutsIssuable.prototype.openSidebarDropdown = function(name) { diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6 index 05234643c18..ee172f2fa6f 100644 --- a/app/assets/javascripts/sidebar.js.es6 +++ b/app/assets/javascripts/sidebar.js.es6 @@ -40,7 +40,7 @@ .on('click', sidebarToggleSelector, () => this.toggleSidebar()) .on('click', pinnedToggleSelector, () => this.togglePinnedState()) .on('click', 'html, body', (e) => this.handleClickEvent(e)) - .on('page:change', () => this.renderState()) + .on('DOMContentLoaded', () => this.renderState()) .on('todo:toggle', (e, count) => this.updateTodoCount(count)); this.renderState(); } diff --git a/app/assets/javascripts/smart_interval.js.es6 b/app/assets/javascripts/smart_interval.js.es6 index 40f67637c7c..d1bdc353be2 100644 --- a/app/assets/javascripts/smart_interval.js.es6 +++ b/app/assets/javascripts/smart_interval.js.es6 @@ -89,7 +89,7 @@ destroy() { this.cancel(); document.removeEventListener('visibilitychange', this.handleVisibilityChange); - $(document).off('visibilitychange').off('page:before-unload'); + $(document).off('visibilitychange').off('beforeunload'); } /* private */ @@ -111,8 +111,9 @@ } initPageUnloadHandling() { + // TODO: Consider refactoring in light of turbolinks removal. // prevent interval continuing after page change, when kept in cache by Turbolinks - $(document).on('page:before-unload', () => this.cancel()); + $(document).on('beforeunload', () => this.cancel()); } handleVisibilityChange(e) { diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js.es6 index 05622916ff8..96c7d927509 100644 --- a/app/assets/javascripts/todos.js.es6 +++ b/app/assets/javascripts/todos.js.es6 @@ -1,6 +1,5 @@ /* eslint-disable class-methods-use-this, no-new, func-names, prefer-template, no-unneeded-ternary, object-shorthand, space-before-function-paren, comma-dangle, quote-props, consistent-return, no-else-return, no-param-reassign, max-len */ /* global UsersSelect */ -/* global Turbolinks */ ((global) => { class Todos { @@ -34,7 +33,7 @@ $('form.filter-form').on('submit', function (event) { event.preventDefault(); - Turbolinks.visit(this.action + '&' + $(this).serialize()); + gl.utils.visitUrl(this.action + '&' + $(this).serialize()); }); } @@ -142,7 +141,7 @@ }; url = gl.utils.mergeUrlParams(pageParams, url); } - return Turbolinks.visit(url); + return gl.utils.visitUrl(url); } } @@ -156,7 +155,7 @@ e.preventDefault(); return window.open(todoLink, '_blank'); } else { - return Turbolinks.visit(todoLink); + return gl.utils.visitUrl(todoLink); } } } diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js index d124ca4f88b..b1b35fdbd6c 100644 --- a/app/assets/javascripts/tree.js +++ b/app/assets/javascripts/tree.js @@ -1,5 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, max-len */ -/* global Turbolinks */ + (function() { this.TreeView = (function() { function TreeView() { @@ -15,7 +15,7 @@ e.preventDefault(); return window.open(path, '_blank'); } else { - return Turbolinks.visit(path); + return gl.utils.visitUrl(path); } } }); @@ -57,7 +57,7 @@ } else if (e.which === 13) { path = $('.tree-item.selected .tree-item-file-name a').attr('href'); if (path) { - return Turbolinks.visit(path); + return gl.utils.visitUrl(path); } } }); diff --git a/app/assets/javascripts/user_tabs.js.es6 b/app/assets/javascripts/user_tabs.js.es6 index 313fb17aee8..465618e3d53 100644 --- a/app/assets/javascripts/user_tabs.js.es6 +++ b/app/assets/javascripts/user_tabs.js.es6 @@ -149,7 +149,6 @@ content on the Users#show page. new_state = new_state.replace(/\/+$/, ''); new_state += this._location.search + this._location.hash; history.replaceState({ - turbolinks: true, url: new_state }, document.title, new_state); return new_state; diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 index ac2fe99af1c..ec4d6ee51d2 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 @@ -1,4 +1,4 @@ -/* global Vue, Turbolinks, gl */ +/* global Vue, gl */ /* eslint-disable no-param-reassign */ //= require vue_shared/components/table_pagination @@ -35,7 +35,7 @@ }, methods: { change(pagenum, apiScope) { - Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`); + gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`); }, }, template: ` diff --git a/app/assets/javascripts/vue_realtime_listener/index.js.es6 b/app/assets/javascripts/vue_realtime_listener/index.js.es6 index 23cac1466d2..95564152cce 100644 --- a/app/assets/javascripts/vue_realtime_listener/index.js.es6 +++ b/app/assets/javascripts/vue_realtime_listener/index.js.es6 @@ -7,12 +7,12 @@ window.removeEventListener('beforeunload', removeIntervals); window.removeEventListener('focus', startIntervals); window.removeEventListener('blur', removeIntervals); - document.removeEventListener('page:fetch', removeAll); + document.removeEventListener('beforeunload', removeAll); }; window.addEventListener('beforeunload', removeIntervals); window.addEventListener('focus', startIntervals); window.addEventListener('blur', removeIntervals); - document.addEventListener('page:fetch', removeAll); + document.addEventListener('beforeunload', removeAll); }; })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 index 605824fa939..d94caa983cd 100644 --- a/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 +++ b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 @@ -13,6 +13,8 @@ gl.VueGlPagination = Vue.extend({ props: { + // TODO: Consider refactoring in light of turbolinks removal. + /** This function will take the information given by the pagination component And make a new Turbolinks call @@ -20,7 +22,7 @@ Here is an example `change` method: change(pagenum, apiScope) { - Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`); + gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`); }, */ diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 3cf49f4ff1b..08f203a1bf6 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -31,7 +31,6 @@ @import "framework/modal.scss"; @import "framework/nav.scss"; @import "framework/pagination.scss"; -@import "framework/progress.scss"; @import "framework/panels.scss"; @import "framework/selects.scss"; @import "framework/sidebar.scss"; diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 6bfb9a6d1cb..ca5861bf3e6 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -227,6 +227,11 @@ } } +.dropdown-menu-drop-up { + top: auto; + bottom: 100%; +} + .dropdown-menu-large { width: 340px; } diff --git a/app/assets/stylesheets/framework/progress.scss b/app/assets/stylesheets/framework/progress.scss deleted file mode 100644 index e9800bd24b5..00000000000 --- a/app/assets/stylesheets/framework/progress.scss +++ /dev/null @@ -1,5 +0,0 @@ -html.turbolinks-progress-bar::before { - background-color: $progress-color!important; - height: 2px!important; - box-shadow: 0 0 10px $progress-color, 0 0 5px $progress-color; -} diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index f2d60bff2b5..9b413f3e61c 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -250,7 +250,7 @@ } .issue-boards-search { - width: 290px; + width: 395px; .form-control { display: inline-block; @@ -354,3 +354,135 @@ padding-right: 0; } } + +.add-issues-modal { + display: -webkit-flex; + display: flex; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: rgba($black, .3); + z-index: 9999; +} + +.add-issues-container { + display: -webkit-flex; + display: flex; + -webkit-flex-direction: column; + flex-direction: column; + width: 90vw; + height: 85vh; + max-width: 1100px; + min-height: 500px; + margin: auto; + padding: 25px 15px 0; + background-color: $white-light; + border-radius: $border-radius-default; + box-shadow: 0 2px 12px rgba($black, .5); + + .empty-state { + display: -webkit-flex; + display: flex; + -webkit-flex: 1; + flex: 1; + margin-top: 0; + + > .row { + width: 100%; + margin: auto 0; + } + + .svg-content { + margin-top: -40px; + } + } +} + +.add-issues-header { + margin: -25px -15px -5px; + border-top: 0; + border-bottom: 1px solid $border-color; + border-top-right-radius: $border-radius-default; + border-top-left-radius: $border-radius-default; + + > h2 { + margin: 0; + font-size: 18px; + } +} + +.add-issues-search { + display: -webkit-flex; + display: flex; +} + +.add-issues-list-column { + width: 100%; + + @media (min-width: $screen-sm-min) { + width: 50%; + } + + @media (min-width: $screen-md-min) { + width: (100% / 3); + } +} + +.add-issues-list { + display: -webkit-flex; + display: flex; + -webkit-flex: 1; + flex: 1; + padding-top: 3px; + margin-left: -$gl-vert-padding; + margin-right: -$gl-vert-padding; + overflow-y: scroll; + + .card-parent { + padding: 0 5px 5px; + } + + .card { + border: 1px solid $border-gray-dark; + box-shadow: 0 1px 2px rgba($issue-boards-card-shadow, .3); + cursor: pointer; + } +} + +.add-issues-list-loading { + -webkit-align-self: center; + align-self: center; + width: 100%; + padding-left: $gl-vert-padding; + padding-right: $gl-vert-padding; + font-size: 35px; +} + +.add-issues-footer { + margin: auto -15px 0; + padding-left: 15px; + padding-right: 15px; + border-bottom-right-radius: $border-radius-default; + border-bottom-left-radius: $border-radius-default; +} + +.add-issues-footer-to-list { + padding-left: $gl-vert-padding; + padding-right: $gl-vert-padding; + line-height: 34px; +} + +.issue-card-selected { + position: absolute; + right: -3px; + top: -3px; + width: 17px; + background-color: $blue-light; + color: $white-light; + border: 1px solid $border-blue-light; + font-size: 9px; + line-height: 15px; + border-radius: 50%; +} diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index c08eb811532..3ba8c2f8bb9 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -10,10 +10,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController @projects = @projects.sort(@sort = params[:sort]) @projects = @projects.page(params[:page]) - @last_push = current_user.recent_push - respond_to do |format| - format.html + format.html { @last_push = current_user.recent_push } format.atom do event_filter load_events diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb index dc33e1405f2..61fef4dc133 100644 --- a/app/controllers/projects/boards/issues_controller.rb +++ b/app/controllers/projects/boards/issues_controller.rb @@ -7,7 +7,7 @@ module Projects def index issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute - issues = issues.page(params[:page]) + issues = issues.page(params[:page]).per(params[:per] || 20) render json: { issues: serialize_as_json(issues), @@ -59,7 +59,7 @@ module Projects end def filter_params - params.merge(board_id: params[:board_id], id: params[:list_id]) + params.merge(board_id: params[:board_id], id: params[:list_id]).compact end def move_params @@ -73,7 +73,7 @@ module Projects def serialize_as_json(resource) resource.as_json( labels: true, - only: [:iid, :title, :confidential, :due_date], + only: [:id, :iid, :title, :confidential, :due_date], include: { assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, milestone: { only: [:id, :title] } diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index 38c586ccd31..f43827da446 100644 --- a/app/helpers/boards_helper.rb +++ b/app/helpers/boards_helper.rb @@ -6,7 +6,9 @@ module BoardsHelper endpoint: namespace_project_boards_path(@project.namespace, @project), board_id: board.id, disabled: "#{!can?(current_user, :admin_list, @project)}", - issue_link_base: namespace_project_issues_path(@project.namespace, @project) + issue_link_base: namespace_project_issues_path(@project.namespace, @project), + root_path: root_path, + bulk_update_path: bulk_update_namespace_project_issues_path(@project.namespace, @project), } end end diff --git a/app/helpers/javascript_helper.rb b/app/helpers/javascript_helper.rb index 0e456214d37..cd4075b340d 100644 --- a/app/helpers/javascript_helper.rb +++ b/app/helpers/javascript_helper.rb @@ -1,5 +1,5 @@ module JavascriptHelper def page_specific_javascript_tag(js) - javascript_include_tag asset_path(js), { "data-turbolinks-track" => true } + javascript_include_tag asset_path(js) end end diff --git a/app/models/board.rb b/app/models/board.rb index c56422914a9..2780acc67c0 100644 --- a/app/models/board.rb +++ b/app/models/board.rb @@ -5,10 +5,6 @@ class Board < ActiveRecord::Base validates :project, presence: true - def backlog_list - lists.merge(List.backlog).take - end - def done_list lists.merge(List.done).take end diff --git a/app/models/list.rb b/app/models/list.rb index 065d75bd1dc..1e5da7f4dd4 100644 --- a/app/models/list.rb +++ b/app/models/list.rb @@ -2,7 +2,7 @@ class List < ActiveRecord::Base belongs_to :board belongs_to :label - enum list_type: { backlog: 0, label: 1, done: 2 } + enum list_type: { label: 1, done: 2 } validates :board, :list_type, presence: true validates :label, :position, presence: true, if: :label? diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb index 9bdd7b6f0cf..f6275a63109 100644 --- a/app/services/boards/create_service.rb +++ b/app/services/boards/create_service.rb @@ -12,7 +12,6 @@ module Boards def create_board! board = project.boards.create - board.lists.create(list_type: :backlog) board.lists.create(list_type: :done) board diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index fd4a462c7b2..8a94c54b6ab 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -3,8 +3,8 @@ module Boards class ListService < BaseService def execute issues = IssuesFinder.new(current_user, filter_params).execute - issues = without_board_labels(issues) unless list.movable? - issues = with_list_label(issues) if list.movable? + issues = without_board_labels(issues) unless movable_list? + issues = with_list_label(issues) if movable_list? issues end @@ -15,7 +15,13 @@ module Boards end def list - @list ||= board.lists.find(params[:id]) + return @list if defined?(@list) + + @list = board.lists.find(params[:id]) if params.key?(:id) + end + + def movable_list? + @movable_list ||= list.present? && list.movable? end def filter_params @@ -40,7 +46,7 @@ module Boards end def set_state - params[:state] = list.done? ? 'closed' : 'opened' + params[:state] = list && list.done? ? 'closed' : 'opened' end def board_label_ids diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb index 3566a8ba92f..3e0a85cf059 100644 --- a/app/services/slash_commands/interpret_service.rb +++ b/app/services/slash_commands/interpret_service.rb @@ -304,6 +304,18 @@ module SlashCommands params '@user' command :cc + desc 'Defines target branch for MR' + params '<Local branch name>' + condition do + issuable.respond_to?(:target_branch) && + (current_user.can?(:"update_#{issuable.to_ability_name}", issuable) || + issuable.new_record?) + end + command :target_branch do |target_branch_param| + branch_name = target_branch_param.strip + @updates[:target_branch] = branch_name if project.repository.branch_names.include?(branch_name) + end + def find_label_ids(labels_param) label_ids_by_reference = extract_references(labels_param, :label).map(&:id) labels_ids_by_name = LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute.select(:id) diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index e87a16a5157..f92f89e73ff 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -6,4 +6,4 @@ - providers.each do |provider| %span.light - has_icon = provider_has_icon?(provider) - = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn'), "data-no-turbolink" => "true" + = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn') diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml index b185b81db7f..5b1a4630c56 100644 --- a/app/views/groups/group_members/_new_group_member.html.haml +++ b/app/views/groups/group_members/_new_group_member.html.haml @@ -3,7 +3,7 @@ .col-md-4.col-lg-6 = users_select_tag(:user_ids, multiple: true, class: 'input-clamp', scope: :all, email_user: true) .help-block.append-bottom-10 - Search for users by name, username, or email, or invite new ones using their email address. + Search for members by name, username, or email, or invite new ones using their email address. .col-md-3.col-lg-2 = select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "form-control project-access-select" @@ -16,7 +16,7 @@ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date' %i.clear-icon.js-clear-input .help-block.append-bottom-10 - On this date, the user(s) will automatically lose access to this group and all of its projects. + On this date, the member(s) will automatically lose access to this group and all of its projects. .col-md-2 = f.submit 'Add to group', class: "btn btn-create btn-block" diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index f4c432a095a..2e4e4511bb6 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -7,7 +7,7 @@ - if can?(current_user, :admin_group_member, @group) .project-members-new.append-bottom-default %p.clearfix - Add new user to + Add new member to %strong= @group.name = render "new_group_member" @@ -15,7 +15,7 @@ .append-bottom-default.clearfix %h5.member.existing-title - Existing users + Existing members = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do .form-group = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } @@ -24,7 +24,7 @@ = render 'shared/members/sort_dropdown' .panel.panel-default .panel-heading - Users with access to + Members with access to %strong= @group.name %span.badge= @members.total_count %ul.content-list diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index 7f1b9ee7141..e18bd47798b 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -82,7 +82,7 @@ rather than Git. Please convert = link_to 'them to Git,', 'https://www.atlassian.com/git/tutorials/migrating-overview' and go through the - = link_to 'import flow', status_import_bitbucket_path, 'data-no-turbolink' => 'true' + = link_to 'import flow', status_import_bitbucket_path again. .js-importer-status{ data: { jobs_import_path: "#{jobs_import_bitbucket_path}", import_path: "#{import_bitbucket_path}" } } diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 3096f0ee19e..79e96f54936 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -33,6 +33,8 @@ - if content_for?(:page_specific_javascripts) = yield :page_specific_javascripts + = yield :project_javascripts + = csrf_meta_tags - unless browser.safari? diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 935517d4913..248d439cd05 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -4,9 +4,6 @@ %body{ class: "#{user_application_theme}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } } = Gon::Base.render_data - -# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body. - = yield :scripts_body_top - = render "layouts/header/default", title: header_title = render 'layouts/page', sidebar: sidebar, nav: nav diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index 277eb71ea73..f5e7ea7710d 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -3,7 +3,7 @@ - header_title project_title(@project) unless header_title - nav "project" -- content_for :scripts_body_top do +- content_for :project_javascripts do - project = @target_project || @project - if @project_wiki && @page - preview_markdown_path = namespace_project_wiki_preview_markdown_path(project.namespace, project, @page.slug) diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 14b330d16ad..a4f4079d556 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -82,7 +82,7 @@ = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do Disconnect - else - = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active', "data-no-turbolink" => "true" do + = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active' do Connect %hr - if current_user.can_change_username? diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml index 356bd50f7f3..13bc20f2ae2 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/projects/boards/_show.html.haml @@ -24,5 +24,10 @@ ":list" => "list", ":disabled" => "disabled", ":issue-link-base" => "issueLinkBase", + ":root-path" => "rootPath", ":key" => "_uid" } = render "projects/boards/components/sidebar" + %board-add-issues-modal{ "blank-state-image" => render('shared/empty_states/icons/issues.svg'), + "new-issue-path" => new_namespace_project_issue_path(@project.namespace, @project), + ":issue-link-base" => "issueLinkBase", + ":root-path" => "rootPath" } diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml index a2e5118a9f3..72bce4049de 100644 --- a/app/views/projects/boards/components/_board.html.haml +++ b/app/views/projects/boards/components/_board.html.haml @@ -29,6 +29,7 @@ ":loading" => "list.loading", ":disabled" => "disabled", ":issue-link-base" => "issueLinkBase", + ":root-path" => "rootPath", "ref" => "board-list" } - if can?(current_user, :admin_list, @project) = render "projects/boards/components/blank_state" diff --git a/app/views/projects/boards/components/_board_list.html.haml b/app/views/projects/boards/components/_board_list.html.haml index 34fdb1f6a74..f413a5e94c1 100644 --- a/app/views/projects/boards/components/_board_list.html.haml +++ b/app/views/projects/boards/components/_board_list.html.haml @@ -34,6 +34,7 @@ ":list" => "list", ":issue" => "issue", ":issue-link-base" => "issueLinkBase", + ":root-path" => "rootPath", ":disabled" => "disabled", ":key" => "issue.id" } %li.board-list-count.text-center{ "v-if" => "showCount" } diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml index e4c2aff46ec..891c2c46251 100644 --- a/app/views/projects/boards/components/_card.html.haml +++ b/app/views/projects/boards/components/_card.html.haml @@ -4,25 +4,7 @@ "@mousedown" => "mouseDown", "@mousemove" => "mouseMove", "@mouseup" => "showIssue($event)" } - %h4.card-title - = icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential") - %a{ ":href" => 'issueLinkBase + "/" + issue.id', - ":title" => "issue.title" } - {{ issue.title }} - .card-footer - %span.card-number{ "v-if" => "issue.id" } - = precede '#' do - {{ issue.id }} - %a.has-tooltip{ ":href" => "\"#{root_path}\" + issue.assignee.username", - ":title" => '"Assigned to " + issue.assignee.name', - "v-if" => "issue.assignee", - data: { container: 'body' } } - %img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20, alt: "Avatar" } - %button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels", - type: "button", - "v-if" => "(!list.label || label.id !== list.label.id)", - "@click" => "filterByLabel(label, $event)", - ":style" => "{ backgroundColor: label.color, color: label.textColor }", - ":title" => "label.description", - data: { container: 'body' } } - {{ label.title }} + %issue-card-inner{ ":list" => "list", + ":issue" => "issue", + ":issue-link-base" => "issueLinkBase", + ":root-path" => "rootPath" } diff --git a/app/views/projects/boards/components/_sidebar.html.haml b/app/views/projects/boards/components/_sidebar.html.haml index df7fa9ddaf2..24d76da6f06 100644 --- a/app/views/projects/boards/components/_sidebar.html.haml +++ b/app/views/projects/boards/components/_sidebar.html.haml @@ -22,3 +22,5 @@ = render "projects/boards/components/sidebar/due_date" = render "projects/boards/components/sidebar/labels" = render "projects/boards/components/sidebar/notifications" + %remove-btn{ ":issue" => "issue", + ":list" => "list" } diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index f361204ecac..98d78af2e45 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -8,8 +8,12 @@ - last_line = right.new_pos if right %tr.line_holder.parallel - if left - - if left.meta? + - case left.type + - when 'match' = diff_match_line left.old_pos, nil, text: left.text, view: :parallel + - when 'nonewline' + %td.old_line.diff-line-num + %td.line_content.match= left.text - else - left_line_code = diff_file.line_code(left) - left_position = diff_file.position(left) @@ -21,8 +25,12 @@ %td.line_content.parallel - if right - - if right.meta? + - case right.type + - when 'match' = diff_match_line nil, right.new_pos, text: left.text, view: :parallel + - when 'nonewline' + %td.new_line.diff-line-num + %td.line_content.match= right.text - else - right_line_code = diff_file.line_code(right) - right_position = diff_file.position(right) diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 3e554063d8b..cc4b1f58b27 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -109,10 +109,10 @@ = render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.merge_commit, title: @merge_request.title :javascript - var merge_request; - - merge_request = new MergeRequest({ - action: "#{controller.action_name}" + $(function () { + new MergeRequest({ + action: "#{controller.action_name}" + }); }); var mrRefreshWidgetUrl = "#{mr_widget_refresh_url(@merge_request)}"; diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 064e92b15eb..cd685f7d0eb 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -50,7 +50,7 @@ = icon('github', text: 'GitHub') %div - if bitbucket_import_enabled? - = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}", "data-no-turbolink" => "true" do + = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do = icon('bitbucket', text: 'Bitbucket') - unless bitbucket_import_configured? = render 'bitbucket_import_modal' diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index b42eaabb111..2ad06dcf25b 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -38,8 +38,9 @@ #js-boards-search.issue-boards-search %input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" } - if can?(current_user, :admin_list, @project) + #js-add-issues-btn.pull-right.prepend-left-10 .dropdown.pull-right - %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } } + %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } } Add list .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" } @@ -91,5 +92,5 @@ new SubscriptionSelect(); $('form.filter-form').on('submit', function (event) { event.preventDefault(); - Turbolinks.visit(this.action + '&' + $(this).serialize()); + gl.utils.visitUrl(this.action + '&' + $(this).serialize()); }); |