diff options
author | Phil Hughes <me@iamphill.com> | 2017-04-14 15:42:10 +0100 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2017-04-14 15:42:10 +0100 |
commit | 302e855f52b5d914e64bf6fbcc7e6544904c4e77 (patch) | |
tree | 10ec4d64d0136080685a8d93e5c0d0c324c54f23 | |
parent | 72fefba4c18e2464725231b3426ec2e3b0eda9c4 (diff) | |
parent | 3c9318a8c58f638f57cfe86cb2f84625445996c8 (diff) | |
download | gitlab-ce-302e855f52b5d914e64bf6fbcc7e6544904c4e77.tar.gz |
Merge branch 'master' into 28433-internationalise-cycle-analytics-page
91 files changed, 2509 insertions, 2319 deletions
@@ -73,6 +73,9 @@ gem 'grape', '~> 0.19.0' gem 'grape-entity', '~> 0.6.0' gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' +# Disable strong_params so that Mash does not respond to :permitted? +gem 'hashie-forbidden_attributes' + # Pagination gem 'kaminari', '~> 0.17.0' diff --git a/Gemfile.lock b/Gemfile.lock index eeec4b67764..ca4084e18a2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -336,7 +336,7 @@ GEM grape-entity (0.6.0) activesupport multi_json (>= 1.3.2) - grpc (1.2.2) + grpc (1.1.2) google-protobuf (~> 3.1) googleauth (~> 0.5.1) haml (4.0.7) @@ -352,6 +352,8 @@ GEM tilt hashdiff (0.3.2) hashie (3.5.5) + hashie-forbidden_attributes (0.1.1) + hashie (>= 3.0) health_check (2.6.0) rails (>= 4.0) hipchat (1.5.2) @@ -925,6 +927,7 @@ DEPENDENCIES grape-entity (~> 0.6.0) haml_lint (~> 0.21.0) hamlit (~> 2.6.1) + hashie-forbidden_attributes health_check (~> 2.6.0) hipchat (~> 1.5.0) html-pipeline (~> 1.11.0) diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index 93b8960da2e..239eeacf2d7 100644 --- a/app/assets/javascripts/boards/components/board.js +++ b/app/assets/javascripts/boards/components/board.js @@ -7,100 +7,98 @@ import boardBlankState from './board_blank_state'; require('./board_delete'); require('./board_list'); -(() => { - const Store = gl.issueBoards.BoardsStore; +const Store = gl.issueBoards.BoardsStore; - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; +window.gl = window.gl || {}; +window.gl.issueBoards = window.gl.issueBoards || {}; - gl.issueBoards.Board = Vue.extend({ - template: '#js-board-template', - components: { - boardList, - 'board-delete': gl.issueBoards.BoardDelete, - boardBlankState, - }, - props: { - list: Object, - disabled: Boolean, - issueLinkBase: String, - rootPath: String, - }, - data () { - return { - detailIssue: Store.detail, - filter: Store.filter, - }; - }, - watch: { - filter: { - handler() { - this.list.page = 1; - this.list.getIssues(true); - }, - deep: true, +gl.issueBoards.Board = Vue.extend({ + template: '#js-board-template', + components: { + boardList, + 'board-delete': gl.issueBoards.BoardDelete, + boardBlankState, + }, + props: { + list: Object, + disabled: Boolean, + issueLinkBase: String, + rootPath: String, + }, + data () { + return { + detailIssue: Store.detail, + filter: Store.filter, + }; + }, + watch: { + filter: { + handler() { + this.list.page = 1; + this.list.getIssues(true); }, - detailIssue: { - handler () { - if (!Object.keys(this.detailIssue.issue).length) return; + deep: true, + }, + detailIssue: { + handler () { + if (!Object.keys(this.detailIssue.issue).length) return; - const issue = this.list.findIssue(this.detailIssue.issue.id); + const issue = this.list.findIssue(this.detailIssue.issue.id); - if (issue) { - const offsetLeft = this.$el.offsetLeft; - const boardsList = document.querySelectorAll('.boards-list')[0]; - const left = boardsList.scrollLeft - offsetLeft; - let right = (offsetLeft + this.$el.offsetWidth); + if (issue) { + const offsetLeft = this.$el.offsetLeft; + const boardsList = document.querySelectorAll('.boards-list')[0]; + const left = boardsList.scrollLeft - offsetLeft; + let right = (offsetLeft + this.$el.offsetWidth); - if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) { - // -290 here because width of boardsList is animating so therefore - // getting the width here is incorrect - // 290 is the width of the sidebar - right -= (boardsList.offsetWidth - 290); - } else { - right -= boardsList.offsetWidth; - } + if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) { + // -290 here because width of boardsList is animating so therefore + // getting the width here is incorrect + // 290 is the width of the sidebar + right -= (boardsList.offsetWidth - 290); + } else { + right -= boardsList.offsetWidth; + } - if (right - boardsList.scrollLeft > 0) { - $(boardsList).animate({ - scrollLeft: right - }, this.sortableOptions.animation); - } else if (left > 0) { - $(boardsList).animate({ - scrollLeft: offsetLeft - }, this.sortableOptions.animation); - } + if (right - boardsList.scrollLeft > 0) { + $(boardsList).animate({ + scrollLeft: right + }, this.sortableOptions.animation); + } else if (left > 0) { + $(boardsList).animate({ + scrollLeft: offsetLeft + }, this.sortableOptions.animation); } - }, - deep: true - } - }, - methods: { - showNewIssueForm() { - this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm; - } - }, - mounted () { - this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({ - disabled: this.disabled, - group: 'boards', - draggable: '.is-draggable', - handle: '.js-board-handle', - onEnd: (e) => { - gl.issueBoards.onEnd(); + } + }, + deep: true + } + }, + methods: { + showNewIssueForm() { + this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm; + } + }, + mounted () { + this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({ + disabled: this.disabled, + group: 'boards', + draggable: '.is-draggable', + handle: '.js-board-handle', + onEnd: (e) => { + gl.issueBoards.onEnd(); - if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) { - const order = this.sortable.toArray(); - const list = Store.findList('id', parseInt(e.item.dataset.id, 10)); + if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) { + const order = this.sortable.toArray(); + const list = Store.findList('id', parseInt(e.item.dataset.id, 10)); - this.$nextTick(() => { - Store.moveList(list, order); - }); - } + this.$nextTick(() => { + Store.moveList(list, order); + }); } - }); + } + }); - this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions); - }, - }); -})(); + this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions); + }, +}); diff --git a/app/assets/javascripts/boards/components/board_delete.js b/app/assets/javascripts/boards/components/board_delete.js index af621cfd57f..8a1b177bba8 100644 --- a/app/assets/javascripts/boards/components/board_delete.js +++ b/app/assets/javascripts/boards/components/board_delete.js @@ -2,22 +2,20 @@ import Vue from 'vue'; -(() => { - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; +window.gl = window.gl || {}; +window.gl.issueBoards = window.gl.issueBoards || {}; - gl.issueBoards.BoardDelete = Vue.extend({ - props: { - list: Object - }, - methods: { - deleteBoard () { - $(this.$el).tooltip('hide'); +gl.issueBoards.BoardDelete = Vue.extend({ + props: { + list: Object + }, + methods: { + deleteBoard () { + $(this.$el).tooltip('hide'); - if (confirm('Are you sure you want to delete this list?')) { - this.list.destroy(); - } + if (confirm('Are you sure you want to delete this list?')) { + this.list.destroy(); } } - }); -})(); + } +}); diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index 3c080008244..004bac09f59 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -8,66 +8,64 @@ import Vue from 'vue'; require('./sidebar/remove_issue'); -(() => { - const Store = gl.issueBoards.BoardsStore; +const Store = gl.issueBoards.BoardsStore; - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; +window.gl = window.gl || {}; +window.gl.issueBoards = window.gl.issueBoards || {}; - gl.issueBoards.BoardSidebar = Vue.extend({ - props: { - currentUser: Object - }, - data() { - return { - detail: Store.detail, - issue: {}, - list: {}, - }; - }, - computed: { - showSidebar () { - return Object.keys(this.issue).length; - } - }, - watch: { - detail: { - handler () { - if (this.issue.id !== this.detail.issue.id) { - $('.js-issue-board-sidebar', this.$el).each((i, el) => { - $(el).data('glDropdown').clearMenu(); - }); - } - - this.issue = this.detail.issue; - this.list = this.detail.list; - }, - deep: true - }, - issue () { - if (this.showSidebar) { - this.$nextTick(() => { - $('.right-sidebar').getNiceScroll(0).doScrollTop(0, 0); - $('.right-sidebar').getNiceScroll().resize(); +gl.issueBoards.BoardSidebar = Vue.extend({ + props: { + currentUser: Object + }, + data() { + return { + detail: Store.detail, + issue: {}, + list: {}, + }; + }, + computed: { + showSidebar () { + return Object.keys(this.issue).length; + } + }, + watch: { + detail: { + handler () { + if (this.issue.id !== this.detail.issue.id) { + $('.js-issue-board-sidebar', this.$el).each((i, el) => { + $(el).data('glDropdown').clearMenu(); }); } - } + + this.issue = this.detail.issue; + this.list = this.detail.list; + }, + deep: true }, - methods: { - closeSidebar () { - this.detail.issue = {}; + issue () { + if (this.showSidebar) { + this.$nextTick(() => { + $('.right-sidebar').getNiceScroll(0).doScrollTop(0, 0); + $('.right-sidebar').getNiceScroll().resize(); + }); } - }, - mounted () { - new IssuableContext(this.currentUser); - new MilestoneSelect(); - new gl.DueDateSelectors(); - new LabelsSelect(); - new Sidebar(); - gl.Subscription.bindAll('.subscription'); - }, - components: { - removeBtn: gl.issueBoards.RemoveIssueBtn, - }, - }); -})(); + } + }, + methods: { + closeSidebar () { + this.detail.issue = {}; + } + }, + mounted () { + new IssuableContext(this.currentUser); + new MilestoneSelect(); + new gl.DueDateSelectors(); + 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 b/app/assets/javascripts/boards/components/issue_card_inner.js index e48d3344a2b..fc154ee7b8b 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js +++ b/app/assets/javascripts/boards/components/issue_card_inner.js @@ -1,141 +1,139 @@ import Vue from 'vue'; import eventHub from '../eventhub'; -(() => { - const Store = gl.issueBoards.BoardsStore; +const Store = gl.issueBoards.BoardsStore; - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; +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, - default: () => ({}), - }, - rootPath: { - type: String, - required: true, - }, - updateFilters: { - type: Boolean, - required: false, - default: false, - }, +gl.issueBoards.IssueCardInner = Vue.extend({ + props: { + issue: { + type: Object, + required: true, }, - computed: { - cardUrl() { - return `${this.issueLinkBase}/${this.issue.id}`; - }, - assigneeUrl() { - return `${this.rootPath}${this.issue.assignee.username}`; - }, - assigneeUrlTitle() { - return `Assigned to ${this.issue.assignee.name}`; - }, - avatarUrlTitle() { - return `Avatar for ${this.issue.assignee.name}`; - }, - issueId() { - return `#${this.issue.id}`; - }, - showLabelFooter() { - return this.issue.labels.find(l => this.showLabel(l)) !== undefined; - }, + issueLinkBase: { + type: String, + required: true, }, - methods: { - showLabel(label) { - if (!this.list) return true; + list: { + type: Object, + required: false, + default: () => ({}), + }, + rootPath: { + type: String, + required: true, + }, + updateFilters: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + cardUrl() { + return `${this.issueLinkBase}/${this.issue.id}`; + }, + assigneeUrl() { + return `${this.rootPath}${this.issue.assignee.username}`; + }, + assigneeUrlTitle() { + return `Assigned to ${this.issue.assignee.name}`; + }, + avatarUrlTitle() { + return `Avatar for ${this.issue.assignee.name}`; + }, + issueId() { + return `#${this.issue.id}`; + }, + showLabelFooter() { + return this.issue.labels.find(l => this.showLabel(l)) !== undefined; + }, + }, + methods: { + showLabel(label) { + if (!this.list) return true; - return !this.list.label || label.id !== this.list.label.id; - }, - filterByLabel(label, e) { - if (!this.updateFilters) return; + return !this.list.label || label.id !== this.list.label.id; + }, + filterByLabel(label, e) { + 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'); + 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) { - filterPath.push(param); - } else { - filterPath.splice(labelIndex, 1); - } + if (labelIndex === -1) { + filterPath.push(param); + } else { + filterPath.splice(labelIndex, 1); + } - gl.issueBoards.BoardsStore.filter.path = filterPath.join('&'); + gl.issueBoards.BoardsStore.filter.path = filterPath.join('&'); - Store.updateFiltersUrl(); + Store.updateFiltersUrl(); - eventHub.$emit('updateTokens'); - }, - labelStyle(label) { - return { - backgroundColor: label.color, - color: label.textColor, - }; - }, + eventHub.$emit('updateTokens'); + }, + labelStyle(label) { + return { + backgroundColor: label.color, + color: label.textColor, + }; }, - template: ` - <div> - <div class="card-header"> - <h4 class="card-title"> - <i - class="fa fa-eye-slash confidential-icon" - v-if="issue.confidential" - aria-hidden="true" - /> - <a - class="js-no-trigger" - :href="cardUrl" - :title="issue.title">{{ issue.title }}</a> - <span - class="card-number" - v-if="issue.id" - > - {{ issueId }} - </span> - </h4> + }, + template: ` + <div> + <div class="card-header"> + <h4 class="card-title"> + <i + class="fa fa-eye-slash confidential-icon" + v-if="issue.confidential" + aria-hidden="true" + /> <a - class="card-assignee has-tooltip js-no-trigger" - :href="assigneeUrl" - :title="assigneeUrlTitle" - v-if="issue.assignee" - data-container="body" + class="js-no-trigger" + :href="cardUrl" + :title="issue.title">{{ issue.title }}</a> + <span + class="card-number" + v-if="issue.id" > - <img - class="avatar avatar-inline s20 js-no-trigger" - :src="issue.assignee.avatar" - width="20" - height="20" - :alt="avatarUrlTitle" - /> - </a> - </div> - <div class="card-footer" v-if="showLabelFooter"> - <button - class="label color-label has-tooltip js-no-trigger" - 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> + {{ issueId }} + </span> + </h4> + <a + class="card-assignee has-tooltip js-no-trigger" + :href="assigneeUrl" + :title="assigneeUrlTitle" + v-if="issue.assignee" + data-container="body" + > + <img + class="avatar avatar-inline s20 js-no-trigger" + :src="issue.assignee.avatar" + width="20" + height="20" + :alt="avatarUrlTitle" + /> + </a> + </div> + <div class="card-footer" v-if="showLabelFooter"> + <button + class="label color-label has-tooltip js-no-trigger" + 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 b/app/assets/javascripts/boards/components/modal/empty_state.js index 823319df6e7..13569df0c20 100644 --- a/app/assets/javascripts/boards/components/modal/empty_state.js +++ b/app/assets/javascripts/boards/components/modal/empty_state.js @@ -1,71 +1,69 @@ import Vue from 'vue'; -(() => { - const ModalStore = gl.issueBoards.ModalStore; +const ModalStore = gl.issueBoards.ModalStore; - gl.issueBoards.ModalEmptyState = Vue.extend({ - mixins: [gl.issueBoards.ModalMixins], - data() { - return ModalStore.store; +gl.issueBoards.ModalEmptyState = Vue.extend({ + mixins: [gl.issueBoards.ModalMixins], + data() { + return ModalStore.store; + }, + props: { + image: { + type: String, + required: true, }, - props: { - image: { - type: String, - required: true, - }, - newIssuePath: { - 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. - `, - }; + }, + computed: { + contents() { + const obj = { + title: 'You haven\'t added any issues to your project yet', + content: ` + An issue can be a bug, a todo or a feature request that needs to be + discussed in a project. Besides, issues are searchable and filterable. + `, + }; - if (this.activeTab === 'selected') { - obj.title = 'You haven\'t selected any issues yet'; - obj.content = ` - Go back to <strong>Open issues</strong> and select some issues - to add to your board. - `; - } + if (this.activeTab === 'selected') { + obj.title = 'You haven\'t selected any issues yet'; + obj.content = ` + Go back to <strong>Open issues</strong> and select some issues + to add to your board. + `; + } - return obj; - }, + 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'"> - Open issues - </button> - </div> + }, + 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'"> + Open issues + </button> </div> </div> - </section> - `, - }); -})(); + </div> + </section> + `, +}); diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js index 887ce373096..ccd270b27da 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js +++ b/app/assets/javascripts/boards/components/modal/footer.js @@ -5,80 +5,78 @@ import Vue from 'vue'; require('./lists_dropdown'); -(() => { - const ModalStore = gl.issueBoards.ModalStore; +const ModalStore = gl.issueBoards.ModalStore; - gl.issueBoards.ModalFooter = Vue.extend({ - mixins: [gl.issueBoards.ModalMixins], - data() { - return { - modal: ModalStore.store, - state: gl.issueBoards.BoardsStore.state, - }; +gl.issueBoards.ModalFooter = Vue.extend({ + mixins: [gl.issueBoards.ModalMixins], + data() { + return { + modal: ModalStore.store, + state: gl.issueBoards.BoardsStore.state, + }; + }, + computed: { + submitDisabled() { + return !ModalStore.selectedCount(); }, - computed: { - submitDisabled() { - return !ModalStore.selectedCount(); - }, - submitText() { - const count = ModalStore.selectedCount(); + submitText() { + const count = ModalStore.selectedCount(); - return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`; - }, + 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); + }, + 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'); + // 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; + list.removeIssue(issue); + list.issuesSize -= 1; }); + }); - this.toggleModal(false); - }, - }, - components: { - 'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown, + // Add the issues on the frontend + selectedIssues.forEach((issue) => { + list.addIssue(issue); + list.issuesSize += 1; + }); + + this.toggleModal(false); }, - 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> + }, + components: { + 'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown, + }, + template: ` + <footer + class="form-actions add-issues-footer"> + <div class="pull-left"> <button - class="btn btn-default pull-right" + class="btn btn-success" type="button" - @click="toggleModal(false)"> - Cancel + :disabled="submitDisabled" + @click="addIssues"> + {{ submitText }} </button> - </footer> - `, - }); -})(); + <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 b/app/assets/javascripts/boards/components/modal/header.js index 116e29cd177..e2b3f9ae7e2 100644 --- a/app/assets/javascripts/boards/components/modal/header.js +++ b/app/assets/javascripts/boards/components/modal/header.js @@ -3,80 +3,78 @@ import modalFilters from './filters'; require('./tabs'); -(() => { - const ModalStore = gl.issueBoards.ModalStore; +const ModalStore = gl.issueBoards.ModalStore; - gl.issueBoards.ModalHeader = Vue.extend({ - mixins: [gl.issueBoards.ModalMixins], - props: { - projectId: { - type: Number, - required: true, - }, - milestonePath: { - type: String, - required: true, - }, - labelPath: { - type: String, - required: true, - }, +gl.issueBoards.ModalHeader = Vue.extend({ + mixins: [gl.issueBoards.ModalMixins], + props: { + projectId: { + type: Number, + required: true, }, - data() { - return ModalStore.store; + milestonePath: { + type: String, + required: true, }, - 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; - }, + labelPath: { + type: String, + required: true, }, - methods: { - toggleAll() { - this.$refs.selectAllBtn.blur(); + }, + data() { + return ModalStore.store; + }, + computed: { + selectAllText() { + if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) { + return 'Select all'; + } - ModalStore.toggleAll(); - }, + return 'Deselect all'; + }, + showSearch() { + return this.activeTab === 'all' && !this.loading && this.issuesCount > 0; }, - components: { - 'modal-tabs': gl.issueBoards.ModalTabs, - modalFilters, + }, + methods: { + toggleAll() { + this.$refs.selectAllBtn.blur(); + + ModalStore.toggleAll(); }, - 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"> - <modal-filters :store="filter" /> + }, + components: { + 'modal-tabs': gl.issueBoards.ModalTabs, + modalFilters, + }, + template: ` + <div> + <header class="add-issues-header form-actions"> + <h2> + Add issues <button type="button" - class="btn btn-success btn-inverted prepend-left-10" - ref="selectAllBtn" - @click="toggleAll"> - {{ selectAllText }} + class="close" + data-dismiss="modal" + aria-label="Close" + @click="toggleModal(false)"> + <span aria-hidden="true">×</span> </button> - </div> + </h2> + </header> + <modal-tabs v-if="!loading && issuesCount > 0"></modal-tabs> + <div + class="add-issues-search append-bottom-10" + v-if="showSearch"> + <modal-filters :store="filter" /> + <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 b/app/assets/javascripts/boards/components/modal/index.js index 91c08cde13a..fb0aac3c0e4 100644 --- a/app/assets/javascripts/boards/components/modal/index.js +++ b/app/assets/javascripts/boards/components/modal/index.js @@ -8,160 +8,158 @@ require('./list'); require('./footer'); require('./empty_state'); -(() => { - const ModalStore = gl.issueBoards.ModalStore; +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, - }, - projectId: { - type: Number, - required: true, - }, - milestonePath: { - type: String, - required: true, - }, - labelPath: { - type: String, - required: true, - }, +gl.issueBoards.IssuesModal = Vue.extend({ + props: { + blankStateImage: { + type: String, + required: true, }, - data() { - return ModalStore.store; + newIssuePath: { + type: String, + required: true, }, - watch: { - page() { - this.loadIssues(); - }, - showAddIssuesModal() { - if (this.showAddIssuesModal && !this.issues.length) { - this.loading = true; + issueLinkBase: { + type: String, + required: true, + }, + rootPath: { + type: String, + required: true, + }, + projectId: { + type: Number, + required: true, + }, + milestonePath: { + type: String, + required: true, + }, + labelPath: { + type: String, + required: true, + }, + }, + data() { + return ModalStore.store; + }, + watch: { + page() { + this.loadIssues(); + }, + 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; + } + }, + filter: { + handler() { + if (this.$el.tagName) { + this.page = 1; + this.filterLoading = true; - this.loadIssues() + this.loadIssues(true) .then(() => { - this.loading = false; + this.filterLoading = false; }); - } else if (!this.showAddIssuesModal) { - this.issues = []; - this.selectedIssues = []; - this.issuesCount = false; } }, - filter: { - handler() { - if (this.$el.tagName) { - this.page = 1; - this.filterLoading = true; - - this.loadIssues(true) - .then(() => { - this.filterLoading = false; - }); - } - }, - deep: true, - }, + deep: true, }, - methods: { - loadIssues(clearIssues = false) { - if (!this.showAddIssuesModal) return false; - - return gl.boardService.getBacklog(queryData(this.filter.path, { - page: this.page, - per: this.perPage, - })).then((res) => { - const data = res.json(); - - if (clearIssues) { - this.issues = []; - } + }, + methods: { + loadIssues(clearIssues = false) { + if (!this.showAddIssuesModal) return false; - data.issues.forEach((issueObj) => { - const issue = new ListIssue(issueObj); - const foundSelectedIssue = ModalStore.findSelectedIssue(issue); - issue.selected = !!foundSelectedIssue; + return gl.boardService.getBacklog(queryData(this.filter.path, { + page: this.page, + per: this.perPage, + })).then((res) => { + const data = res.json(); - this.issues.push(issue); - }); + if (clearIssues) { + this.issues = []; + } - this.loadingNewPage = false; + data.issues.forEach((issueObj) => { + const issue = new ListIssue(issueObj); + const foundSelectedIssue = ModalStore.findSelectedIssue(issue); + issue.selected = !!foundSelectedIssue; - if (!this.issuesCount) { - this.issuesCount = data.size; - } + this.issues.push(issue); }); - }, - }, - computed: { - showList() { - if (this.activeTab === 'selected') { - return this.selectedIssues.length > 0; - } - return this.issuesCount > 0; - }, - showEmptyState() { - if (!this.loading && this.issuesCount === 0) { - return true; - } + this.loadingNewPage = false; - return this.activeTab === 'selected' && this.selectedIssues.length === 0; - }, + if (!this.issuesCount) { + this.issuesCount = data.size; + } + }); }, - created() { - this.page = 1; + }, + computed: { + showList() { + if (this.activeTab === 'selected') { + return this.selectedIssues.length > 0; + } + + return this.issuesCount > 0; }, - components: { - 'modal-header': gl.issueBoards.ModalHeader, - 'modal-list': gl.issueBoards.ModalList, - 'modal-footer': gl.issueBoards.ModalFooter, - 'empty-state': gl.issueBoards.ModalEmptyState, + showEmptyState() { + if (!this.loading && this.issuesCount === 0) { + return true; + } + + return this.activeTab === 'selected' && this.selectedIssues.length === 0; }, - template: ` - <div - class="add-issues-modal" - v-if="showAddIssuesModal"> - <div class="add-issues-container"> - <modal-header - :project-id="projectId" - :milestone-path="milestonePath" - :label-path="labelPath"> - </modal-header> - <modal-list - :image="blankStateImage" - :issue-link-base="issueLinkBase" - :root-path="rootPath" - v-if="!loading && showList && !filterLoading"></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 || filterLoading"> - <div class="add-issues-list-loading"> - <i class="fa fa-spinner fa-spin"></i> - </div> - </section> - <modal-footer></modal-footer> - </div> + }, + created() { + this.page = 1; + }, + components: { + 'modal-header': gl.issueBoards.ModalHeader, + 'modal-list': gl.issueBoards.ModalList, + 'modal-footer': gl.issueBoards.ModalFooter, + 'empty-state': gl.issueBoards.ModalEmptyState, + }, + template: ` + <div + class="add-issues-modal" + v-if="showAddIssuesModal"> + <div class="add-issues-container"> + <modal-header + :project-id="projectId" + :milestone-path="milestonePath" + :label-path="labelPath"> + </modal-header> + <modal-list + :image="blankStateImage" + :issue-link-base="issueLinkBase" + :root-path="rootPath" + v-if="!loading && showList && !filterLoading"></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 || filterLoading"> + <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 b/app/assets/javascripts/boards/components/modal/list.js index aba56d4aa31..363269c0d5d 100644 --- a/app/assets/javascripts/boards/components/modal/list.js +++ b/app/assets/javascripts/boards/components/modal/list.js @@ -3,159 +3,157 @@ import Vue from 'vue'; -(() => { - const ModalStore = gl.issueBoards.ModalStore; +const ModalStore = gl.issueBoards.ModalStore; - gl.issueBoards.ModalList = Vue.extend({ - props: { - issueLinkBase: { - type: String, - required: true, - }, - rootPath: { - type: String, - required: true, - }, - image: { - type: String, - required: true, - }, +gl.issueBoards.ModalList = Vue.extend({ + props: { + issueLinkBase: { + type: String, + required: true, }, - data() { - return ModalStore.store; + rootPath: { + type: String, + required: true, }, - watch: { - activeTab() { - if (this.activeTab === 'all') { - ModalStore.purgeUnselectedIssues(); - } - }, + image: { + type: String, + required: true, }, - 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); - }); + }, + data() { + return ModalStore.store; + }, + watch: { + activeTab() { + if (this.activeTab === 'all') { + ModalStore.purgeUnselectedIssues(); + } + }, + }, + computed: { + loopIssues() { + if (this.activeTab === 'all') { + return this.issues; + } - return groups; - }, + return this.selectedIssues; }, - methods: { - scrollHandler() { - const currentPage = Math.floor(this.issues.length / this.perPage); + groupedIssues() { + const groups = []; + this.loopIssues.forEach((issue, i) => { + const index = i % this.columns; - if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage - && currentPage === this.page) { - this.loadingNewPage = true; - this.page += 1; + if (!groups[index]) { + groups.push([]); } - }, - 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(); + groups[index].push(issue); + }); - if (breakpoint === 'lg' || breakpoint === 'md') { - this.columns = 3; - } else if (breakpoint === 'sm') { - this.columns = 2; - } else { - this.columns = 1; - } - }, + return groups; }, - mounted() { - this.scrollHandlerWrapper = this.scrollHandler.bind(this); - this.setColumnCountWrapper = this.setColumnCount.bind(this); - this.setColumnCount(); + }, + methods: { + scrollHandler() { + const currentPage = Math.floor(this.issues.length / this.perPage); - this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper); - window.addEventListener('resize', this.setColumnCountWrapper); + 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; }, - beforeDestroy() { - this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper); - window.removeEventListener('resize', this.setColumnCountWrapper); + scrollHeight() { + return this.$refs.list.scrollHeight; }, - components: { - 'issue-card-inner': gl.issueBoards.IssueCardInner, + scrollTop() { + return this.$refs.list.scrollTop + this.listHeight(); }, - template: ` - <section - class="add-issues-list add-issues-list-columns" - ref="list"> + 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 + class="empty-state add-issues-empty-state-filter text-center" + v-if="issuesCount > 0 && issues.length === 0"> <div - class="empty-state add-issues-empty-state-filter text-center" - v-if="issuesCount > 0 && issues.length === 0"> - <div - class="svg-content" - v-html="image"> - </div> - <div class="text-content"> - <h4> - There are no issues to show. - </h4> - </div> + class="svg-content" + v-html="image"> + </div> + <div class="text-content"> + <h4> + There are no issues to show. + </h4> </div> + </div> + <div + v-for="group in groupedIssues" + class="add-issues-list-column"> <div - v-for="group in groupedIssues" - class="add-issues-list-column"> + v-for="issue in group" + v-if="showIssue(issue)" + class="card-parent"> <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> + 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> - </section> - `, - }); -})(); + </div> + </section> + `, +}); diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js b/app/assets/javascripts/boards/components/modal/lists_dropdown.js index 9e9ed46ab8d..8cd15df90fa 100644 --- a/app/assets/javascripts/boards/components/modal/lists_dropdown.js +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.js @@ -1,57 +1,55 @@ import Vue from 'vue'; -(() => { - const ModalStore = gl.issueBoards.ModalStore; +const ModalStore = gl.issueBoards.ModalStore; - gl.issueBoards.ModalFooterListsDropdown = Vue.extend({ - data() { - return { - modal: ModalStore.store, - state: gl.issueBoards.BoardsStore.state, - }; +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]; }, - 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> + }, + 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 b/app/assets/javascripts/boards/components/modal/tabs.js index 23cb1b13d11..3e5d08e3d75 100644 --- a/app/assets/javascripts/boards/components/modal/tabs.js +++ b/app/assets/javascripts/boards/components/modal/tabs.js @@ -1,48 +1,46 @@ import Vue from 'vue'; -(() => { - const ModalStore = gl.issueBoards.ModalStore; +const ModalStore = gl.issueBoards.ModalStore; - gl.issueBoards.ModalTabs = Vue.extend({ - mixins: [gl.issueBoards.ModalMixins], - data() { - return ModalStore.store; +gl.issueBoards.ModalTabs = Vue.extend({ + mixins: [gl.issueBoards.ModalMixins], + data() { + return ModalStore.store; + }, + computed: { + selectedCount() { + return ModalStore.selectedCount(); }, - computed: { - selectedCount() { - return ModalStore.selectedCount(); - }, - }, - destroyed() { - this.activeTab = 'all'; - }, - template: ` - <div class="top-area prepend-top-10 append-bottom-10"> - <ul class="nav-links issues-state-filters"> - <li :class="{ 'active': activeTab == 'all' }"> - <a - href="#" - role="button" - @click.prevent="changeTab('all')"> - Open issues - <span class="badge"> - {{ issuesCount }} - </span> - </a> - </li> - <li :class="{ 'active': activeTab == 'selected' }"> - <a - href="#" - role="button" - @click.prevent="changeTab('selected')"> - Selected issues - <span class="badge"> - {{ selectedCount }} - </span> - </a> - </li> - </ul> - </div> - `, - }); -})(); + }, + destroyed() { + this.activeTab = 'all'; + }, + template: ` + <div class="top-area prepend-top-10 append-bottom-10"> + <ul class="nav-links issues-state-filters"> + <li :class="{ 'active': activeTab == 'all' }"> + <a + href="#" + role="button" + @click.prevent="changeTab('all')"> + Open issues + <span class="badge"> + {{ issuesCount }} + </span> + </a> + </li> + <li :class="{ 'active': activeTab == 'selected' }"> + <a + href="#" + role="button" + @click.prevent="changeTab('selected')"> + Selected issues + <span class="badge"> + {{ selectedCount }} + </span> + </a> + </li> + </ul> + </div> + `, +}); diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js index 556826a9148..22f20305624 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js @@ -1,76 +1,74 @@ /* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var */ -(() => { - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; +window.gl = window.gl || {}; +window.gl.issueBoards = window.gl.issueBoards || {}; - const Store = gl.issueBoards.BoardsStore; +const Store = gl.issueBoards.BoardsStore; - $(document).off('created.label').on('created.label', (e, label) => { - Store.new({ +$(document).off('created.label').on('created.label', (e, label) => { + Store.new({ + title: label.title, + position: Store.state.lists.length - 2, + list_type: 'label', + label: { + id: label.id, title: label.title, - position: Store.state.lists.length - 2, - list_type: 'label', - label: { - id: label.id, - title: label.title, - color: label.color - } - }); + color: label.color + } }); +}); - gl.issueBoards.newListDropdownInit = () => { - $('.js-new-board-list').each(function () { - const $this = $(this); - new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path')); +gl.issueBoards.newListDropdownInit = () => { + $('.js-new-board-list').each(function () { + const $this = $(this); + new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path')); - $this.glDropdown({ - data(term, callback) { - $.get($this.attr('data-labels')) - .then((resp) => { - callback(resp); - }); - }, - renderRow (label) { - const active = Store.findList('title', label.title); - const $li = $('<li />'); - const $a = $('<a />', { - class: (active ? `is-active js-board-list-${active.id}` : ''), - text: label.title, - href: '#' - }); - const $labelColor = $('<span />', { - class: 'dropdown-label-box', - style: `background-color: ${label.color}` + $this.glDropdown({ + data(term, callback) { + $.get($this.attr('data-labels')) + .then((resp) => { + callback(resp); }); + }, + renderRow (label) { + const active = Store.findList('title', label.title); + const $li = $('<li />'); + const $a = $('<a />', { + class: (active ? `is-active js-board-list-${active.id}` : ''), + text: label.title, + href: '#' + }); + const $labelColor = $('<span />', { + class: 'dropdown-label-box', + style: `background-color: ${label.color}` + }); - return $li.append($a.prepend($labelColor)); - }, - search: { - fields: ['title'] - }, - filterable: true, - selectable: true, - multiSelect: true, - clicked (label, $el, e) { - e.preventDefault(); + return $li.append($a.prepend($labelColor)); + }, + search: { + fields: ['title'] + }, + filterable: true, + selectable: true, + multiSelect: true, + clicked (label, $el, e) { + e.preventDefault(); - if (!Store.findList('title', label.title)) { - Store.new({ + if (!Store.findList('title', label.title)) { + Store.new({ + title: label.title, + position: Store.state.lists.length - 2, + list_type: 'label', + label: { + id: label.id, title: label.title, - position: Store.state.lists.length - 2, - list_type: 'label', - label: { - id: label.id, - 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'); } - }); + } }); - }; -})(); + }); +}; diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js index 772ea4c5565..5597f128b80 100644 --- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js @@ -3,59 +3,57 @@ import Vue from 'vue'; -(() => { - 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, - }, +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, }, - 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); - }); - }); + 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'); - // Remove from the frontend store lists.forEach((list) => { - list.removeIssue(issue); + list.addIssue(issue); }); + }); + + // Remove from the frontend store + lists.forEach((list) => { + list.removeIssue(issue); + }); - Store.detail.issue = {}; - }, + Store.detail.issue = {}; }, - template: ` - <div - class="block list" - v-if="list.type !== 'closed'"> - <button - class="btn btn-default btn-block" - type="button" - @click="removeIssue"> - Remove from board - </button> - </div> - `, - }); -})(); + }, + template: ` + <div + class="block list" + v-if="list.type !== 'closed'"> + <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 b/app/assets/javascripts/boards/mixins/modal_mixins.js index d378b7d4baf..2b0a1aaa89f 100644 --- a/app/assets/javascripts/boards/mixins/modal_mixins.js +++ b/app/assets/javascripts/boards/mixins/modal_mixins.js @@ -1,14 +1,12 @@ -(() => { - const ModalStore = gl.issueBoards.ModalStore; +const ModalStore = gl.issueBoards.ModalStore; - gl.issueBoards.ModalMixins = { - methods: { - toggleModal(toggle) { - ModalStore.store.showAddIssuesModal = toggle; - }, - changeTab(tab) { - ModalStore.store.activeTab = tab; - }, +gl.issueBoards.ModalMixins = { + methods: { + toggleModal(toggle) { + ModalStore.store.showAddIssuesModal = toggle; }, - }; -})(); + changeTab(tab) { + ModalStore.store.activeTab = tab; + }, + }, +}; diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js b/app/assets/javascripts/boards/mixins/sortable_default_options.js index b6c6d17274f..38a0eb12f92 100644 --- a/app/assets/javascripts/boards/mixins/sortable_default_options.js +++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js @@ -1,39 +1,37 @@ /* eslint-disable no-unused-vars, no-mixed-operators, comma-dangle */ /* global DocumentTouch */ -((w) => { - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; +window.gl = window.gl || {}; +window.gl.issueBoards = window.gl.issueBoards || {}; - gl.issueBoards.onStart = () => { - $('.has-tooltip').tooltip('hide') - .tooltip('disable'); - document.body.classList.add('is-dragging'); - }; - - gl.issueBoards.onEnd = () => { - $('.has-tooltip').tooltip('enable'); - document.body.classList.remove('is-dragging'); - }; +gl.issueBoards.onStart = () => { + $('.has-tooltip').tooltip('hide') + .tooltip('disable'); + document.body.classList.add('is-dragging'); +}; - gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch; +gl.issueBoards.onEnd = () => { + $('.has-tooltip').tooltip('enable'); + document.body.classList.remove('is-dragging'); +}; - gl.issueBoards.getBoardSortableDefaultOptions = (obj) => { - const defaultSortOptions = { - animation: 200, - forceFallback: true, - fallbackClass: 'is-dragging', - fallbackOnBody: true, - ghostClass: 'is-ghost', - filter: '.board-delete, .btn', - delay: gl.issueBoards.touchEnabled ? 100 : 0, - scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100, - scrollSpeed: 20, - onStart: gl.issueBoards.onStart, - onEnd: gl.issueBoards.onEnd - }; +gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch; - Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; }); - return defaultSortOptions; +gl.issueBoards.getBoardSortableDefaultOptions = (obj) => { + const defaultSortOptions = { + animation: 200, + forceFallback: true, + fallbackClass: 'is-dragging', + fallbackOnBody: true, + ghostClass: 'is-ghost', + filter: '.board-delete, .btn', + delay: gl.issueBoards.touchEnabled ? 100 : 0, + scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100, + scrollSpeed: 20, + onStart: gl.issueBoards.onStart, + onEnd: gl.issueBoards.onEnd }; -})(window); + + Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; }); + return defaultSortOptions; +}; diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index bcda70d0638..66384d9c038 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -3,125 +3,123 @@ import Cookies from 'js-cookie'; -(() => { - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; +window.gl = window.gl || {}; +window.gl.issueBoards = window.gl.issueBoards || {}; - gl.issueBoards.BoardsStore = { - disabled: false, - filter: { - path: '', - }, - state: {}, - detail: { - issue: {} - }, - moving: { - issue: {}, - list: {} - }, - create () { - this.state.lists = []; - this.filter.path = gl.utils.getUrlParamsArray().join('&'); - }, - addList (listObj) { - const list = new List(listObj); - this.state.lists.push(list); +gl.issueBoards.BoardsStore = { + disabled: false, + filter: { + path: '', + }, + state: {}, + detail: { + issue: {} + }, + moving: { + issue: {}, + list: {} + }, + create () { + this.state.lists = []; + this.filter.path = gl.utils.getUrlParamsArray().join('&'); + }, + addList (listObj) { + const list = new List(listObj); + this.state.lists.push(list); - return list; - }, - new (listObj) { - const list = this.addList(listObj); + return list; + }, + new (listObj) { + const list = this.addList(listObj); - list - .save() - .then(() => { - this.state.lists = _.sortBy(this.state.lists, 'position'); - }); - this.removeBlankState(); - }, - updateNewListDropdown (listId) { - $(`.js-board-list-${listId}`).removeClass('is-active'); - }, - shouldAddBlankState () { - // Decide whether to add the blank state - return !(this.state.lists.filter(list => list.type !== 'closed')[0]); - }, - addBlankState () { - if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return; - - this.addList({ - id: 'blank', - list_type: 'blank', - title: 'Welcome to your Issue Board!', - position: 0 + list + .save() + .then(() => { + this.state.lists = _.sortBy(this.state.lists, 'position'); }); + this.removeBlankState(); + }, + updateNewListDropdown (listId) { + $(`.js-board-list-${listId}`).removeClass('is-active'); + }, + shouldAddBlankState () { + // Decide whether to add the blank state + return !(this.state.lists.filter(list => list.type !== 'closed')[0]); + }, + addBlankState () { + if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return; - this.state.lists = _.sortBy(this.state.lists, 'position'); - }, - removeBlankState () { - this.removeList('blank'); - - Cookies.set('issue_board_welcome_hidden', 'true', { - expires: 365 * 10, - path: '' - }); - }, - welcomeIsHidden () { - return Cookies.get('issue_board_welcome_hidden') === 'true'; - }, - removeList (id, type = 'blank') { - const list = this.findList('id', id, type); + this.addList({ + id: 'blank', + list_type: 'blank', + title: 'Welcome to your Issue Board!', + position: 0 + }); - if (!list) return; + this.state.lists = _.sortBy(this.state.lists, 'position'); + }, + removeBlankState () { + this.removeList('blank'); - this.state.lists = this.state.lists.filter(list => list.id !== id); - }, - moveList (listFrom, orderLists) { - orderLists.forEach((id, i) => { - const list = this.findList('id', parseInt(id, 10)); + Cookies.set('issue_board_welcome_hidden', 'true', { + expires: 365 * 10, + path: '' + }); + }, + welcomeIsHidden () { + return Cookies.get('issue_board_welcome_hidden') === 'true'; + }, + removeList (id, type = 'blank') { + const list = this.findList('id', id, type); - list.position = i; - }); - listFrom.update(); - }, - moveIssueToList (listFrom, listTo, issue, newIndex) { - const issueTo = listTo.findIssue(issue.id); - const issueLists = issue.getLists(); - const listLabels = issueLists.map(listIssue => listIssue.label); + if (!list) return; - if (!issueTo) { - // Add to new lists issues if it doesn't already exist - listTo.addIssue(issue, listFrom, newIndex); - } else { - listTo.updateIssueLabel(issue, listFrom); - issueTo.removeLabel(listFrom.label); - } + this.state.lists = this.state.lists.filter(list => list.id !== id); + }, + moveList (listFrom, orderLists) { + orderLists.forEach((id, i) => { + const list = this.findList('id', parseInt(id, 10)); - if (listTo.type === 'closed') { - issueLists.forEach((list) => { - list.removeIssue(issue); - }); - issue.removeLabels(listLabels); - } else { - listFrom.removeIssue(issue); - } - }, - moveIssueInList (list, issue, oldIndex, newIndex, idArray) { - const beforeId = parseInt(idArray[newIndex - 1], 10) || null; - const afterId = parseInt(idArray[newIndex + 1], 10) || null; + list.position = i; + }); + listFrom.update(); + }, + moveIssueToList (listFrom, listTo, issue, newIndex) { + const issueTo = listTo.findIssue(issue.id); + const issueLists = issue.getLists(); + const listLabels = issueLists.map(listIssue => listIssue.label); - list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId); - }, - findList (key, val, type = 'label') { - return this.state.lists.filter((list) => { - const byType = type ? list['type'] === type : true; + if (!issueTo) { + // Add to new lists issues if it doesn't already exist + listTo.addIssue(issue, listFrom, newIndex); + } else { + listTo.updateIssueLabel(issue, listFrom); + issueTo.removeLabel(listFrom.label); + } - return list[key] === val && byType; - })[0]; - }, - updateFiltersUrl () { - history.pushState(null, null, `?${this.filter.path}`); + if (listTo.type === 'closed') { + issueLists.forEach((list) => { + list.removeIssue(issue); + }); + issue.removeLabels(listLabels); + } else { + listFrom.removeIssue(issue); } - }; -})(); + }, + moveIssueInList (list, issue, oldIndex, newIndex, idArray) { + const beforeId = parseInt(idArray[newIndex - 1], 10) || null; + const afterId = parseInt(idArray[newIndex + 1], 10) || null; + + list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId); + }, + findList (key, val, type = 'label') { + return this.state.lists.filter((list) => { + const byType = type ? list['type'] === type : true; + + return list[key] === val && byType; + })[0]; + }, + updateFiltersUrl () { + 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 9b009483a3c..4fdc925c825 100644 --- a/app/assets/javascripts/boards/stores/modal_store.js +++ b/app/assets/javascripts/boards/stores/modal_store.js @@ -1,100 +1,98 @@ -(() => { - 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, - filterLoading: false, - page: 1, - perPage: 50, - filter: { - path: '', - }, - }; - } +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, + filterLoading: false, + page: 1, + perPage: 50, + filter: { + path: '', + }, + }; + } - selectedCount() { - return this.getSelectedIssues().length; - } + selectedCount() { + return this.getSelectedIssues().length; + } - toggleIssue(issueObj) { - const issue = issueObj; - const selected = issue.selected; + toggleIssue(issueObj) { + const issue = issueObj; + const selected = issue.selected; - issue.selected = !selected; + issue.selected = !selected; - if (!selected) { - this.addSelectedIssue(issue); - } else { - this.removeSelectedIssue(issue); - } + if (!selected) { + this.addSelectedIssue(issue); + } else { + this.removeSelectedIssue(issue); } + } - toggleAll() { - const select = this.selectedCount() !== this.store.issues.length; + toggleAll() { + const select = this.selectedCount() !== this.store.issues.length; - this.store.issues.forEach((issue) => { - const issueUpdate = issue; + this.store.issues.forEach((issue) => { + const issueUpdate = issue; - if (issueUpdate.selected !== select) { - issueUpdate.selected = select; + if (issueUpdate.selected !== select) { + issueUpdate.selected = select; - if (select) { - this.addSelectedIssue(issue); - } else { - this.removeSelectedIssue(issue); - } + if (select) { + this.addSelectedIssue(issue); + } else { + this.removeSelectedIssue(issue); } - }); - } + } + }); + } - getSelectedIssues() { - return this.store.selectedIssues.filter(issue => issue.selected); - } + getSelectedIssues() { + return this.store.selectedIssues.filter(issue => issue.selected); + } - addSelectedIssue(issue) { - const index = this.selectedIssueIndex(issue); + addSelectedIssue(issue) { + const index = this.selectedIssueIndex(issue); - if (index === -1) { - this.store.selectedIssues.push(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); - } + 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); - } - }); - } + purgeUnselectedIssues() { + this.store.selectedIssues.forEach((issue) => { + if (!issue.selected) { + this.removeSelectedIssue(issue, true); + } + }); + } - selectedIssueIndex(issue) { - return this.store.selectedIssues.indexOf(issue); - } + selectedIssueIndex(issue) { + return this.store.selectedIssues.indexOf(issue); + } - findSelectedIssue(issue) { - return this.store.selectedIssues - .filter(filteredIssue => filteredIssue.id === issue.id)[0]; - } + findSelectedIssue(issue) { + return this.store.selectedIssues + .filter(filteredIssue => filteredIssue.id === issue.id)[0]; } +} - gl.issueBoards.ModalStore = new ModalStore(); -})(); +gl.issueBoards.ModalStore = new ModalStore(); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js index 3f419a96ff9..80bd2df6f42 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js @@ -2,46 +2,45 @@ import Vue from 'vue'; -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; +const global = window.gl || (window.gl = {}); +global.cycleAnalytics = global.cycleAnalytics || {}; - global.cycleAnalytics.StageCodeComponent = Vue.extend({ - props: { - items: Array, - stage: Object, - }, - template: ` - <div> - <div class="events-description"> - {{ stage.description }} - <limit-warning :count="items.length" /> - </div> - <ul class="stage-event-list"> - <li v-for="mergeRequest in items" class="stage-event-item"> - <div class="item-details"> - <img class="avatar" :src="mergeRequest.author.avatarUrl"> - <h5 class="item-title merge-merquest-title"> - <a :href="mergeRequest.url"> - {{ mergeRequest.title }} - </a> - </h5> - <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> - · - <span> - Opened - <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a> - </span> - <span> - by - <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a> - </span> - </div> - <div class="item-time"> - <total-time :time="mergeRequest.totalTime"></total-time> - </div> - </li> - </ul> +global.cycleAnalytics.StageCodeComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + template: ` + <div> + <div class="events-description"> + {{ stage.description }} + <limit-warning :count="items.length" /> </div> - `, - }); -})(window.gl || (window.gl = {})); + <ul class="stage-event-list"> + <li v-for="mergeRequest in items" class="stage-event-item"> + <div class="item-details"> + <img class="avatar" :src="mergeRequest.author.avatarUrl"> + <h5 class="item-title merge-merquest-title"> + <a :href="mergeRequest.url"> + {{ mergeRequest.title }} + </a> + </h5> + <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> + · + <span> + Opened + <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a> + </span> + <span> + by + <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a> + </span> + </div> + <div class="item-time"> + <total-time :time="mergeRequest.totalTime"></total-time> + </div> + </li> + </ul> + </div> + `, +}); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js index 7ffa38edd9e..20a43798fbe 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js @@ -2,48 +2,47 @@ import Vue from 'vue'; -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; +const global = window.gl || (window.gl = {}); +global.cycleAnalytics = global.cycleAnalytics || {}; - global.cycleAnalytics.StageIssueComponent = Vue.extend({ - props: { - items: Array, - stage: Object, - }, - template: ` - <div> - <div class="events-description"> - {{ stage.description }} - <limit-warning :count="items.length" /> - </div> - <ul class="stage-event-list"> - <li v-for="issue in items" class="stage-event-item"> - <div class="item-details"> - <img class="avatar" :src="issue.author.avatarUrl"> - <h5 class="item-title issue-title"> - <a class="issue-title" :href="issue.url"> - {{ issue.title }} - </a> - </h5> - <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a> - · - <span> - Opened - <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a> - </span> - <span> - by - <a :href="issue.author.webUrl" class="issue-author-link"> - {{ issue.author.name }} - </a> - </span> - </div> - <div class="item-time"> - <total-time :time="issue.totalTime"></total-time> - </div> - </li> - </ul> +global.cycleAnalytics.StageIssueComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + template: ` + <div> + <div class="events-description"> + {{ stage.description }} + <limit-warning :count="items.length" /> </div> - `, - }); -})(window.gl || (window.gl = {})); + <ul class="stage-event-list"> + <li v-for="issue in items" class="stage-event-item"> + <div class="item-details"> + <img class="avatar" :src="issue.author.avatarUrl"> + <h5 class="item-title issue-title"> + <a class="issue-title" :href="issue.url"> + {{ issue.title }} + </a> + </h5> + <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a> + · + <span> + Opened + <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a> + </span> + <span> + by + <a :href="issue.author.webUrl" class="issue-author-link"> + {{ issue.author.name }} + </a> + </span> + </div> + <div class="item-time"> + <total-time :time="issue.totalTime"></total-time> + </div> + </li> + </ul> + </div> + `, +}); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js index d736c8b0c28..f33cac3da82 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js @@ -2,50 +2,49 @@ import Vue from 'vue'; import iconCommit from '../svg/icon_commit.svg'; -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; +const global = window.gl || (window.gl = {}); +global.cycleAnalytics = global.cycleAnalytics || {}; - global.cycleAnalytics.StagePlanComponent = Vue.extend({ - props: { - items: Array, - stage: Object, - }, +global.cycleAnalytics.StagePlanComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, - data() { - return { iconCommit }; - }, + data() { + return { iconCommit }; + }, - template: ` - <div> - <div class="events-description"> - {{ stage.description }} - <limit-warning :count="items.length" /> - </div> - <ul class="stage-event-list"> - <li v-for="commit in items" class="stage-event-item"> - <div class="item-details item-conmmit-component"> - <img class="avatar" :src="commit.author.avatarUrl"> - <h5 class="item-title commit-title"> - <a :href="commit.commitUrl"> - {{ commit.title }} - </a> - </h5> - <span> - First - <span class="commit-icon">${iconCommit}</span> - <a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a> - pushed by - <a :href="commit.author.webUrl" class="commit-author-link"> - {{ commit.author.name }} - </a> - </span> - </div> - <div class="item-time"> - <total-time :time="commit.totalTime"></total-time> - </div> - </li> - </ul> + template: ` + <div> + <div class="events-description"> + {{ stage.description }} + <limit-warning :count="items.length" /> </div> - `, - }); -})(window.gl || (window.gl = {})); + <ul class="stage-event-list"> + <li v-for="commit in items" class="stage-event-item"> + <div class="item-details item-conmmit-component"> + <img class="avatar" :src="commit.author.avatarUrl"> + <h5 class="item-title commit-title"> + <a :href="commit.commitUrl"> + {{ commit.title }} + </a> + </h5> + <span> + First + <span class="commit-icon">${iconCommit}</span> + <a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a> + pushed by + <a :href="commit.author.webUrl" class="commit-author-link"> + {{ commit.author.name }} + </a> + </span> + </div> + <div class="item-time"> + <total-time :time="commit.totalTime"></total-time> + </div> + </li> + </ul> + </div> + `, +}); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js index 698a79ca68c..657f5385374 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js @@ -2,48 +2,47 @@ import Vue from 'vue'; -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; +const global = window.gl || (window.gl = {}); +global.cycleAnalytics = global.cycleAnalytics || {}; - global.cycleAnalytics.StageProductionComponent = Vue.extend({ - props: { - items: Array, - stage: Object, - }, - template: ` - <div> - <div class="events-description"> - {{ stage.description }} - <limit-warning :count="items.length" /> - </div> - <ul class="stage-event-list"> - <li v-for="issue in items" class="stage-event-item"> - <div class="item-details"> - <img class="avatar" :src="issue.author.avatarUrl"> - <h5 class="item-title issue-title"> - <a class="issue-title" :href="issue.url"> - {{ issue.title }} - </a> - </h5> - <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a> - · - <span> - Opened - <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a> - </span> - <span> - by - <a :href="issue.author.webUrl" class="issue-author-link"> - {{ issue.author.name }} - </a> - </span> - </div> - <div class="item-time"> - <total-time :time="issue.totalTime"></total-time> - </div> - </li> - </ul> +global.cycleAnalytics.StageProductionComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + template: ` + <div> + <div class="events-description"> + {{ stage.description }} + <limit-warning :count="items.length" /> </div> - `, - }); -})(window.gl || (window.gl = {})); + <ul class="stage-event-list"> + <li v-for="issue in items" class="stage-event-item"> + <div class="item-details"> + <img class="avatar" :src="issue.author.avatarUrl"> + <h5 class="item-title issue-title"> + <a class="issue-title" :href="issue.url"> + {{ issue.title }} + </a> + </h5> + <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a> + · + <span> + Opened + <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a> + </span> + <span> + by + <a :href="issue.author.webUrl" class="issue-author-link"> + {{ issue.author.name }} + </a> + </span> + </div> + <div class="item-time"> + <total-time :time="issue.totalTime"></total-time> + </div> + </li> + </ul> + </div> + `, +}); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js index e63c41f2a57..8a801300647 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js @@ -2,58 +2,57 @@ import Vue from 'vue'; -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; +const global = window.gl || (window.gl = {}); +global.cycleAnalytics = global.cycleAnalytics || {}; - global.cycleAnalytics.StageReviewComponent = Vue.extend({ - props: { - items: Array, - stage: Object, - }, - template: ` - <div> - <div class="events-description"> - {{ stage.description }} - <limit-warning :count="items.length" /> - </div> - <ul class="stage-event-list"> - <li v-for="mergeRequest in items" class="stage-event-item"> - <div class="item-details"> - <img class="avatar" :src="mergeRequest.author.avatarUrl"> - <h5 class="item-title merge-merquest-title"> - <a :href="mergeRequest.url"> - {{ mergeRequest.title }} - </a> - </h5> - <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> - · - <span> - Opened - <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a> +global.cycleAnalytics.StageReviewComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + template: ` + <div> + <div class="events-description"> + {{ stage.description }} + <limit-warning :count="items.length" /> + </div> + <ul class="stage-event-list"> + <li v-for="mergeRequest in items" class="stage-event-item"> + <div class="item-details"> + <img class="avatar" :src="mergeRequest.author.avatarUrl"> + <h5 class="item-title merge-merquest-title"> + <a :href="mergeRequest.url"> + {{ mergeRequest.title }} + </a> + </h5> + <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> + · + <span> + Opened + <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a> + </span> + <span> + by + <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a> + </span> + <template v-if="mergeRequest.state === 'closed'"> + <span class="merge-request-state"> + <i class="fa fa-ban"></i> + {{ mergeRequest.state.toUpperCase() }} </span> - <span> - by - <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a> + </template> + <template v-else> + <span class="merge-request-branch" v-if="mergeRequest.branch"> + <i class= "fa fa-code-fork"></i> + <a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a> </span> - <template v-if="mergeRequest.state === 'closed'"> - <span class="merge-request-state"> - <i class="fa fa-ban"></i> - {{ mergeRequest.state.toUpperCase() }} - </span> - </template> - <template v-else> - <span class="merge-request-branch" v-if="mergeRequest.branch"> - <i class= "fa fa-code-fork"></i> - <a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a> - </span> - </template> - </div> - <div class="item-time"> - <total-time :time="mergeRequest.totalTime"></total-time> - </div> - </li> - </ul> - </div> - `, - }); -})(window.gl || (window.gl = {})); + </template> + </div> + <div class="item-time"> + <total-time :time="mergeRequest.totalTime"></total-time> + </div> + </li> + </ul> + </div> + `, +}); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js index d51f7134e25..4a286379588 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js @@ -2,48 +2,47 @@ import Vue from 'vue'; import iconBranch from '../svg/icon_branch.svg'; -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; +const global = window.gl || (window.gl = {}); +global.cycleAnalytics = global.cycleAnalytics || {}; - global.cycleAnalytics.StageStagingComponent = Vue.extend({ - props: { - items: Array, - stage: Object, - }, - data() { - return { iconBranch }; - }, - template: ` - <div> - <div class="events-description"> - {{ stage.description }} - <limit-warning :count="items.length" /> - </div> - <ul class="stage-event-list"> - <li v-for="build in items" class="stage-event-item item-build-component"> - <div class="item-details"> - <img class="avatar" :src="build.author.avatarUrl"> - <h5 class="item-title"> - <a :href="build.url" class="pipeline-id">#{{ build.id }}</a> - <i class="fa fa-code-fork"></i> - <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a> - <span class="icon-branch">${iconBranch}</span> - <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a> - </h5> - <span> - <a :href="build.url" class="build-date">{{ build.date }}</a> - by - <a :href="build.author.webUrl" class="issue-author-link"> - {{ build.author.name }} - </a> - </span> - </div> - <div class="item-time"> - <total-time :time="build.totalTime"></total-time> - </div> - </li> - </ul> +global.cycleAnalytics.StageStagingComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + data() { + return { iconBranch }; + }, + template: ` + <div> + <div class="events-description"> + {{ stage.description }} + <limit-warning :count="items.length" /> </div> - `, - }); -})(window.gl || (window.gl = {})); + <ul class="stage-event-list"> + <li v-for="build in items" class="stage-event-item item-build-component"> + <div class="item-details"> + <img class="avatar" :src="build.author.avatarUrl"> + <h5 class="item-title"> + <a :href="build.url" class="pipeline-id">#{{ build.id }}</a> + <i class="fa fa-code-fork"></i> + <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a> + <span class="icon-branch">${iconBranch}</span> + <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a> + </h5> + <span> + <a :href="build.url" class="build-date">{{ build.date }}</a> + by + <a :href="build.author.webUrl" class="issue-author-link"> + {{ build.author.name }} + </a> + </span> + </div> + <div class="item-time"> + <total-time :time="build.totalTime"></total-time> + </div> + </li> + </ul> + </div> + `, +}); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js index 17ae3a9ddc1..e306026429e 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js @@ -3,48 +3,47 @@ import Vue from 'vue'; import iconBuildStatus from '../svg/icon_build_status.svg'; import iconBranch from '../svg/icon_branch.svg'; -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; +const global = window.gl || (window.gl = {}); +global.cycleAnalytics = global.cycleAnalytics || {}; - global.cycleAnalytics.StageTestComponent = Vue.extend({ - props: { - items: Array, - stage: Object, - }, - data() { - return { iconBuildStatus, iconBranch }; - }, - template: ` - <div> - <div class="events-description"> - {{ stage.description }} - <limit-warning :count="items.length" /> - </div> - <ul class="stage-event-list"> - <li v-for="build in items" class="stage-event-item item-build-component"> - <div class="item-details"> - <h5 class="item-title"> - <span class="icon-build-status">${iconBuildStatus}</span> - <a :href="build.url" class="item-build-name">{{ build.name }}</a> - · - <a :href="build.url" class="pipeline-id">#{{ build.id }}</a> - <i class="fa fa-code-fork"></i> - <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a> - <span class="icon-branch">${iconBranch}</span> - <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a> - </h5> - <span> - <a :href="build.url" class="issue-date"> - {{ build.date }} - </a> - </span> - </div> - <div class="item-time"> - <total-time :time="build.totalTime"></total-time> - </div> - </li> - </ul> +global.cycleAnalytics.StageTestComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + data() { + return { iconBuildStatus, iconBranch }; + }, + template: ` + <div> + <div class="events-description"> + {{ stage.description }} + <limit-warning :count="items.length" /> </div> - `, - }); -})(window.gl || (window.gl = {})); + <ul class="stage-event-list"> + <li v-for="build in items" class="stage-event-item item-build-component"> + <div class="item-details"> + <h5 class="item-title"> + <span class="icon-build-status">${iconBuildStatus}</span> + <a :href="build.url" class="item-build-name">{{ build.name }}</a> + · + <a :href="build.url" class="pipeline-id">#{{ build.id }}</a> + <i class="fa fa-code-fork"></i> + <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a> + <span class="icon-branch">${iconBranch}</span> + <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a> + </h5> + <span> + <a :href="build.url" class="issue-date"> + {{ build.date }} + </a> + </span> + </div> + <div class="item-time"> + <total-time :time="build.totalTime"></total-time> + </div> + </li> + </ul> + </div> + `, +}); diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.js b/app/assets/javascripts/cycle_analytics/components/total_time_component.js index b4442ea5566..77edcb76273 100644 --- a/app/assets/javascripts/cycle_analytics/components/total_time_component.js +++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.js @@ -2,25 +2,24 @@ import Vue from 'vue'; -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; +const global = window.gl || (window.gl = {}); +global.cycleAnalytics = global.cycleAnalytics || {}; - global.cycleAnalytics.TotalTimeComponent = Vue.extend({ - props: { - time: Object, - }, - template: ` - <span class="total-time"> - <template v-if="Object.keys(time).length"> - <template v-if="time.days">{{ time.days }} <span>{{ time.days === 1 ? 'day' : 'days' }}</span></template> - <template v-if="time.hours">{{ time.hours }} <span>hr</span></template> - <template v-if="time.mins && !time.days">{{ time.mins }} <span>mins</span></template> - <template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>s</span></template> - </template> - <template v-else> - -- - </template> - </span> - `, - }); -})(window.gl || (window.gl = {})); +global.cycleAnalytics.TotalTimeComponent = Vue.extend({ + props: { + time: Object, + }, + template: ` + <span class="total-time"> + <template v-if="Object.keys(time).length"> + <template v-if="time.days">{{ time.days }} <span>{{ time.days === 1 ? 'day' : 'days' }}</span></template> + <template v-if="time.hours">{{ time.hours }} <span>hr</span></template> + <template v-if="time.mins && !time.days">{{ time.mins }} <span>mins</span></template> + <template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>s</span></template> + </template> + <template v-else> + -- + </template> + </span> + `, +}); diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js index 9f74b14c4b9..681d6eef565 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js @@ -1,41 +1,41 @@ /* eslint-disable no-param-reassign */ -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; - class CycleAnalyticsService { - constructor(options) { - this.requestPath = options.requestPath; - } +const global = window.gl || (window.gl = {}); +global.cycleAnalytics = global.cycleAnalytics || {}; - fetchCycleAnalyticsData(options) { - options = options || { startDate: 30 }; - - return $.ajax({ - url: this.requestPath, - method: 'GET', - dataType: 'json', - contentType: 'application/json', - data: { - cycle_analytics: { - start_date: options.startDate, - }, - }, - }); - } +class CycleAnalyticsService { + constructor(options) { + this.requestPath = options.requestPath; + } - fetchStageData(options) { - const { - stage, - startDate, - } = options; + fetchCycleAnalyticsData(options) { + options = options || { startDate: 30 }; - return $.get(`${this.requestPath}/events/${stage.title.toLowerCase()}.json`, { + return $.ajax({ + url: this.requestPath, + method: 'GET', + dataType: 'json', + contentType: 'application/json', + data: { cycle_analytics: { - start_date: startDate, + start_date: options.startDate, }, - }); - } + }, + }); + } + + fetchStageData(options) { + const { + stage, + startDate, + } = options; + + return $.get(`${this.requestPath}/events/${stage.title.toLowerCase()}.json`, { + cycle_analytics: { + start_date: startDate, + }, + }); } +} - global.cycleAnalytics.CycleAnalyticsService = CycleAnalyticsService; -})(window.gl || (window.gl = {})); +global.cycleAnalytics.CycleAnalyticsService = CycleAnalyticsService; diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js index 7ae9de7297c..6536a8fd7fa 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js @@ -3,102 +3,101 @@ require('../lib/utils/text_utility'); const DEFAULT_EVENT_OBJECTS = require('./default_event_objects'); -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; +const global = window.gl || (window.gl = {}); +global.cycleAnalytics = global.cycleAnalytics || {}; - const EMPTY_STAGE_TEXTS = { - issue: 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.', - plan: 'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.', - code: 'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.', - test: 'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.', - review: 'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.', - staging: 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.', - production: 'The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.', - }; +const EMPTY_STAGE_TEXTS = { + issue: 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.', + plan: 'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.', + code: 'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.', + test: 'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.', + review: 'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.', + staging: 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.', + production: 'The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.', +}; - global.cycleAnalytics.CycleAnalyticsStore = { - state: { - summary: '', - stats: '', - analytics: '', - events: [], - stages: [], - }, - setCycleAnalyticsData(data) { - this.state = Object.assign(this.state, this.decorateData(data)); - }, - decorateData(data) { - const newData = {}; +global.cycleAnalytics.CycleAnalyticsStore = { + state: { + summary: '', + stats: '', + analytics: '', + events: [], + stages: [], + }, + setCycleAnalyticsData(data) { + this.state = Object.assign(this.state, this.decorateData(data)); + }, + decorateData(data) { + const newData = {}; - newData.stages = data.stats || []; - newData.summary = data.summary || []; + newData.stages = data.stats || []; + newData.summary = data.summary || []; - newData.summary.forEach((item) => { - item.value = item.value || '-'; - }); + newData.summary.forEach((item) => { + item.value = item.value || '-'; + }); - newData.stages.forEach((item) => { - const stageSlug = gl.text.dasherize(item.title.toLowerCase()); - item.active = false; - item.isUserAllowed = data.permissions[stageSlug]; - item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug]; - item.component = `stage-${stageSlug}-component`; - item.slug = stageSlug; - }); - newData.analytics = data; - return newData; - }, - setLoadingState(state) { - this.state.isLoading = state; - }, - setErrorState(state) { - this.state.hasError = state; - }, - deactivateAllStages() { - this.state.stages.forEach((stage) => { - stage.active = false; - }); - }, - setActiveStage(stage) { - this.deactivateAllStages(); - stage.active = true; - }, - setStageEvents(events, stage) { - this.state.events = this.decorateEvents(events, stage); - }, - decorateEvents(events, stage) { - const newEvents = []; + newData.stages.forEach((item) => { + const stageSlug = gl.text.dasherize(item.title.toLowerCase()); + item.active = false; + item.isUserAllowed = data.permissions[stageSlug]; + item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug]; + item.component = `stage-${stageSlug}-component`; + item.slug = stageSlug; + }); + newData.analytics = data; + return newData; + }, + setLoadingState(state) { + this.state.isLoading = state; + }, + setErrorState(state) { + this.state.hasError = state; + }, + deactivateAllStages() { + this.state.stages.forEach((stage) => { + stage.active = false; + }); + }, + setActiveStage(stage) { + this.deactivateAllStages(); + stage.active = true; + }, + setStageEvents(events, stage) { + this.state.events = this.decorateEvents(events, stage); + }, + decorateEvents(events, stage) { + const newEvents = []; - events.forEach((item) => { - if (!item) return; + events.forEach((item) => { + if (!item) return; - const eventItem = Object.assign({}, DEFAULT_EVENT_OBJECTS[stage.slug], item); + const eventItem = Object.assign({}, DEFAULT_EVENT_OBJECTS[stage.slug], item); - eventItem.totalTime = eventItem.total_time; + eventItem.totalTime = eventItem.total_time; - if (eventItem.author) { - eventItem.author.webUrl = eventItem.author.web_url; - eventItem.author.avatarUrl = eventItem.author.avatar_url; - } + if (eventItem.author) { + eventItem.author.webUrl = eventItem.author.web_url; + eventItem.author.avatarUrl = eventItem.author.avatar_url; + } - if (eventItem.created_at) eventItem.createdAt = eventItem.created_at; - if (eventItem.short_sha) eventItem.shortSha = eventItem.short_sha; - if (eventItem.commit_url) eventItem.commitUrl = eventItem.commit_url; + if (eventItem.created_at) eventItem.createdAt = eventItem.created_at; + if (eventItem.short_sha) eventItem.shortSha = eventItem.short_sha; + if (eventItem.commit_url) eventItem.commitUrl = eventItem.commit_url; - delete eventItem.author.web_url; - delete eventItem.author.avatar_url; - delete eventItem.total_time; - delete eventItem.created_at; - delete eventItem.short_sha; - delete eventItem.commit_url; + delete eventItem.author.web_url; + delete eventItem.author.avatar_url; + delete eventItem.total_time; + delete eventItem.created_at; + delete eventItem.short_sha; + delete eventItem.commit_url; - newEvents.push(eventItem); - }); + newEvents.push(eventItem); + }); - return newEvents; - }, - currentActiveStage() { - return this.state.stages.find(stage => stage.active); - }, - }; -})(window.gl || (window.gl = {})); + return newEvents; + }, + currentActiveStage() { + return this.state.stages.find(stage => stage.active); + }, +}; diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 2e5f8a09fc1..fecd531328d 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -1,192 +1,188 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len */ - +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */ require('vendor/latinise'); -(function() { - (function(w) { - var base; - if (w.gl == null) { - w.gl = {}; - } - if ((base = w.gl).text == null) { - base.text = {}; +var base; +var w = window; +if (w.gl == null) { + w.gl = {}; +} +if ((base = w.gl).text == null) { + base.text = {}; +} +gl.text.addDelimiter = function(text) { + return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text; +}; +gl.text.highCountTrim = function(count) { + return count > 99 ? '99+' : count; +}; +gl.text.randomString = function() { + return Math.random().toString(36).substring(7); +}; +gl.text.replaceRange = function(s, start, end, substitute) { + return s.substring(0, start) + substitute + s.substring(end); +}; +gl.text.getTextWidth = function(text, font) { + /** + * Uses canvas.measureText to compute and return the width of the given text of given font in pixels. + * + * @param {String} text The text to be rendered. + * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana"). + * + * @see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393 + */ + // re-use canvas object for better performance + var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement('canvas')); + var context = canvas.getContext('2d'); + context.font = font; + return context.measureText(text).width; +}; +gl.text.selectedText = function(text, textarea) { + return text.substring(textarea.selectionStart, textarea.selectionEnd); +}; +gl.text.lineBefore = function(text, textarea) { + var split; + split = text.substring(0, textarea.selectionStart).trim().split('\n'); + return split[split.length - 1]; +}; +gl.text.lineAfter = function(text, textarea) { + return text.substring(textarea.selectionEnd).trim().split('\n')[0]; +}; +gl.text.blockTagText = function(text, textArea, blockTag, selected) { + var lineAfter, lineBefore; + lineBefore = this.lineBefore(text, textArea); + lineAfter = this.lineAfter(text, textArea); + if (lineBefore === blockTag && lineAfter === blockTag) { + // To remove the block tag we have to select the line before & after + if (blockTag != null) { + textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1); + textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1); } - gl.text.addDelimiter = function(text) { - return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text; - }; - gl.text.highCountTrim = function(count) { - return count > 99 ? '99+' : count; - }; - gl.text.randomString = function() { - return Math.random().toString(36).substring(7); - }; - gl.text.replaceRange = function(s, start, end, substitute) { - return s.substring(0, start) + substitute + s.substring(end); - }; - gl.text.getTextWidth = function(text, font) { - /** - * Uses canvas.measureText to compute and return the width of the given text of given font in pixels. - * - * @param {String} text The text to be rendered. - * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana"). - * - * @see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393 - */ - // re-use canvas object for better performance - var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement('canvas')); - var context = canvas.getContext('2d'); - context.font = font; - return context.measureText(text).width; - }; - gl.text.selectedText = function(text, textarea) { - return text.substring(textarea.selectionStart, textarea.selectionEnd); - }; - gl.text.lineBefore = function(text, textarea) { - var split; - split = text.substring(0, textarea.selectionStart).trim().split('\n'); - return split[split.length - 1]; - }; - gl.text.lineAfter = function(text, textarea) { - return text.substring(textarea.selectionEnd).trim().split('\n')[0]; - }; - gl.text.blockTagText = function(text, textArea, blockTag, selected) { - var lineAfter, lineBefore; - lineBefore = this.lineBefore(text, textArea); - lineAfter = this.lineAfter(text, textArea); - if (lineBefore === blockTag && lineAfter === blockTag) { - // To remove the block tag we have to select the line before & after - if (blockTag != null) { - textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1); - textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1); - } - return selected; - } else { - return blockTag + "\n" + selected + "\n" + blockTag; - } - }; - gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) { - var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine; - removedLastNewLine = false; - removedFirstNewLine = false; - currentLineEmpty = false; + return selected; + } else { + return blockTag + "\n" + selected + "\n" + blockTag; + } +}; +gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) { + var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine; + removedLastNewLine = false; + removedFirstNewLine = false; + currentLineEmpty = false; - // Remove the first newline - if (selected.indexOf('\n') === 0) { - removedFirstNewLine = true; - selected = selected.replace(/\n+/, ''); - } + // Remove the first newline + if (selected.indexOf('\n') === 0) { + removedFirstNewLine = true; + selected = selected.replace(/\n+/, ''); + } - // Remove the last newline - if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) { - removedLastNewLine = true; - selected = selected.replace(/\n$/, ''); - } + // Remove the last newline + if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) { + removedLastNewLine = true; + selected = selected.replace(/\n$/, ''); + } - selectedSplit = selected.split('\n'); + selectedSplit = selected.split('\n'); - if (!wrap) { - lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n'); + if (!wrap) { + lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n'); - // Check whether the current line is empty or consists only of spaces(=handle as empty) - if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) { - currentLineEmpty = true; - } - } + // Check whether the current line is empty or consists only of spaces(=handle as empty) + if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) { + currentLineEmpty = true; + } + } - startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : ''; + startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : ''; - if (selectedSplit.length > 1 && (!wrap || (blockTag != null))) { - if (blockTag != null) { - insertText = this.blockTagText(text, textArea, blockTag, selected); + if (selectedSplit.length > 1 && (!wrap || (blockTag != null))) { + if (blockTag != null) { + insertText = this.blockTagText(text, textArea, blockTag, selected); + } else { + insertText = selectedSplit.map(function(val) { + if (val.indexOf(tag) === 0) { + return "" + (val.replace(tag, '')); } else { - insertText = selectedSplit.map(function(val) { - if (val.indexOf(tag) === 0) { - return "" + (val.replace(tag, '')); - } else { - return "" + tag + val; - } - }).join('\n'); + return "" + tag + val; } - } else { - insertText = "" + startChar + tag + selected + (wrap ? tag : ' '); - } + }).join('\n'); + } + } else { + insertText = "" + startChar + tag + selected + (wrap ? tag : ' '); + } - if (removedFirstNewLine) { - insertText = '\n' + insertText; - } + if (removedFirstNewLine) { + insertText = '\n' + insertText; + } - if (removedLastNewLine) { - insertText += '\n'; - } + if (removedLastNewLine) { + insertText += '\n'; + } - if (document.queryCommandSupported('insertText')) { - inserted = document.execCommand('insertText', false, insertText); - } - if (!inserted) { - try { - document.execCommand("ms-beginUndoUnit"); - } catch (error) {} - textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText); - try { - document.execCommand("ms-endUndoUnit"); - } catch (error) {} - } - return this.moveCursor(textArea, tag, wrap, removedLastNewLine); - }; - gl.text.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) { - var pos; - if (!textArea.setSelectionRange) { - return; - } - if (textArea.selectionStart === textArea.selectionEnd) { - if (wrapped) { - pos = textArea.selectionStart - tag.length; - } else { - pos = textArea.selectionStart; - } + if (document.queryCommandSupported('insertText')) { + inserted = document.execCommand('insertText', false, insertText); + } + if (!inserted) { + try { + document.execCommand("ms-beginUndoUnit"); + } catch (error) {} + textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText); + try { + document.execCommand("ms-endUndoUnit"); + } catch (error) {} + } + return this.moveCursor(textArea, tag, wrap, removedLastNewLine); +}; +gl.text.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) { + var pos; + if (!textArea.setSelectionRange) { + return; + } + if (textArea.selectionStart === textArea.selectionEnd) { + if (wrapped) { + pos = textArea.selectionStart - tag.length; + } else { + pos = textArea.selectionStart; + } - if (removedLastNewLine) { - pos -= 1; - } + if (removedLastNewLine) { + pos -= 1; + } - return textArea.setSelectionRange(pos, pos); - } - }; - gl.text.updateText = function(textArea, tag, blockTag, wrap) { - var $textArea, selected, text; - $textArea = $(textArea); - textArea = $textArea.get(0); - text = $textArea.val(); - selected = this.selectedText(text, textArea); - $textArea.focus(); - return this.insertText(textArea, text, tag, blockTag, selected, wrap); - }; - gl.text.init = function(form) { - var self; - self = this; - return $('.js-md', form).off('click').on('click', function() { - var $this; - $this = $(this); - return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend')); - }); - }; - gl.text.removeListeners = function(form) { - return $('.js-md', form).off(); - }; - 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' : ''); - }; - gl.text.truncate = function(string, maxLength) { - return string.substr(0, (maxLength - 3)) + '...'; - }; - gl.text.dasherize = function(str) { - return str.replace(/[_\s]+/g, '-'); - }; - gl.text.slugify = function(str) { - return str.trim().toLowerCase().latinise(); - }; - })(window); -}).call(window); + return textArea.setSelectionRange(pos, pos); + } +}; +gl.text.updateText = function(textArea, tag, blockTag, wrap) { + var $textArea, selected, text; + $textArea = $(textArea); + textArea = $textArea.get(0); + text = $textArea.val(); + selected = this.selectedText(text, textArea); + $textArea.focus(); + return this.insertText(textArea, text, tag, blockTag, selected, wrap); +}; +gl.text.init = function(form) { + var self; + self = this; + return $('.js-md', form).off('click').on('click', function() { + var $this; + $this = $(this); + return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend')); + }); +}; +gl.text.removeListeners = function(form) { + return $('.js-md', form).off(); +}; +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' : ''); +}; +gl.text.truncate = function(string, maxLength) { + return string.substr(0, (maxLength - 3)) + '...'; +}; +gl.text.dasherize = function(str) { + return str.replace(/[_\s]+/g, '-'); +}; +gl.text.slugify = function(str) { + return str.trim().toLowerCase().latinise(); +}; diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 09c4261b318..b9d2fc25c39 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -1,93 +1,90 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, guard-for-in, no-restricted-syntax, prefer-template, quotes, max-len */ -(function() { - (function(w) { - var base; - if (w.gl == null) { - w.gl = {}; +var base; +var w = window; +if (w.gl == null) { + w.gl = {}; +} +if ((base = w.gl).utils == null) { + base.utils = {}; +} +// Returns an array containing the value(s) of the +// of the key passed as an argument +w.gl.utils.getParameterValues = function(sParam) { + var i, sPageURL, sParameterName, sURLVariables, values; + sPageURL = decodeURIComponent(window.location.search.substring(1)); + sURLVariables = sPageURL.split('&'); + sParameterName = void 0; + values = []; + i = 0; + while (i < sURLVariables.length) { + sParameterName = sURLVariables[i].split('='); + if (sParameterName[0] === sParam) { + values.push(sParameterName[1].replace(/\+/g, ' ')); } - if ((base = w.gl).utils == null) { - base.utils = {}; + i += 1; + } + return values; +}; +// @param {Object} params - url keys and value to merge +// @param {String} url +w.gl.utils.mergeUrlParams = function(params, url) { + var lastChar, newUrl, paramName, paramValue, pattern; + newUrl = decodeURIComponent(url); + for (paramName in params) { + paramValue = params[paramName]; + pattern = new RegExp("\\b(" + paramName + "=).*?(&|$)"); + if (paramValue == null) { + newUrl = newUrl.replace(pattern, ''); + } else if (url.search(pattern) !== -1) { + newUrl = newUrl.replace(pattern, "$1" + paramValue + "$2"); + } else { + newUrl = "" + newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + "=" + paramValue; } - // Returns an array containing the value(s) of the - // of the key passed as an argument - w.gl.utils.getParameterValues = function(sParam) { - var i, sPageURL, sParameterName, sURLVariables, values; - sPageURL = decodeURIComponent(window.location.search.substring(1)); - sURLVariables = sPageURL.split('&'); - sParameterName = void 0; - values = []; - i = 0; - while (i < sURLVariables.length) { - sParameterName = sURLVariables[i].split('='); - if (sParameterName[0] === sParam) { - values.push(sParameterName[1].replace(/\+/g, ' ')); - } - i += 1; + } + // Remove a trailing ampersand + lastChar = newUrl[newUrl.length - 1]; + if (lastChar === '&') { + newUrl = newUrl.slice(0, -1); + } + return newUrl; +}; +// removes parameter query string from url. returns the modified url +w.gl.utils.removeParamQueryString = function(url, param) { + var urlVariables, variables; + url = decodeURIComponent(url); + urlVariables = url.split('&'); + return ((function() { + var j, len, results; + results = []; + for (j = 0, len = urlVariables.length; j < len; j += 1) { + variables = urlVariables[j]; + if (variables.indexOf(param) === -1) { + results.push(variables); } - return values; - }; - // @param {Object} params - url keys and value to merge - // @param {String} url - w.gl.utils.mergeUrlParams = function(params, url) { - var lastChar, newUrl, paramName, paramValue, pattern; - newUrl = decodeURIComponent(url); - for (paramName in params) { - paramValue = params[paramName]; - pattern = new RegExp("\\b(" + paramName + "=).*?(&|$)"); - if (paramValue == null) { - newUrl = newUrl.replace(pattern, ''); - } else if (url.search(pattern) !== -1) { - newUrl = newUrl.replace(pattern, "$1" + paramValue + "$2"); - } else { - newUrl = "" + newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + "=" + paramValue; - } - } - // Remove a trailing ampersand - lastChar = newUrl[newUrl.length - 1]; - if (lastChar === '&') { - newUrl = newUrl.slice(0, -1); - } - return newUrl; - }; - // removes parameter query string from url. returns the modified url - w.gl.utils.removeParamQueryString = function(url, param) { - var urlVariables, variables; - url = decodeURIComponent(url); - urlVariables = url.split('&'); - return ((function() { - var j, len, results; - results = []; - for (j = 0, len = urlVariables.length; j < len; j += 1) { - variables = urlVariables[j]; - if (variables.indexOf(param) === -1) { - results.push(variables); - } - } - return results; - })()).join('&'); - }; - w.gl.utils.removeParams = (params) => { - const url = new URL(window.location.href); - params.forEach((param) => { - url.search = w.gl.utils.removeParamQueryString(url.search, param); - }); - return url.href; - }; - w.gl.utils.getLocationHash = function(url) { - var hashIndex; - if (typeof url === 'undefined') { - // Note: We can't use window.location.hash here because it's - // not consistent across browsers - Firefox will pre-decode it - url = window.location.href; - } - hashIndex = url.indexOf('#'); - return hashIndex === -1 ? null : url.substring(hashIndex + 1); - }; + } + return results; + })()).join('&'); +}; +w.gl.utils.removeParams = (params) => { + const url = new URL(window.location.href); + params.forEach((param) => { + url.search = w.gl.utils.removeParamQueryString(url.search, param); + }); + return url.href; +}; +w.gl.utils.getLocationHash = function(url) { + var hashIndex; + if (typeof url === 'undefined') { + // Note: We can't use window.location.hash here because it's + // not consistent across browsers - Firefox will pre-decode it + url = window.location.href; + } + hashIndex = url.indexOf('#'); + return hashIndex === -1 ? null : url.substring(hashIndex + 1); +}; - w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href); +w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href); - w.gl.utils.visitUrl = (url) => { - document.location.href = url; - }; - })(window); -}).call(window); +w.gl.utils.visitUrl = (url) => { + document.location.href = url; +}; diff --git a/app/assets/javascripts/merged_buttons.js b/app/assets/javascripts/merged_buttons.js index 9548a98f499..7b0997c6520 100644 --- a/app/assets/javascripts/merged_buttons.js +++ b/app/assets/javascripts/merged_buttons.js @@ -1,11 +1,13 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len */ -(function() { - var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; +import '~/lib/utils/url_utility'; +(function() { this.MergedButtons = (function() { function MergedButtons() { - this.removeSourceBranch = bind(this.removeSourceBranch, this); + this.removeSourceBranch = this.removeSourceBranch.bind(this); + this.removeBranchSuccess = this.removeBranchSuccess.bind(this); + this.removeBranchError = this.removeBranchError.bind(this); this.$removeBranchWidget = $('.remove_source_branch_widget'); this.$removeBranchProgress = $('.remove_source_branch_in_progress'); this.$removeBranchFailed = $('.remove_source_branch_widget.failed'); @@ -22,7 +24,7 @@ MergedButtons.prototype.initEventListeners = function() { $(document).on('click', '.remove_source_branch', this.removeSourceBranch); $(document).on('ajax:success', '.remove_source_branch', this.removeBranchSuccess); - return $(document).on('ajax:error', '.remove_source_branch', this.removeBranchError); + $(document).on('ajax:error', '.remove_source_branch', this.removeBranchError); }; MergedButtons.prototype.removeSourceBranch = function() { @@ -31,7 +33,7 @@ }; MergedButtons.prototype.removeBranchSuccess = function() { - return location.reload(); + gl.utils.refreshCurrentPage(); }; MergedButtons.prototype.removeBranchError = function() { diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 279a50eaa33..11d44df4867 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -246,17 +246,17 @@ } } -.filtered-search-history-dropdown-toggle-button { +.filtered-search-history-dropdown-wrapper { + position: static; display: flex; - align-items: center; + flex-direction: column; +} + +.filtered-search-history-dropdown-toggle-button { + flex: 1; width: auto; - height: 100%; - padding-top: 0; - padding-left: 0.75em; - padding-bottom: 0; - padding-right: 0.5em; + padding-right: 10px; - background-color: transparent; border-radius: 0; border-top: 0; border-left: 0; @@ -264,6 +264,7 @@ border-right: 1px solid $border-color; color: $gl-text-color-secondary; + line-height: 1; transition: color 0.1s linear; @@ -275,24 +276,21 @@ } .dropdown-toggle-text { + display: inline-block; color: inherit; .fa { + vertical-align: middle; color: inherit; } } .fa { - position: initial; + position: static; } } -.filtered-search-history-dropdown-wrapper { - position: initial; - flex-shrink: 0; -} - .filtered-search-history-dropdown { width: 40%; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index c241816788b..664539e93e1 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -158,6 +158,7 @@ li.task-list-item { list-style-type: none; position: relative; + min-height: 22px; padding-left: 28px; margin-left: 0 !important; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 712eb7caf33..20ef9a774e4 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -26,6 +26,7 @@ $gray-dark: darken($gray-light, $darken-dark-factor); $gray-darker: #eee; $gray-darkest: #c4c4c4; +$green-25: #f6fcf8; $green-50: #e4f5eb; $green-100: #bae6cc; $green-200: #8dd5aa; @@ -37,6 +38,7 @@ $green-700: #12753a; $green-800: #0e5a2d; $green-900: #0a4020; +$blue-25: #f6fafd; $blue-50: #e4eff9; $blue-100: #bcd7f1; $blue-200: #8fbce8; @@ -48,6 +50,7 @@ $blue-700: #17599c; $blue-800: #134a81; $blue-900: #0f3b66; +$orange-25: #fffcf8; $orange-50: #fff2e1; $orange-100: #fedfb3; $orange-200: #feca81; @@ -59,6 +62,7 @@ $orange-700: #c26700; $orange-800: #a35100; $orange-900: #853b00; +$red-25: #fef7f6; $red-50: #fbe7e4; $red-100: #f4c4bc; $red-200: #ed9d90; @@ -147,7 +151,7 @@ $gl-sidebar-padding: 22px; /* * Misc */ -$row-hover: lighten($blue-50, 2%); +$row-hover: $blue-25; $row-hover-border: $blue-100; $progress-color: #c0392b; $header-height: 50px; @@ -223,18 +227,18 @@ $gl-btn-active-gradient: inset 0 2px 3px $gl-btn-active-background; /* * Commit Diff Colors */ -$added: $green-300; -$deleted: $red-300; -$line-added: $green-50; -$line-added-dark: $green-100; -$line-removed: $red-50; -$line-removed-dark: $red-100; -$line-number-old: lighten($red-100, 5%); -$line-number-new: lighten($green-100, 5%); -$line-number-select: lighten($orange-100, 5%); -$line-target-blue: $blue-50; -$line-select-yellow: $orange-50; -$line-select-yellow-dark: $orange-100; +$added: #63c363; +$deleted: #f77; +$line-added: #ecfdf0; +$line-added-dark: #c7f0d2; +$line-removed: #fbe9eb; +$line-removed-dark: #fac5cd; +$line-number-old: #f9d7dc; +$line-number-new: #ddfbe6; +$line-number-select: #fbf2da; +$line-target-blue: #f6faff; +$line-select-yellow: #fcf8e7; +$line-select-yellow-dark: #f0e2bd; $dark-diff-match-bg: rgba(255, 255, 255, 0.3); $dark-diff-match-color: rgba(255, 255, 255, 0.1); $file-mode-changed: #777; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index ad0f2f6efbb..c78fb8ede79 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -627,7 +627,6 @@ ul.notes { } &:not(.is-disabled):hover, - &:not(.is-disabled):focus, &.is-active { color: $gl-text-green; @@ -641,6 +640,11 @@ ul.notes { height: 15px; width: 15px; } + + .loading { + margin: 0; + height: auto; + } } .discussion-next-btn { diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 8109427a45f..3ca14dee33c 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -60,7 +60,7 @@ class RegistrationsController < Devise::RegistrationsController end def resource - @resource ||= Users::CreateService.new(current_user, sign_up_params).build + @resource ||= Users::BuildService.new(current_user, sign_up_params).execute end def devise_mapping diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb index 82f4182d59a..d0c94d3b694 100644 --- a/app/models/container_repository.rb +++ b/app/models/container_repository.rb @@ -20,7 +20,8 @@ class ContainerRepository < ActiveRecord::Base end def path - @path ||= [project.full_path, name].select(&:present?).join('/') + @path ||= [project.full_path, name] + .select(&:present?).join('/').downcase end def location diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index fbdaa455651..7828c5806b0 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -58,6 +58,9 @@ module Projects fail(error: @project.errors.full_messages.join(', ')) end @project + rescue ActiveRecord::RecordInvalid => e + message = "Unable to save #{e.record.type}: #{e.record.errors.full_messages.join(", ")} " + fail(error: message) rescue => e fail(error: e.message) end diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb new file mode 100644 index 00000000000..9a0a5a12f91 --- /dev/null +++ b/app/services/users/build_service.rb @@ -0,0 +1,100 @@ +module Users + # Service for building a new user. + class BuildService < BaseService + def initialize(current_user, params = {}) + @current_user = current_user + @params = params.dup + end + + def execute + raise Gitlab::Access::AccessDeniedError unless can_create_user? + + user = User.new(build_user_params) + + if current_user&.admin? + if params[:reset_password] + user.generate_reset_token + params[:force_random_password] = true + end + + if params[:force_random_password] + random_password = Devise.friendly_token.first(Devise.password_length.min) + user.password = user.password_confirmation = random_password + end + end + + identity_attrs = params.slice(:extern_uid, :provider) + + if identity_attrs.any? + user.identities.build(identity_attrs) + end + + user + end + + private + + def can_create_user? + (current_user.nil? && current_application_settings.signup_enabled?) || current_user&.admin? + end + + # Allowed params for creating a user (admins only) + def admin_create_params + [ + :access_level, + :admin, + :avatar, + :bio, + :can_create_group, + :color_scheme_id, + :email, + :external, + :force_random_password, + :hide_no_password, + :hide_no_ssh_key, + :key_id, + :linkedin, + :name, + :password, + :password_automatically_set, + :password_expires_at, + :projects_limit, + :remember_me, + :skip_confirmation, + :skype, + :theme_id, + :twitter, + :username, + :website_url + ] + end + + # Allowed params for user signup + def signup_params + [ + :email, + :email_confirmation, + :password_automatically_set, + :name, + :password, + :username + ] + end + + def build_user_params + if current_user&.admin? + user_params = params.slice(*admin_create_params) + user_params[:created_by_id] = current_user&.id + + if params[:reset_password] + user_params.merge!(force_random_password: true, password_expires_at: nil) + end + else + user_params = params.slice(*signup_params) + user_params[:skip_confirmation] = !current_application_settings.send_user_confirmation_email + end + + user_params + end + end +end diff --git a/app/services/users/create_service.rb b/app/services/users/create_service.rb index 93ca7b1141a..a2105d31f71 100644 --- a/app/services/users/create_service.rb +++ b/app/services/users/create_service.rb @@ -6,34 +6,10 @@ module Users @params = params.dup end - def build - raise Gitlab::Access::AccessDeniedError unless can_create_user? - - user = User.new(build_user_params) - - if current_user&.admin? - if params[:reset_password] - @reset_token = user.generate_reset_token - params[:force_random_password] = true - end - - if params[:force_random_password] - random_password = Devise.friendly_token.first(Devise.password_length.min) - user.password = user.password_confirmation = random_password - end - end - - identity_attrs = params.slice(:extern_uid, :provider) - - if identity_attrs.any? - user.identities.build(identity_attrs) - end - - user - end - def execute - user = build + user = Users::BuildService.new(current_user, params).execute + + @reset_token = user.generate_reset_token if user.recently_sent_password_reset? if user.save log_info("User \"#{user.name}\" (#{user.email}) was created") @@ -43,70 +19,5 @@ module Users user end - - private - - def can_create_user? - (current_user.nil? && current_application_settings.signup_enabled?) || current_user&.admin? - end - - # Allowed params for creating a user (admins only) - def admin_create_params - [ - :access_level, - :admin, - :avatar, - :bio, - :can_create_group, - :color_scheme_id, - :email, - :external, - :force_random_password, - :password_automatically_set, - :hide_no_password, - :hide_no_ssh_key, - :key_id, - :linkedin, - :name, - :password, - :password_expires_at, - :projects_limit, - :remember_me, - :skip_confirmation, - :skype, - :theme_id, - :twitter, - :username, - :website_url - ] - end - - # Allowed params for user signup - def signup_params - [ - :email, - :email_confirmation, - :password_automatically_set, - :name, - :password, - :username - ] - end - - def build_user_params - if current_user&.admin? - user_params = params.slice(*admin_create_params) - user_params[:created_by_id] = current_user&.id - - if params[:reset_password] - user_params.merge!(force_random_password: true, password_expires_at: nil) - end - else - user_params = params.slice(*signup_params) - user_params[:skip_confirmation] = !current_application_settings.send_user_confirmation_email - end - - user_params - end end end diff --git a/app/views/admin/services/index.html.haml b/app/views/admin/services/index.html.haml index 6a5986f496a..50132572096 100644 --- a/app/views/admin/services/index.html.haml +++ b/app/views/admin/services/index.html.haml @@ -13,7 +13,7 @@ - @services.sort_by(&:title).each do |service| %tr %td - = icon("copy", class: 'clgray') + = boolean_to_icon service.activated? %td = link_to edit_admin_application_settings_service_path(service.id) do %strong= service.title diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 299dace3406..e34cddeb3e2 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -23,7 +23,7 @@ Registry - if project_nav_tab? :issues - = nav_link(controller: [:issues, :labels, :milestones, :boards]) do + = nav_link(controller: @project.default_issues_tracker? ? [:issues, :labels, :milestones, :boards] : :issues) do = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues', class: 'shortcuts-issues' do %span Issues @@ -31,7 +31,7 @@ %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) - if project_nav_tab? :merge_requests - = nav_link(controller: :merge_requests) do + = nav_link(controller: @project.default_issues_tracker? ? :merge_requests : [:merge_requests, :labels, :milestones]) do = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do %span Merge Requests diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml index a80a07b52e6..7f0059cdcda 100644 --- a/app/views/projects/labels/edit.html.haml +++ b/app/views/projects/labels/edit.html.haml @@ -1,6 +1,6 @@ - @no_container = true - page_title "Edit", @label.name, "Labels" -= render "projects/issues/head" += render "shared/mr_head" %div{ class: container_class } %h3.page-title diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 29f861c09c6..fc72c4fb635 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -1,7 +1,7 @@ - @no_container = true - page_title "Labels" - hide_class = '' -= render "projects/issues/head" += render "shared/mr_head" - if @labels.exists? || @prioritized_labels.exists? %div{ class: container_class } diff --git a/app/views/projects/labels/new.html.haml b/app/views/projects/labels/new.html.haml index f0d9be744d1..8f6c085a361 100644 --- a/app/views/projects/labels/new.html.haml +++ b/app/views/projects/labels/new.html.haml @@ -1,6 +1,6 @@ - @no_container = true - page_title "New Label" -= render "projects/issues/head" += render "shared/mr_head" %div{ class: container_class } %h3.page-title diff --git a/app/views/projects/merge_requests/_head.html.haml b/app/views/projects/merge_requests/_head.html.haml new file mode 100644 index 00000000000..b7f73fe5339 --- /dev/null +++ b/app/views/projects/merge_requests/_head.html.haml @@ -0,0 +1,21 @@ += content_for :sub_nav do + .scrolling-tabs-container.sub-nav-scroll + = render 'shared/nav_scroll' + .nav-links.sub-nav.scrolling-tabs + %ul{ class: (container_class) } + = nav_link(controller: :merge_requests) do + = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do + %span + List + + - if project_nav_tab? :labels + = nav_link(controller: :labels) do + = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do + %span + Labels + + - if project_nav_tab? :milestones + = nav_link(controller: :milestones) do + = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do + %span + Milestones diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index 64f17ab34b1..6bf0035e051 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -2,6 +2,9 @@ - @bulk_edit = can?(current_user, :admin_merge_request, @project) - page_title "Merge Requests" +- unless @project.default_issues_tracker? + = content_for :sub_nav do + = render "projects/merge_requests/head" = render 'projects/last_push' - content_for :page_specific_javascripts do diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml index 55b0b837c6d..e57a76dbfd2 100644 --- a/app/views/projects/milestones/edit.html.haml +++ b/app/views/projects/milestones/edit.html.haml @@ -1,6 +1,6 @@ - @no_container = true - page_title "Edit", @milestone.title, "Milestones" -= render "projects/issues/head" += render "shared/mr_head" %div{ class: container_class } diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index 8e85b2e8a20..e1096bd1d67 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -1,6 +1,6 @@ - @no_container = true - page_title 'Milestones' -= render 'projects/issues/head' += render "shared/mr_head" %div{ class: container_class } .top-area diff --git a/app/views/projects/milestones/new.html.haml b/app/views/projects/milestones/new.html.haml index cda093ade81..586eb909afa 100644 --- a/app/views/projects/milestones/new.html.haml +++ b/app/views/projects/milestones/new.html.haml @@ -1,6 +1,6 @@ - @no_container = true - page_title "New Milestone" -= render "projects/issues/head" += render "shared/mr_head" %div{ class: container_class } %h3.page-title diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 8b62b156853..a173117984d 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -1,7 +1,7 @@ - @no_container = true - page_title @milestone.title, "Milestones" - page_description @milestone.description -= render "projects/issues/head" += render "shared/mr_head" %div{ class: container_class } .detail-page-header.milestone-page-header diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index c12c05eeb73..1f021ad77e5 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -52,11 +52,10 @@ ":aria-label" => "buttonText", "@click" => "resolve", ":title" => "buttonText", - "v-show" => "!loading", ":ref" => "'button'" } - = icon("spin spinner", "v-show" => "loading") - = render "shared/icons/icon_status_success.svg" + = icon("spin spinner", "v-show" => "loading", class: 'loading') + %div{ 'v-show' => '!loading' }= render "shared/icons/icon_status_success.svg" - if current_user - if note.emoji_awardable? diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index de1229d58aa..fd7bd21677c 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -12,7 +12,7 @@ = render "projects/last_push" = render "home_panel" -- if current_user && can?(current_user, :download_code, @project) +- if can?(current_user, :download_code, @project) %nav.project-stats{ class: container_class } %ul.nav %li diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml index 54b5ae2402e..1c7c73be933 100644 --- a/app/views/shared/_import_form.html.haml +++ b/app/views/shared/_import_form.html.haml @@ -2,7 +2,7 @@ = f.label :import_url, class: 'control-label' do %span Git repository URL .col-sm-10 - = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', disabled: true + = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git' .well.prepend-top-20 %ul diff --git a/app/views/shared/_mr_head.html.haml b/app/views/shared/_mr_head.html.haml new file mode 100644 index 00000000000..4211ec6351d --- /dev/null +++ b/app/views/shared/_mr_head.html.haml @@ -0,0 +1,4 @@ +- if @project.default_issues_tracker? + = render "projects/issues/head" +- else + = render "projects/merge_requests/head" diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml index 7a7e3d46796..c229d18903f 100644 --- a/app/views/shared/empty_states/_issues.html.haml +++ b/app/views/shared/empty_states/_issues.html.haml @@ -16,6 +16,8 @@ Also, issues are searchable and filterable. - if project_select_button = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue' + = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link' - else - %h4 There are no issues to show. - = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link' + .text-center + %h4 There are no issues to show. + = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link' diff --git a/changelogs/unreleased/30349-create-users-build-service.yml b/changelogs/unreleased/30349-create-users-build-service.yml new file mode 100644 index 00000000000..49b571f5646 --- /dev/null +++ b/changelogs/unreleased/30349-create-users-build-service.yml @@ -0,0 +1,4 @@ +--- +title: Implement Users::BuildService +merge_request: 30349 +author: George Andrinopoulos diff --git a/changelogs/unreleased/30779-show-mr-subnav-issue-tracker.yml b/changelogs/unreleased/30779-show-mr-subnav-issue-tracker.yml new file mode 100644 index 00000000000..59f8942911c --- /dev/null +++ b/changelogs/unreleased/30779-show-mr-subnav-issue-tracker.yml @@ -0,0 +1,4 @@ +--- +title: Show sub-nav under Merge Requests when issue tracker is non-default. +merge_request: 10658 +author: diff --git a/changelogs/unreleased/empty-task-list-alignment.yml b/changelogs/unreleased/empty-task-list-alignment.yml new file mode 100644 index 00000000000..ca04e1cab5a --- /dev/null +++ b/changelogs/unreleased/empty-task-list-alignment.yml @@ -0,0 +1,4 @@ +--- +title: Fixed alignment of empty task list items +merge_request: +author: diff --git a/changelogs/unreleased/fix-trace-seeking.yml b/changelogs/unreleased/fix-trace-seeking.yml new file mode 100644 index 00000000000..b753df4bb43 --- /dev/null +++ b/changelogs/unreleased/fix-trace-seeking.yml @@ -0,0 +1,4 @@ +--- +title: Fix invalid encoding when showing some traces +merge_request: 10681 +author: diff --git a/changelogs/unreleased/issues-empty-state-not-centered.yml b/changelogs/unreleased/issues-empty-state-not-centered.yml new file mode 100644 index 00000000000..883125e28b1 --- /dev/null +++ b/changelogs/unreleased/issues-empty-state-not-centered.yml @@ -0,0 +1,4 @@ +--- +title: Centered issues empty state +merge_request: +author: diff --git a/changelogs/unreleased/pms-lighter-colors.yml b/changelogs/unreleased/pms-lighter-colors.yml new file mode 100644 index 00000000000..958d4bc0ac0 --- /dev/null +++ b/changelogs/unreleased/pms-lighter-colors.yml @@ -0,0 +1,4 @@ +--- +title: Add lighter colors and fix existing light colors +merge_request: 10690 +author: diff --git a/changelogs/unreleased/use-hashie-forbidden_attributes.yml b/changelogs/unreleased/use-hashie-forbidden_attributes.yml new file mode 100644 index 00000000000..4f429b03a0d --- /dev/null +++ b/changelogs/unreleased/use-hashie-forbidden_attributes.yml @@ -0,0 +1,4 @@ +--- +title: Add hashie-forbidden_attributes gem +merge_request: 10579 +author: Andy Brown diff --git a/doc/update/README.md b/doc/update/README.md index 7921d03d611..d024a809f24 100644 --- a/doc/update/README.md +++ b/doc/update/README.md @@ -50,20 +50,17 @@ update them are in [a separate document][omnidocker]. ## Upgrading without downtime -Starting with GitLab 9.1.0 it's possible to upgrade to a newer version of GitLab +Starting with GitLab 9.1.0 it's possible to upgrade to a newer major, minor, or patch version of GitLab without having to take your GitLab instance offline. However, for this to work there are the following requirements: -1. You can only upgrade 1 release at a time. For example, if 9.1.15 is the last - release of 9.1 then you can safely upgrade from that version to 9.2.0. +1. You can only upgrade 1 minor release at a time. So from 9.1 to 9.2, not to 9.3. +2. You have to be on the most recent patch release. For example, if 9.1.15 is the last + release of 9.1 then you can safely upgrade from that version to any 9.2.x version. However, if you are running 9.1.14 you first need to upgrade to 9.1.15. 2. You have to use [post-deployment migrations](../development/post_deployment_migrations.md). -3. You are using PostgreSQL. If you are using MySQL you will still need downtime - when upgrading. - -This applies to major, minor, and patch releases unless stated otherwise in a -release post. +3. You are using PostgreSQL. If you are using MySQL please look at the release post to see if downtime is required. ## Upgrading between editions diff --git a/lib/banzai/filter/issuable_state_filter.rb b/lib/banzai/filter/issuable_state_filter.rb index 6b78aa795b4..0b2b8bd7f4d 100644 --- a/lib/banzai/filter/issuable_state_filter.rb +++ b/lib/banzai/filter/issuable_state_filter.rb @@ -13,8 +13,8 @@ module Banzai issuables = extractor.extract([doc]) issuables.each do |node, issuable| - if VISIBLE_STATES.include?(issuable.state) - node.children.last.content += " [#{issuable.state}]" + if VISIBLE_STATES.include?(issuable.state) && node.children.present? + node.add_child(Nokogiri::XML::Text.new(" [#{issuable.state}]", doc)) end end diff --git a/lib/container_registry/path.rb b/lib/container_registry/path.rb index a4b5f2aba6c..4a585996aa5 100644 --- a/lib/container_registry/path.rb +++ b/lib/container_registry/path.rb @@ -15,7 +15,7 @@ module ContainerRegistry LEVELS_SUPPORTED = 3 def initialize(path) - @path = path + @path = path.to_s.downcase end def valid? @@ -25,7 +25,7 @@ module ContainerRegistry end def components - @components ||= @path.to_s.split('/') + @components ||= @path.split('/') end def nodes diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb index 41dcf846fed..3b335cdfd01 100644 --- a/lib/gitlab/ci/trace/stream.rb +++ b/lib/gitlab/ci/trace/stream.rb @@ -25,11 +25,10 @@ module Gitlab end def limit(last_bytes = LIMIT_SIZE) - stream_size = size - if stream_size < last_bytes - last_bytes = stream_size + if last_bytes < size + stream.seek(-last_bytes, IO::SEEK_END) + stream.readline end - stream.seek(-last_bytes, IO::SEEK_END) end def append(data, offset) diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb index f98481c6d3a..6e42d8941fb 100644 --- a/lib/gitlab/o_auth/user.rb +++ b/lib/gitlab/o_auth/user.rb @@ -148,7 +148,7 @@ module Gitlab def build_new_user user_params = user_attributes.merge(extern_uid: auth_hash.uid, provider: auth_hash.provider, skip_confirmation: true) - Users::CreateService.new(nil, user_params).build + Users::BuildService.new(nil, user_params).execute end def user_attributes diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake index 9f6cfe3957c..8079c6e416c 100644 --- a/lib/tasks/gitlab/gitaly.rake +++ b/lib/tasks/gitlab/gitaly.rake @@ -7,10 +7,10 @@ namespace :gitlab do abort %(Please specify the directory where you want to install gitaly:\n rake "gitlab:gitaly:install[/home/git/gitaly]") end - tag = "v#{Gitlab::GitalyClient.expected_server_version}" + version = Gitlab::GitalyClient.expected_server_version repo = 'https://gitlab.com/gitlab-org/gitaly.git' - checkout_or_clone_tag(tag: tag, repo: repo, target_dir: args.dir) + checkout_or_clone_version(version: version, repo: repo, target_dir: args.dir) _, status = Gitlab::Popen.popen(%w[which gmake]) command = status.zero? ? 'gmake' : 'make' diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake index dd2fda54e62..95687066819 100644 --- a/lib/tasks/gitlab/shell.rake +++ b/lib/tasks/gitlab/shell.rake @@ -1,19 +1,18 @@ namespace :gitlab do namespace :shell do desc "GitLab | Install or upgrade gitlab-shell" - task :install, [:tag, :repo] => :environment do |t, args| + task :install, [:repo] => :environment do |t, args| warn_user_is_not_gitlab default_version = Gitlab::Shell.version_required - default_version_tag = "v#{default_version}" - args.with_defaults(tag: default_version_tag, repo: 'https://gitlab.com/gitlab-org/gitlab-shell.git') + args.with_defaults(repo: 'https://gitlab.com/gitlab-org/gitlab-shell.git') gitlab_url = Gitlab.config.gitlab.url # gitlab-shell requires a / at the end of the url gitlab_url += '/' unless gitlab_url.end_with?('/') target_dir = Gitlab.config.gitlab_shell.path - checkout_or_clone_tag(tag: default_version_tag, repo: args.repo, target_dir: target_dir) + checkout_or_clone_version(version: default_version, repo: args.repo, target_dir: target_dir) # Make sure we're on the right tag Dir.chdir(target_dir) do diff --git a/lib/tasks/gitlab/task_helpers.rb b/lib/tasks/gitlab/task_helpers.rb index cdba2262bc2..e3c9d3b491c 100644 --- a/lib/tasks/gitlab/task_helpers.rb +++ b/lib/tasks/gitlab/task_helpers.rb @@ -147,41 +147,30 @@ module Gitlab Rails.env.test? ? Rails.root.join('tmp/tests') : Gitlab.config.gitlab.user_home end - def checkout_or_clone_tag(tag:, repo:, target_dir:) - if Dir.exist?(target_dir) - checkout_tag(tag, target_dir) - else - clone_repo(repo, target_dir) - end + def checkout_or_clone_version(version:, repo:, target_dir:) + version = + if version.starts_with?("=") + version.sub(/\A=/, '') # tag or branch + else + "v#{version}" # tag + end - reset_to_tag(tag, target_dir) + clone_repo(repo, target_dir) unless Dir.exist?(target_dir) + checkout_version(version, target_dir) + reset_to_version(version, target_dir) end def clone_repo(repo, target_dir) run_command!(%W[#{Gitlab.config.git.bin_path} clone -- #{repo} #{target_dir}]) end - def checkout_tag(tag, target_dir) - run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} fetch --tags --quiet]) - run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} checkout --quiet #{tag}]) + def checkout_version(version, target_dir) + run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} fetch --quiet]) + run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} checkout --quiet #{version}]) end - def reset_to_tag(tag_wanted, target_dir) - tag = - begin - # First try to checkout without fetching - # to avoid stalling tests if the Internet is down. - run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} describe -- #{tag_wanted}]) - rescue Gitlab::TaskFailedError - run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} fetch origin]) - run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} describe -- origin/#{tag_wanted}]) - end - - if tag - run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} reset --hard #{tag.strip}]) - else - raise Gitlab::TaskFailedError - end + def reset_to_version(version, target_dir) + run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} reset --hard #{version}]) end end end diff --git a/lib/tasks/gitlab/workhorse.rake b/lib/tasks/gitlab/workhorse.rake index baea94bf8ca..a00b02188cf 100644 --- a/lib/tasks/gitlab/workhorse.rake +++ b/lib/tasks/gitlab/workhorse.rake @@ -7,10 +7,10 @@ namespace :gitlab do abort %(Please specify the directory where you want to install gitlab-workhorse:\n rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]") end - tag = "v#{Gitlab::Workhorse.version}" + version = Gitlab::Workhorse.version repo = 'https://gitlab.com/gitlab-org/gitlab-workhorse.git' - checkout_or_clone_tag(tag: tag, repo: repo, target_dir: args.dir) + checkout_or_clone_version(version: version, repo: repo, target_dir: args.dir) _, status = Gitlab::Popen.popen(%w[which gmake]) command = status.zero? ? 'gmake' : 'make' diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb index 88d28b649a4..0e23c3a8849 100644 --- a/spec/features/merge_requests/diff_notes_resolve_spec.rb +++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb @@ -198,6 +198,8 @@ feature 'Diff notes resolve', feature: true, js: true do it 'does not mark discussion as resolved when resolving single note' do page.first '.diff-content .note' do first('.line-resolve-btn').click + + expect(page).to have_selector('.note-action-button .loading') expect(first('.line-resolve-btn')['data-original-title']).to eq("Resolved by #{user.name}") end diff --git a/spec/fixtures/trace/ansi-sequence-and-unicode b/spec/fixtures/trace/ansi-sequence-and-unicode new file mode 100644 index 00000000000..5d2466f0d0f --- /dev/null +++ b/spec/fixtures/trace/ansi-sequence-and-unicode @@ -0,0 +1,5 @@ +[0m[01;34m.[0m +[30;42m..[0m +😺 +ヾ(´༎ຶД༎ຶ`)ノ +[01;32m許功蓋[0m diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb index fddeaaf504d..47d904b865b 100644 --- a/spec/javascripts/fixtures/merge_requests.rb +++ b/spec/javascripts/fixtures/merge_requests.rb @@ -7,6 +7,7 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} let(:project) { create(:project, namespace: namespace, path: 'merge-requests-project') } let(:merge_request) { create(:merge_request, :with_diffs, source_project: project, target_project: project, description: '- [ ] Task List Item') } + let(:merged_merge_request) { create(:merge_request, :merged, source_project: project, target_project: project) } let(:pipeline) do create( :ci_pipeline, @@ -32,6 +33,12 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont render_merge_request(example.description, merge_request) end + it 'merge_requests/merged_merge_request.html.raw' do |example| + allow_any_instance_of(MergeRequest).to receive(:source_branch_exists?).and_return(true) + allow_any_instance_of(MergeRequest).to receive(:can_remove_source_branch?).and_return(true) + render_merge_request(example.description, merged_merge_request) + end + private def render_merge_request(fixture_file_name, merge_request) diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js index 4200e943121..daef9b93fa5 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js +++ b/spec/javascripts/lib/utils/text_utility_spec.js @@ -1,110 +1,108 @@ require('~/lib/utils/text_utility'); -(() => { - describe('text_utility', () => { - describe('gl.text.getTextWidth', () => { - it('returns zero width when no text is passed', () => { - expect(gl.text.getTextWidth('')).toBe(0); - }); +describe('text_utility', () => { + describe('gl.text.getTextWidth', () => { + it('returns zero width when no text is passed', () => { + expect(gl.text.getTextWidth('')).toBe(0); + }); - it('returns zero width when no text is passed and font is passed', () => { - expect(gl.text.getTextWidth('', '100px sans-serif')).toBe(0); - }); + it('returns zero width when no text is passed and font is passed', () => { + expect(gl.text.getTextWidth('', '100px sans-serif')).toBe(0); + }); - it('returns width when text is passed', () => { - expect(gl.text.getTextWidth('foo') > 0).toBe(true); - }); + it('returns width when text is passed', () => { + expect(gl.text.getTextWidth('foo') > 0).toBe(true); + }); - it('returns bigger width when font is larger', () => { - const largeFont = gl.text.getTextWidth('foo', '100px sans-serif'); - const regular = gl.text.getTextWidth('foo', '10px sans-serif'); - expect(largeFont > regular).toBe(true); - }); + it('returns bigger width when font is larger', () => { + const largeFont = gl.text.getTextWidth('foo', '100px sans-serif'); + const regular = gl.text.getTextWidth('foo', '10px sans-serif'); + expect(largeFont > regular).toBe(true); }); + }); - describe('gl.text.pluralize', () => { - it('returns pluralized', () => { - expect(gl.text.pluralize('test', 2)).toBe('tests'); - }); + describe('gl.text.pluralize', () => { + it('returns pluralized', () => { + expect(gl.text.pluralize('test', 2)).toBe('tests'); + }); - it('returns pluralized when count is 0', () => { - expect(gl.text.pluralize('test', 0)).toBe('tests'); - }); + it('returns pluralized when count is 0', () => { + expect(gl.text.pluralize('test', 0)).toBe('tests'); + }); - it('does not return pluralized', () => { - expect(gl.text.pluralize('test', 1)).toBe('test'); - }); + it('does not return pluralized', () => { + expect(gl.text.pluralize('test', 1)).toBe('test'); }); + }); - describe('gl.text.highCountTrim', () => { - it('returns 99+ for count >= 100', () => { - expect(gl.text.highCountTrim(105)).toBe('99+'); - expect(gl.text.highCountTrim(100)).toBe('99+'); - }); + describe('gl.text.highCountTrim', () => { + it('returns 99+ for count >= 100', () => { + expect(gl.text.highCountTrim(105)).toBe('99+'); + expect(gl.text.highCountTrim(100)).toBe('99+'); + }); - it('returns exact number for count < 100', () => { - expect(gl.text.highCountTrim(45)).toBe(45); - }); + it('returns exact number for count < 100', () => { + expect(gl.text.highCountTrim(45)).toBe(45); }); + }); - describe('gl.text.insertText', () => { - let textArea; + describe('gl.text.insertText', () => { + let textArea; - beforeAll(() => { - textArea = document.createElement('textarea'); - document.querySelector('body').appendChild(textArea); - }); + beforeAll(() => { + textArea = document.createElement('textarea'); + document.querySelector('body').appendChild(textArea); + }); - afterAll(() => { - textArea.parentNode.removeChild(textArea); - }); + afterAll(() => { + textArea.parentNode.removeChild(textArea); + }); - describe('without selection', () => { - it('inserts the tag on an empty line', () => { - const initialValue = ''; + describe('without selection', () => { + it('inserts the tag on an empty line', () => { + const initialValue = ''; - textArea.value = initialValue; - textArea.selectionStart = 0; - textArea.selectionEnd = 0; + textArea.value = initialValue; + textArea.selectionStart = 0; + textArea.selectionEnd = 0; - gl.text.insertText(textArea, textArea.value, '*', null, '', false); + gl.text.insertText(textArea, textArea.value, '*', null, '', false); - expect(textArea.value).toEqual(`${initialValue}* `); - }); + expect(textArea.value).toEqual(`${initialValue}* `); + }); - it('inserts the tag on a new line if the current one is not empty', () => { - const initialValue = 'some text'; + it('inserts the tag on a new line if the current one is not empty', () => { + const initialValue = 'some text'; - textArea.value = initialValue; - textArea.setSelectionRange(initialValue.length, initialValue.length); + textArea.value = initialValue; + textArea.setSelectionRange(initialValue.length, initialValue.length); - gl.text.insertText(textArea, textArea.value, '*', null, '', false); + gl.text.insertText(textArea, textArea.value, '*', null, '', false); - expect(textArea.value).toEqual(`${initialValue}\n* `); - }); + expect(textArea.value).toEqual(`${initialValue}\n* `); + }); - it('inserts the tag on the same line if the current line only contains spaces', () => { - const initialValue = ' '; + it('inserts the tag on the same line if the current line only contains spaces', () => { + const initialValue = ' '; - textArea.value = initialValue; - textArea.setSelectionRange(initialValue.length, initialValue.length); + textArea.value = initialValue; + textArea.setSelectionRange(initialValue.length, initialValue.length); - gl.text.insertText(textArea, textArea.value, '*', null, '', false); + gl.text.insertText(textArea, textArea.value, '*', null, '', false); - expect(textArea.value).toEqual(`${initialValue}* `); - }); + expect(textArea.value).toEqual(`${initialValue}* `); + }); - it('inserts the tag on the same line if the current line only contains tabs', () => { - const initialValue = '\t\t\t'; + it('inserts the tag on the same line if the current line only contains tabs', () => { + const initialValue = '\t\t\t'; - textArea.value = initialValue; - textArea.setSelectionRange(initialValue.length, initialValue.length); + textArea.value = initialValue; + textArea.setSelectionRange(initialValue.length, initialValue.length); - gl.text.insertText(textArea, textArea.value, '*', null, '', false); + gl.text.insertText(textArea, textArea.value, '*', null, '', false); - expect(textArea.value).toEqual(`${initialValue}* `); - }); + expect(textArea.value).toEqual(`${initialValue}* `); }); }); }); -})(); +}); diff --git a/spec/javascripts/merged_buttons_spec.js b/spec/javascripts/merged_buttons_spec.js new file mode 100644 index 00000000000..b5c5e60dd97 --- /dev/null +++ b/spec/javascripts/merged_buttons_spec.js @@ -0,0 +1,44 @@ +/* global MergedButtons */ + +import '~/merged_buttons'; + +describe('MergedButtons', () => { + const fixturesPath = 'merge_requests/merged_merge_request.html.raw'; + preloadFixtures(fixturesPath); + + beforeEach(() => { + loadFixtures(fixturesPath); + this.mergedButtons = new MergedButtons(); + this.$removeBranchWidget = $('.remove_source_branch_widget:not(.failed)'); + this.$removeBranchProgress = $('.remove_source_branch_in_progress'); + this.$removeBranchFailed = $('.remove_source_branch_widget.failed'); + this.$removeBranchButton = $('.remove_source_branch'); + }); + + describe('removeSourceBranch', () => { + it('shows loader', () => { + $('.remove_source_branch').trigger('click'); + expect(this.$removeBranchProgress).toBeVisible(); + expect(this.$removeBranchWidget).not.toBeVisible(); + }); + }); + + describe('removeBranchSuccess', () => { + it('refreshes page when branch removed', () => { + spyOn(gl.utils, 'refreshCurrentPage').and.stub(); + const response = { status: 200 }; + this.$removeBranchButton.trigger('ajax:success', response, 'xhr'); + expect(gl.utils.refreshCurrentPage).toHaveBeenCalled(); + }); + }); + + describe('removeBranchError', () => { + it('shows error message', () => { + const response = { status: 500 }; + this.$removeBranchButton.trigger('ajax:error', response, 'xhr'); + expect(this.$removeBranchFailed).toBeVisible(); + expect(this.$removeBranchProgress).not.toBeVisible(); + expect(this.$removeBranchWidget).not.toBeVisible(); + }); + }); +}); diff --git a/spec/lib/banzai/filter/issuable_state_filter_spec.rb b/spec/lib/banzai/filter/issuable_state_filter_spec.rb index 603b79a323c..5cb98163746 100644 --- a/spec/lib/banzai/filter/issuable_state_filter_spec.rb +++ b/spec/lib/banzai/filter/issuable_state_filter_spec.rb @@ -6,8 +6,8 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do let(:user) { create(:user) } - def create_link(data) - link_to('text', '', class: 'gfm has-tooltip', data: data) + def create_link(text, data) + link_to(text, '', class: 'gfm has-tooltip', data: data) end it 'ignores non-GFM links' do @@ -19,16 +19,37 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do it 'ignores non-issuable links' do project = create(:empty_project, :public) - link = create_link(project: project, reference_type: 'issue') + link = create_link('text', project: project, reference_type: 'issue') doc = filter(link, current_user: user) expect(doc.css('a').last.text).to eq('text') end + it 'ignores issuable links with empty content' do + issue = create(:issue, :closed) + link = create_link('', issue: issue.id, reference_type: 'issue') + doc = filter(link, current_user: user) + + expect(doc.css('a').last.text).to eq('') + end + + it 'adds text with standard formatting' do + issue = create(:issue, :closed) + link = create_link( + 'something <strong>else</strong>'.html_safe, + issue: issue.id, + reference_type: 'issue' + ) + doc = filter(link, current_user: user) + + expect(doc.css('a').last.inner_html). + to eq('something <strong>else</strong> [closed]') + end + context 'for issue references' do it 'ignores open issue references' do issue = create(:issue) - link = create_link(issue: issue.id, reference_type: 'issue') + link = create_link('text', issue: issue.id, reference_type: 'issue') doc = filter(link, current_user: user) expect(doc.css('a').last.text).to eq('text') @@ -36,7 +57,7 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do it 'ignores reopened issue references' do reopened_issue = create(:issue, :reopened) - link = create_link(issue: reopened_issue.id, reference_type: 'issue') + link = create_link('text', issue: reopened_issue.id, reference_type: 'issue') doc = filter(link, current_user: user) expect(doc.css('a').last.text).to eq('text') @@ -44,7 +65,7 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do it 'appends [closed] to closed issue references' do closed_issue = create(:issue, :closed) - link = create_link(issue: closed_issue.id, reference_type: 'issue') + link = create_link('text', issue: closed_issue.id, reference_type: 'issue') doc = filter(link, current_user: user) expect(doc.css('a').last.text).to eq('text [closed]') @@ -54,7 +75,7 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do context 'for merge request references' do it 'ignores open merge request references' do mr = create(:merge_request) - link = create_link(merge_request: mr.id, reference_type: 'merge_request') + link = create_link('text', merge_request: mr.id, reference_type: 'merge_request') doc = filter(link, current_user: user) expect(doc.css('a').last.text).to eq('text') @@ -62,7 +83,7 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do it 'ignores reopened merge request references' do mr = create(:merge_request, :reopened) - link = create_link(merge_request: mr.id, reference_type: 'merge_request') + link = create_link('text', merge_request: mr.id, reference_type: 'merge_request') doc = filter(link, current_user: user) expect(doc.css('a').last.text).to eq('text') @@ -70,7 +91,7 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do it 'ignores locked merge request references' do mr = create(:merge_request, :locked) - link = create_link(merge_request: mr.id, reference_type: 'merge_request') + link = create_link('text', merge_request: mr.id, reference_type: 'merge_request') doc = filter(link, current_user: user) expect(doc.css('a').last.text).to eq('text') @@ -78,7 +99,7 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do it 'appends [closed] to closed merge request references' do mr = create(:merge_request, :closed) - link = create_link(merge_request: mr.id, reference_type: 'merge_request') + link = create_link('text', merge_request: mr.id, reference_type: 'merge_request') doc = filter(link, current_user: user) expect(doc.css('a').last.text).to eq('text [closed]') @@ -86,7 +107,7 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do it 'appends [merged] to merged merge request references' do mr = create(:merge_request, :merged) - link = create_link(merge_request: mr.id, reference_type: 'merge_request') + link = create_link('text', merge_request: mr.id, reference_type: 'merge_request') doc = filter(link, current_user: user) expect(doc.css('a').last.text).to eq('text [merged]') diff --git a/spec/lib/container_registry/path_spec.rb b/spec/lib/container_registry/path_spec.rb index b9c4572c269..f3b3a9a715f 100644 --- a/spec/lib/container_registry/path_spec.rb +++ b/spec/lib/container_registry/path_spec.rb @@ -33,10 +33,20 @@ describe ContainerRegistry::Path do end describe '#to_s' do - let(:path) { 'some/image' } + context 'when path does not have uppercase characters' do + let(:path) { 'some/image' } - it 'return a string with a repository path' do - expect(subject.to_s).to eq path + it 'return a string with a repository path' do + expect(subject.to_s).to eq 'some/image' + end + end + + context 'when path has uppercase characters' do + let(:path) { 'SoMe/ImAgE' } + + it 'return a string with a repository path' do + expect(subject.to_s).to eq 'some/image' + end end end @@ -70,6 +80,12 @@ describe ContainerRegistry::Path do it { is_expected.to be_valid } end + + context 'when path contains uppercase letters' do + let(:path) { 'Some/Registry' } + + it { is_expected.to be_valid } + end end describe '#has_repository?' do diff --git a/spec/lib/gitlab/ci/trace/stream_spec.rb b/spec/lib/gitlab/ci/trace/stream_spec.rb index 2e57ccef182..9e3bd6d662f 100644 --- a/spec/lib/gitlab/ci/trace/stream_spec.rb +++ b/spec/lib/gitlab/ci/trace/stream_spec.rb @@ -17,12 +17,12 @@ describe Gitlab::Ci::Trace::Stream do describe '#limit' do let(:stream) do described_class.new do - StringIO.new("12345678") + StringIO.new((1..8).to_a.join("\n")) end end - it 'if size is larger we start from beggining' do - stream.limit(10) + it 'if size is larger we start from beginning' do + stream.limit(20) expect(stream.tell).to eq(0) end @@ -30,7 +30,27 @@ describe Gitlab::Ci::Trace::Stream do it 'if size is smaller we start from the end' do stream.limit(2) - expect(stream.tell).to eq(6) + expect(stream.raw).to eq("8") + end + + context 'when the trace contains ANSI sequence and Unicode' do + let(:stream) do + described_class.new do + File.open(expand_fixture_path('trace/ansi-sequence-and-unicode')) + end + end + + it 'forwards to the next linefeed, case 1' do + stream.limit(7) + + expect(stream.raw).to eq('') + end + + it 'forwards to the next linefeed, case 2' do + stream.limit(29) + + expect(stream.raw).to eq("\e[01;32m許功蓋\e[0m\n") + end end end diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb index 6d6c9f2adfc..eff41d85972 100644 --- a/spec/models/container_repository_spec.rb +++ b/spec/models/container_repository_spec.rb @@ -34,8 +34,18 @@ describe ContainerRepository do end describe '#path' do - it 'returns a full path to the repository' do - expect(repository.path).to eq('group/test/my_image') + context 'when project path does not contain uppercase letters' do + it 'returns a full path to the repository' do + expect(repository.path).to eq('group/test/my_image') + end + end + + context 'when path contains uppercase letters' do + let(:project) { create(:project, path: 'MY_PROJECT', group: group) } + + it 'returns a full path without capital letters' do + expect(repository.path).to eq('group/my_project/my_image') + end end end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 62f21049b0b..7a07ea618c0 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -144,6 +144,20 @@ describe Projects::CreateService, '#execute', services: true do end end + context 'when a bad service template is created' do + before do + create(:service, type: 'DroneCiService', project: nil, template: true, active: true) + end + + it 'reports an error in the imported project' do + opts[:import_url] = 'http://www.gitlab.com/gitlab-org/gitlab-ce' + project = create_project(user, opts) + + expect(project.errors.full_messages_for(:base).first).to match /Unable to save project. Error: Unable to save DroneCiService/ + expect(project.services.count).to eq 0 + end + end + def create_project(user, opts) Projects::CreateService.new(user, opts).execute end diff --git a/spec/services/users/build_service_spec.rb b/spec/services/users/build_service_spec.rb new file mode 100644 index 00000000000..2a6bfc1b3a0 --- /dev/null +++ b/spec/services/users/build_service_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe Users::BuildService, services: true do + describe '#execute' do + let(:params) do + { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass' } + end + + context 'with an admin user' do + let(:admin_user) { create(:admin) } + let(:service) { described_class.new(admin_user, params) } + + it 'returns a valid user' do + expect(service.execute).to be_valid + end + end + + context 'with non admin user' do + let(:user) { create(:user) } + let(:service) { described_class.new(user, params) } + + it 'raises AccessDeniedError exception' do + expect { service.execute }.to raise_error Gitlab::Access::AccessDeniedError + end + end + + context 'with nil user' do + let(:service) { described_class.new(nil, params) } + + it 'returns a valid user' do + expect(service.execute).to be_valid + end + + context 'when "send_user_confirmation_email" application setting is true' do + before do + stub_application_setting(send_user_confirmation_email: true, signup_enabled?: true) + end + + it 'does not confirm the user' do + expect(service.execute).not_to be_confirmed + end + end + + context 'when "send_user_confirmation_email" application setting is false' do + before do + stub_application_setting(send_user_confirmation_email: false, signup_enabled?: true) + end + + it 'confirms the user' do + expect(service.execute).to be_confirmed + end + end + end + end +end diff --git a/spec/services/users/create_service_spec.rb b/spec/services/users/create_service_spec.rb index a111aec2f89..75746278573 100644 --- a/spec/services/users/create_service_spec.rb +++ b/spec/services/users/create_service_spec.rb @@ -1,38 +1,6 @@ require 'spec_helper' describe Users::CreateService, services: true do - describe '#build' do - let(:params) do - { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass' } - end - - context 'with an admin user' do - let(:admin_user) { create(:admin) } - let(:service) { described_class.new(admin_user, params) } - - it 'returns a valid user' do - expect(service.build).to be_valid - end - end - - context 'with non admin user' do - let(:user) { create(:user) } - let(:service) { described_class.new(user, params) } - - it 'raises AccessDeniedError exception' do - expect { service.build }.to raise_error Gitlab::Access::AccessDeniedError - end - end - - context 'with nil user' do - let(:service) { described_class.new(nil, params) } - - it 'returns a valid user' do - expect(service.build).to be_valid - end - end - end - describe '#execute' do let(:admin_user) { create(:admin) } @@ -185,40 +153,18 @@ describe Users::CreateService, services: true do end let(:service) { described_class.new(nil, params) } - context 'when "send_user_confirmation_email" application setting is true' do - before do - current_application_settings = double(:current_application_settings, send_user_confirmation_email: true, signup_enabled?: true) - allow(service).to receive(:current_application_settings).and_return(current_application_settings) - end - - it 'does not confirm the user' do - expect(service.execute).not_to be_confirmed - end - end - - context 'when "send_user_confirmation_email" application setting is false' do - before do - current_application_settings = double(:current_application_settings, send_user_confirmation_email: false, signup_enabled?: true) - allow(service).to receive(:current_application_settings).and_return(current_application_settings) - end - - it 'confirms the user' do - expect(service.execute).to be_confirmed - end - - it 'persists the given attributes' do - user = service.execute - user.reload - - expect(user).to have_attributes( - name: params[:name], - username: params[:username], - email: params[:email], - password: params[:password], - created_by_id: nil, - admin: false - ) - end + it 'persists the given attributes' do + user = service.execute + user.reload + + expect(user).to have_attributes( + name: params[:name], + username: params[:username], + email: params[:email], + password: params[:password], + created_by_id: nil, + admin: false + ) end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a3665795452..e67ad8f3455 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -9,8 +9,14 @@ require 'rspec/rails' require 'shoulda/matchers' require 'rspec/retry' -if (ENV['RSPEC_PROFILING_POSTGRES_URL'] || ENV['RSPEC_PROFILING']) && - (!ENV.has_key?('CI') || ENV['CI_COMMIT_REF_NAME'] == 'master') +rspec_profiling_is_configured = + ENV['RSPEC_PROFILING_POSTGRES_URL'] || + ENV['RSPEC_PROFILING'] +branch_can_be_profiled = + ENV['CI_COMMIT_REF_NAME'] == 'master' || + ENV['CI_COMMIT_REF_NAME'] =~ /rspec-profile/ + +if rspec_profiling_is_configured && (!ENV.key?('CI') || branch_can_be_profiled) require 'rspec_profiling/rspec' end diff --git a/spec/support/fixture_helpers.rb b/spec/support/fixture_helpers.rb index a05c9d18002..5515c355cea 100644 --- a/spec/support/fixture_helpers.rb +++ b/spec/support/fixture_helpers.rb @@ -1,8 +1,11 @@ module FixtureHelpers def fixture_file(filename) return '' if filename.blank? - file_path = File.expand_path(Rails.root.join('spec/fixtures/', filename)) - File.read(file_path) + File.read(expand_fixture_path(filename)) + end + + def expand_fixture_path(filename) + File.expand_path(Rails.root.join('spec/fixtures/', filename)) end end diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb index b369dcbb305..aaf998a546f 100644 --- a/spec/tasks/gitlab/gitaly_rake_spec.rb +++ b/spec/tasks/gitlab/gitaly_rake_spec.rb @@ -8,7 +8,7 @@ describe 'gitlab:gitaly namespace rake task' do describe 'install' do let(:repo) { 'https://gitlab.com/gitlab-org/gitaly.git' } let(:clone_path) { Rails.root.join('tmp/tests/gitaly').to_s } - let(:tag) { "v#{File.read(Rails.root.join(Gitlab::GitalyClient::SERVER_VERSION_FILE)).chomp}" } + let(:version) { File.read(Rails.root.join(Gitlab::GitalyClient::SERVER_VERSION_FILE)).chomp } context 'no dir given' do it 'aborts and display a help message' do @@ -21,7 +21,7 @@ describe 'gitlab:gitaly namespace rake task' do context 'when an underlying Git command fail' do it 'aborts and display a help message' do expect_any_instance_of(Object). - to receive(:checkout_or_clone_tag).and_raise 'Git error' + to receive(:checkout_or_clone_version).and_raise 'Git error' expect { run_rake_task('gitlab:gitaly:install', clone_path) }.to raise_error 'Git error' end @@ -32,9 +32,9 @@ describe 'gitlab:gitaly namespace rake task' do expect(Dir).to receive(:chdir).with(clone_path) end - it 'calls checkout_or_clone_tag with the right arguments' do + it 'calls checkout_or_clone_version with the right arguments' do expect_any_instance_of(Object). - to receive(:checkout_or_clone_tag).with(tag: tag, repo: repo, target_dir: clone_path) + to receive(:checkout_or_clone_version).with(version: version, repo: repo, target_dir: clone_path) run_rake_task('gitlab:gitaly:install', clone_path) end @@ -48,7 +48,7 @@ describe 'gitlab:gitaly namespace rake task' do context 'gmake is available' do before do - expect_any_instance_of(Object).to receive(:checkout_or_clone_tag) + expect_any_instance_of(Object).to receive(:checkout_or_clone_version) allow_any_instance_of(Object).to receive(:run_command!).with(['gmake']).and_return(true) end @@ -62,7 +62,7 @@ describe 'gitlab:gitaly namespace rake task' do context 'gmake is not available' do before do - expect_any_instance_of(Object).to receive(:checkout_or_clone_tag) + expect_any_instance_of(Object).to receive(:checkout_or_clone_version) allow_any_instance_of(Object).to receive(:run_command!).with(['make']).and_return(true) end diff --git a/spec/tasks/gitlab/task_helpers_spec.rb b/spec/tasks/gitlab/task_helpers_spec.rb index 86e42d845ce..3d9ba7cdc6f 100644 --- a/spec/tasks/gitlab/task_helpers_spec.rb +++ b/spec/tasks/gitlab/task_helpers_spec.rb @@ -10,19 +10,38 @@ describe Gitlab::TaskHelpers do let(:repo) { 'https://gitlab.com/gitlab-org/gitlab-test.git' } let(:clone_path) { Rails.root.join('tmp/tests/task_helpers_tests').to_s } + let(:version) { '1.1.0' } let(:tag) { 'v1.1.0' } - describe '#checkout_or_clone_tag' do + describe '#checkout_or_clone_version' do before do allow(subject).to receive(:run_command!) - expect(subject).to receive(:reset_to_tag).with(tag, clone_path) end - context 'target_dir does not exist' do - it 'clones the repo, retrieve the tag from origin, and checkout the tag' do + it 'checkout the version and reset to it' do + expect(subject).to receive(:checkout_version).with(tag, clone_path) + expect(subject).to receive(:reset_to_version).with(tag, clone_path) + + subject.checkout_or_clone_version(version: version, repo: repo, target_dir: clone_path) + end + + context 'with a branch version' do + let(:version) { '=branch_name' } + let(:branch) { 'branch_name' } + + it 'checkout the version and reset to it with a branch name' do + expect(subject).to receive(:checkout_version).with(branch, clone_path) + expect(subject).to receive(:reset_to_version).with(branch, clone_path) + + subject.checkout_or_clone_version(version: version, repo: repo, target_dir: clone_path) + end + end + + context "target_dir doesn't exist" do + it 'clones the repo' do expect(subject).to receive(:clone_repo).with(repo, clone_path) - subject.checkout_or_clone_tag(tag: tag, repo: repo, target_dir: clone_path) + subject.checkout_or_clone_version(version: version, repo: repo, target_dir: clone_path) end end @@ -31,10 +50,10 @@ describe Gitlab::TaskHelpers do expect(Dir).to receive(:exist?).and_return(true) end - it 'fetch and checkout the tag' do - expect(subject).to receive(:checkout_tag).with(tag, clone_path) + it "doesn't clone the repository" do + expect(subject).not_to receive(:clone_repo) - subject.checkout_or_clone_tag(tag: tag, repo: repo, target_dir: clone_path) + subject.checkout_or_clone_version(version: version, repo: repo, target_dir: clone_path) end end end @@ -48,49 +67,23 @@ describe Gitlab::TaskHelpers do end end - describe '#checkout_tag' do + describe '#checkout_version' do it 'clones the repo in the target dir' do expect(subject). - to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} fetch --tags --quiet]) + to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} fetch --quiet]) expect(subject). to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} checkout --quiet #{tag}]) - subject.checkout_tag(tag, clone_path) + subject.checkout_version(tag, clone_path) end end - describe '#reset_to_tag' do - let(:tag) { 'v1.1.0' } - before do + describe '#reset_to_version' do + it 'resets --hard to the given version' do expect(subject). to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} reset --hard #{tag}]) - end - context 'when the tag is not checked out locally' do - before do - expect(subject). - to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} describe -- #{tag}]).and_raise(Gitlab::TaskFailedError) - end - - it 'fetch origin, ensure the tag exists, and resets --hard to the given tag' do - expect(subject). - to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} fetch origin]) - expect(subject). - to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} describe -- origin/#{tag}]).and_return(tag) - - subject.reset_to_tag(tag, clone_path) - end - end - - context 'when the tag is checked out locally' do - before do - expect(subject). - to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} describe -- #{tag}]).and_return(tag) - end - - it 'resets --hard to the given tag' do - subject.reset_to_tag(tag, clone_path) - end + subject.reset_to_version(tag, clone_path) end end end diff --git a/spec/tasks/gitlab/workhorse_rake_spec.rb b/spec/tasks/gitlab/workhorse_rake_spec.rb index 8a66a4aa047..63d1cf2bbe5 100644 --- a/spec/tasks/gitlab/workhorse_rake_spec.rb +++ b/spec/tasks/gitlab/workhorse_rake_spec.rb @@ -8,7 +8,7 @@ describe 'gitlab:workhorse namespace rake task' do describe 'install' do let(:repo) { 'https://gitlab.com/gitlab-org/gitlab-workhorse.git' } let(:clone_path) { Rails.root.join('tmp/tests/gitlab-workhorse').to_s } - let(:tag) { "v#{File.read(Rails.root.join(Gitlab::Workhorse::VERSION_FILE)).chomp}" } + let(:version) { File.read(Rails.root.join(Gitlab::Workhorse::VERSION_FILE)).chomp } context 'no dir given' do it 'aborts and display a help message' do @@ -21,7 +21,7 @@ describe 'gitlab:workhorse namespace rake task' do context 'when an underlying Git command fail' do it 'aborts and display a help message' do expect_any_instance_of(Object). - to receive(:checkout_or_clone_tag).and_raise 'Git error' + to receive(:checkout_or_clone_version).and_raise 'Git error' expect { run_rake_task('gitlab:workhorse:install', clone_path) }.to raise_error 'Git error' end @@ -32,9 +32,9 @@ describe 'gitlab:workhorse namespace rake task' do expect(Dir).to receive(:chdir).with(clone_path) end - it 'calls checkout_or_clone_tag with the right arguments' do + it 'calls checkout_or_clone_version with the right arguments' do expect_any_instance_of(Object). - to receive(:checkout_or_clone_tag).with(tag: tag, repo: repo, target_dir: clone_path) + to receive(:checkout_or_clone_version).with(version: version, repo: repo, target_dir: clone_path) run_rake_task('gitlab:workhorse:install', clone_path) end @@ -48,7 +48,7 @@ describe 'gitlab:workhorse namespace rake task' do context 'gmake is available' do before do - expect_any_instance_of(Object).to receive(:checkout_or_clone_tag) + expect_any_instance_of(Object).to receive(:checkout_or_clone_version) allow_any_instance_of(Object).to receive(:run_command!).with(['gmake']).and_return(true) end @@ -62,7 +62,7 @@ describe 'gitlab:workhorse namespace rake task' do context 'gmake is not available' do before do - expect_any_instance_of(Object).to receive(:checkout_or_clone_tag) + expect_any_instance_of(Object).to receive(:checkout_or_clone_version) allow_any_instance_of(Object).to receive(:run_command!).with(['make']).and_return(true) end diff --git a/vendor/assets/javascripts/notebooklab.js b/vendor/assets/javascripts/notebooklab.js index 601a645b655..b8cfdc53b48 100644 --- a/vendor/assets/javascripts/notebooklab.js +++ b/vendor/assets/javascripts/notebooklab.js @@ -699,6 +699,48 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de // // +var renderer = new _marked2.default.Renderer(); + +/* + Regex to match KaTex blocks. + + Supports the following: + + \begin{equation}<math>\end{equation} + $$<math>$$ + inline $<math>$ + + The matched text then goes through the KaTex renderer & then outputs the HTML +*/ +var katexRegexString = '(\n ^\\\\begin{[a-zA-Z]+}\\s\n |\n ^\\$\\$\n |\n \\s\\$(?!\\$)\n)\n (.+?)\n(\n \\s\\\\end{[a-zA-Z]+}$\n |\n \\$\\$$\n |\n \\$\n)\n'.replace(/\s/g, '').trim(); + +renderer.paragraph = function (t) { + var text = t; + var inline = false; + + if (typeof katex !== 'undefined') { + var katexString = text.replace(/\\/g, '\\'); + var matches = new RegExp(katexRegexString, 'gi').exec(katexString); + + if (matches && matches.length > 0) { + if (matches[1].trim() === '$' && matches[3].trim() === '$') { + inline = true; + + text = katexString.replace(matches[0], '') + ' ' + katex.renderToString(matches[2]); + } else { + text = katex.renderToString(matches[2]); + } + } + } + + return '<p class="' + (inline ? 'inline-katex' : '') + '">' + text + '</p>'; +}; + +_marked2.default.setOptions({ + sanitize: true, + renderer: renderer +}); + exports.default = { components: { prompt: _prompt2.default @@ -711,20 +753,7 @@ exports.default = { }, computed: { markdown: function markdown() { - var regex = new RegExp('^\\$\\$(.*)\\$\\$$', 'g'); - - var source = this.cell.source.map(function (line) { - var matches = regex.exec(line.trim()); - - // Only render use the Katex library if it is actually loaded - if (matches && matches.length > 0 && typeof katex !== 'undefined') { - return katex.renderToString(matches[1]); - } - - return line; - }); - - return (0, _marked2.default)(source.join('')); + return (0, _marked2.default)(this.cell.source.join('')); } } }; @@ -3047,7 +3076,7 @@ exports = module.exports = __webpack_require__(1)(undefined); // module -exports.push([module.i, ".markdown .katex{display:block;text-align:center}", ""]); +exports.push([module.i, ".markdown .katex{display:block;text-align:center}.markdown .inline-katex .katex{display:inline;text-align:initial}", ""]); // exports |