diff options
Diffstat (limited to 'app/assets/javascripts')
139 files changed, 2096 insertions, 1524 deletions
diff --git a/app/assets/javascripts/ajax_loading_spinner.js b/app/assets/javascripts/ajax_loading_spinner.js index bd08308904c..54e86f329e4 100644 --- a/app/assets/javascripts/ajax_loading_spinner.js +++ b/app/assets/javascripts/ajax_loading_spinner.js @@ -26,7 +26,7 @@ export default class AjaxLoadingSpinner { } static toggleLoadingIcon(iconElement) { - const classList = iconElement.classList; + const { classList } = iconElement; classList.toggle(iconElement.dataset.icon); classList.toggle('fa-spinner'); classList.toggle('fa-spin'); diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 000938e475f..d62fae99c6b 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -243,6 +243,15 @@ const Api = { }); }, + createBranch(id, { ref, branch }) { + const url = Api.buildUrl(this.createBranchPath).replace(':id', encodeURIComponent(id)); + + return axios.post(url, { + ref, + branch, + }); + }, + buildUrl(url) { let urlRoot = ''; if (gon.relative_url_root != null) { diff --git a/app/assets/javascripts/behaviors/copy_to_clipboard.js b/app/assets/javascripts/behaviors/copy_to_clipboard.js index 75834ba351d..00419e80cbb 100644 --- a/app/assets/javascripts/behaviors/copy_to_clipboard.js +++ b/app/assets/javascripts/behaviors/copy_to_clipboard.js @@ -52,7 +52,7 @@ export default function initCopyToClipboard() { * data types to the intended values. */ $(document).on('copy', 'body > textarea[readonly]', (e) => { - const clipboardData = e.originalEvent.clipboardData; + const { clipboardData } = e.originalEvent; if (!clipboardData) return; const text = e.target.value; diff --git a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js index 9745e37acce..5d7a3bed301 100644 --- a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js @@ -321,7 +321,7 @@ export class CopyAsGFM { } static copyAsGFM(e, transformer) { - const clipboardData = e.originalEvent.clipboardData; + const { clipboardData } = e.originalEvent; if (!clipboardData) return; const documentFragment = getSelectedFragment(); @@ -338,7 +338,7 @@ export class CopyAsGFM { } static pasteGFM(e) { - const clipboardData = e.originalEvent.clipboardData; + const { clipboardData } = e.originalEvent; if (!clipboardData) return; const text = clipboardData.getData('text/plain'); diff --git a/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js index 766039404ce..7986287f7e7 100644 --- a/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js +++ b/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js @@ -84,7 +84,7 @@ class BalsamiqViewer { renderTemplate(preview) { const resource = this.getResource(preview.resourceID); const name = BalsamiqViewer.parseTitle(resource); - const image = preview.image; + const { image } = preview; const template = PREVIEW_TEMPLATE({ name, diff --git a/app/assets/javascripts/blob/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq_viewer.js index 06ef86ecb77..b88e69a07bf 100644 --- a/app/assets/javascripts/blob/balsamiq_viewer.js +++ b/app/assets/javascripts/blob/balsamiq_viewer.js @@ -12,7 +12,7 @@ export default function loadBalsamiqFile() { if (!(viewer instanceof Element)) return; - const endpoint = viewer.dataset.endpoint; + const { endpoint } = viewer.dataset; const balsamiqViewer = new BalsamiqViewer(viewer); balsamiqViewer.loadFile(endpoint).catch(onError); diff --git a/app/assets/javascripts/blob/stl_viewer.js b/app/assets/javascripts/blob/stl_viewer.js index 63236b6477f..339906adc34 100644 --- a/app/assets/javascripts/blob/stl_viewer.js +++ b/app/assets/javascripts/blob/stl_viewer.js @@ -5,7 +5,7 @@ export default () => { [].slice.call(document.querySelectorAll('.js-material-changer')).forEach((el) => { el.addEventListener('click', (e) => { - const target = e.target; + const { target } = e; e.preventDefault(); diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index b7d3574bc80..0398102ad02 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -1,78 +1,78 @@ <script> -/* eslint-disable vue/require-default-prop */ -import './issue_card_inner'; -import eventHub from '../eventhub'; + /* eslint-disable vue/require-default-prop */ + import IssueCardInner from './issue_card_inner.vue'; + import eventHub from '../eventhub'; -const Store = gl.issueBoards.BoardsStore; + const Store = gl.issueBoards.BoardsStore; -export default { - name: 'BoardsIssueCard', - components: { - 'issue-card-inner': gl.issueBoards.IssueCardInner, - }, - props: { - list: { - type: Object, - default: () => ({}), + export default { + name: 'BoardsIssueCard', + components: { + IssueCardInner, }, - issue: { - type: Object, - default: () => ({}), + props: { + list: { + type: Object, + default: () => ({}), + }, + issue: { + type: Object, + default: () => ({}), + }, + issueLinkBase: { + type: String, + default: '', + }, + disabled: { + type: Boolean, + default: false, + }, + index: { + type: Number, + default: 0, + }, + rootPath: { + type: String, + default: '', + }, + groupId: { + type: Number, + }, }, - issueLinkBase: { - type: String, - default: '', + data() { + return { + showDetail: false, + detailIssue: Store.detail, + }; }, - disabled: { - type: Boolean, - default: false, + computed: { + issueDetailVisible() { + return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id; + }, }, - index: { - type: Number, - default: 0, - }, - rootPath: { - type: String, - default: '', - }, - groupId: { - type: Number, - }, - }, - data() { - return { - showDetail: false, - detailIssue: Store.detail, - }; - }, - computed: { - issueDetailVisible() { - return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id; - }, - }, - methods: { - mouseDown() { - this.showDetail = true; - }, - mouseMove() { - this.showDetail = false; - }, - showIssue(e) { - if (e.target.classList.contains('js-no-trigger')) return; - - if (this.showDetail) { + methods: { + mouseDown() { + this.showDetail = true; + }, + mouseMove() { this.showDetail = false; + }, + showIssue(e) { + if (e.target.classList.contains('js-no-trigger')) return; + + if (this.showDetail) { + this.showDetail = false; - if (Store.detail.issue && Store.detail.issue.id === this.issue.id) { - eventHub.$emit('clearDetailIssue'); - } else { - eventHub.$emit('newDetailIssue', this.issue); - Store.detail.list = this.list; + if (Store.detail.issue && Store.detail.issue.id === this.issue.id) { + eventHub.$emit('clearDetailIssue'); + } else { + eventHub.$emit('newDetailIssue', this.issue); + Store.detail.list = this.list; + } } - } + }, }, - }, -}; + }; </script> <template> diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index b717c4b0fd4..371be109229 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -6,13 +6,13 @@ import Flash from '../../flash'; import { __ } from '../../locale'; import Sidebar from '../../right_sidebar'; import eventHub from '../../sidebar/event_hub'; -import assigneeTitle from '../../sidebar/components/assignees/assignee_title.vue'; -import assignees from '../../sidebar/components/assignees/assignees.vue'; +import AssigneeTitle from '../../sidebar/components/assignees/assignee_title.vue'; +import Assignees from '../../sidebar/components/assignees/assignees.vue'; import DueDateSelectors from '../../due_date_select'; -import './sidebar/remove_issue'; +import RemoveBtn from './sidebar/remove_issue.vue'; import IssuableContext from '../../issuable_context'; import LabelsSelect from '../../labels_select'; -import subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue'; +import Subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue'; import MilestoneSelect from '../../milestone_select'; const Store = gl.issueBoards.BoardsStore; @@ -22,10 +22,10 @@ window.gl.issueBoards = window.gl.issueBoards || {}; gl.issueBoards.BoardSidebar = Vue.extend({ components: { - assigneeTitle, - assignees, - removeBtn: gl.issueBoards.RemoveIssueBtn, - subscriptions, + AssigneeTitle, + Assignees, + RemoveBtn, + Subscriptions, }, props: { currentUser: { diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js deleted file mode 100644 index f7d7b910e2f..00000000000 --- a/app/assets/javascripts/boards/components/issue_card_inner.js +++ /dev/null @@ -1,196 +0,0 @@ -import $ from 'jquery'; -import Vue from 'vue'; -import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; -import eventHub from '../eventhub'; - -const Store = gl.issueBoards.BoardsStore; - -window.gl = window.gl || {}; -window.gl.issueBoards = window.gl.issueBoards || {}; - -gl.issueBoards.IssueCardInner = Vue.extend({ - components: { - UserAvatarLink, - }, - 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, - }, - groupId: { - type: Number, - required: false, - default: null, - }, - }, - data() { - return { - limitBeforeCounter: 3, - maxRender: 4, - maxCounter: 99, - }; - }, - computed: { - numberOverLimit() { - return this.issue.assignees.length - this.limitBeforeCounter; - }, - assigneeCounterTooltip() { - return `${this.assigneeCounterLabel} more`; - }, - assigneeCounterLabel() { - if (this.numberOverLimit > this.maxCounter) { - return `${this.maxCounter}+`; - } - - return `+${this.numberOverLimit}`; - }, - shouldRenderCounter() { - if (this.issue.assignees.length <= this.maxRender) { - return false; - } - - return this.issue.assignees.length > this.numberOverLimit; - }, - issueId() { - if (this.issue.iid) { - return `#${this.issue.iid}`; - } - return false; - }, - showLabelFooter() { - return this.issue.labels.find(l => this.showLabel(l)) !== undefined; - }, - }, - methods: { - isIndexLessThanlimit(index) { - return index < this.limitBeforeCounter; - }, - shouldRenderAssignee(index) { - // Eg. maxRender is 4, - // Render up to all 4 assignees if there are only 4 assigness - // Otherwise render up to the limitBeforeCounter - if (this.issue.assignees.length <= this.maxRender) { - return index < this.maxRender; - } - - return index < this.limitBeforeCounter; - }, - assigneeUrl(assignee) { - return `${this.rootPath}${assignee.username}`; - }, - assigneeUrlTitle(assignee) { - return `Assigned to ${assignee.name}`; - }, - avatarUrlTitle(assignee) { - return `Avatar for ${assignee.name}`; - }, - showLabel(label) { - if (!label.id) return false; - return true; - }, - 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'); - - if (labelIndex === -1) { - filterPath.push(param); - } else { - filterPath.splice(labelIndex, 1); - } - - gl.issueBoards.BoardsStore.filter.path = filterPath.join('&'); - - Store.updateFiltersUrl(); - - eventHub.$emit('updateTokens'); - }, - labelStyle(label) { - return { - backgroundColor: label.color, - color: label.textColor, - }; - }, - }, - template: ` - <div> - <div class="board-card-header"> - <h4 class="board-card-title"> - <i - class="fa fa-eye-slash confidential-icon" - v-if="issue.confidential" - aria-hidden="true" - /> - <a - class="js-no-trigger" - :href="issue.path" - :title="issue.title">{{ issue.title }}</a> - <span - class="board-card-number" - v-if="issueId" - > - {{ issue.referencePath }} - </span> - </h4> - <div class="board-card-assignee"> - <user-avatar-link - v-for="(assignee, index) in issue.assignees" - :key="assignee.id" - v-if="shouldRenderAssignee(index)" - class="js-no-trigger" - :link-href="assigneeUrl(assignee)" - :img-alt="avatarUrlTitle(assignee)" - :img-src="assignee.avatar" - :tooltip-text="assigneeUrlTitle(assignee)" - tooltip-placement="bottom" - /> - <span - class="avatar-counter has-tooltip" - :title="assigneeCounterTooltip" - v-if="shouldRenderCounter" - > - {{ assigneeCounterLabel }} - </span> - </div> - </div> - <div - class="board-card-footer" - v-if="showLabelFooter" - > - <button - class="badge color-label has-tooltip" - v-for="label in issue.labels" - type="button" - v-if="showLabel(label)" - @click="filterByLabel(label, $event)" - :style="labelStyle(label)" - :title="label.description" - data-container="body"> - {{ label.title }} - </button> - </div> - </div> - `, -}); diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue new file mode 100644 index 00000000000..d50641dc3a9 --- /dev/null +++ b/app/assets/javascripts/boards/components/issue_card_inner.vue @@ -0,0 +1,202 @@ +<script> + import $ from 'jquery'; + import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; + import eventHub from '../eventhub'; + import tooltip from '../../vue_shared/directives/tooltip'; + + const Store = gl.issueBoards.BoardsStore; + + export default { + components: { + UserAvatarLink, + }, + directives: { + tooltip, + }, + 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, + }, + groupId: { + type: Number, + required: false, + default: null, + }, + }, + data() { + return { + limitBeforeCounter: 3, + maxRender: 4, + maxCounter: 99, + }; + }, + computed: { + numberOverLimit() { + return this.issue.assignees.length - this.limitBeforeCounter; + }, + assigneeCounterTooltip() { + return `${this.assigneeCounterLabel} more`; + }, + assigneeCounterLabel() { + if (this.numberOverLimit > this.maxCounter) { + return `${this.maxCounter}+`; + } + + return `+${this.numberOverLimit}`; + }, + shouldRenderCounter() { + if (this.issue.assignees.length <= this.maxRender) { + return false; + } + + return this.issue.assignees.length > this.numberOverLimit; + }, + issueId() { + if (this.issue.iid) { + return `#${this.issue.iid}`; + } + return false; + }, + showLabelFooter() { + return this.issue.labels.find(l => this.showLabel(l)) !== undefined; + }, + }, + methods: { + isIndexLessThanlimit(index) { + return index < this.limitBeforeCounter; + }, + shouldRenderAssignee(index) { + // Eg. maxRender is 4, + // Render up to all 4 assignees if there are only 4 assigness + // Otherwise render up to the limitBeforeCounter + if (this.issue.assignees.length <= this.maxRender) { + return index < this.maxRender; + } + + return index < this.limitBeforeCounter; + }, + assigneeUrl(assignee) { + return `${this.rootPath}${assignee.username}`; + }, + assigneeUrlTitle(assignee) { + return `Assigned to ${assignee.name}`; + }, + avatarUrlTitle(assignee) { + return `Avatar for ${assignee.name}`; + }, + showLabel(label) { + if (!label.id) return false; + return true; + }, + 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'); + + if (labelIndex === -1) { + filterPath.push(param); + } else { + filterPath.splice(labelIndex, 1); + } + + gl.issueBoards.BoardsStore.filter.path = filterPath.join('&'); + + Store.updateFiltersUrl(); + + eventHub.$emit('updateTokens'); + }, + labelStyle(label) { + return { + backgroundColor: label.color, + color: label.textColor, + }; + }, + }, + }; +</script> +<template> + <div> + <div class="board-card-header"> + <h4 class="board-card-title"> + <i + v-if="issue.confidential" + class="fa fa-eye-slash confidential-icon" + aria-hidden="true" + ></i> + <a + :href="issue.path" + :title="issue.title" + class="js-no-trigger">{{ issue.title }}</a> + <span + v-if="issueId" + class="board-card-number" + > + {{ issue.referencePath }} + </span> + </h4> + <div class="board-card-assignee"> + <user-avatar-link + v-for="(assignee, index) in issue.assignees" + v-if="shouldRenderAssignee(index)" + :key="assignee.id" + :link-href="assigneeUrl(assignee)" + :img-alt="avatarUrlTitle(assignee)" + :img-src="assignee.avatar" + :tooltip-text="assigneeUrlTitle(assignee)" + class="js-no-trigger" + tooltip-placement="bottom" + /> + <span + v-tooltip + v-if="shouldRenderCounter" + :title="assigneeCounterTooltip" + class="avatar-counter" + > + {{ assigneeCounterLabel }} + </span> + </div> + </div> + <div + v-if="showLabelFooter" + class="board-card-footer" + > + <button + v-tooltip + v-for="label in issue.labels" + v-if="showLabel(label)" + :key="label.id" + :style="labelStyle(label)" + :title="label.description" + class="badge color-label" + type="button" + data-container="body" + @click="filterByLabel(label, $event)" + > + {{ label.title }} + </button> + </div> + </div> +</template> diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.vue index 2745ca219ad..e0dac6003f1 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js +++ b/app/assets/javascripts/boards/components/modal/footer.vue @@ -1,14 +1,14 @@ -import Vue from 'vue'; +<script> import Flash from '../../../flash'; import { __ } from '../../../locale'; -import './lists_dropdown'; +import ListsDropdown from './lists_dropdown.vue'; import { pluralize } from '../../../lib/utils/text_utility'; import ModalStore from '../../stores/modal_store'; import modalMixin from '../../mixins/modal_mixins'; -gl.issueBoards.ModalFooter = Vue.extend({ +export default { components: { - 'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown, + ListsDropdown, }, mixins: [modalMixin], data() { @@ -55,28 +55,32 @@ gl.issueBoards.ModalFooter = Vue.extend({ this.toggleModal(false); }, }, - template: ` - <footer - class="form-actions add-issues-footer"> - <div class="float-left"> - <button - class="btn btn-success" - type="button" - :disabled="submitDisabled" - @click="addIssues"> - {{ submitText }} - </button> - <span class="inline add-issues-footer-to-list"> - to list - </span> - <lists-dropdown></lists-dropdown> - </div> +}; +</script> +<template> + <footer + class="form-actions add-issues-footer" + > + <div class="float-left"> <button - class="btn btn-default float-right" + :disabled="submitDisabled" + class="btn btn-success" type="button" - @click="toggleModal(false)"> - Cancel + @click="addIssues" + > + {{ submitText }} </button> - </footer> - `, -}); + <span class="inline add-issues-footer-to-list"> + to list + </span> + <lists-dropdown/> + </div> + <button + class="btn btn-default float-right" + type="button" + @click="toggleModal(false)" + > + Cancel + </button> + </footer> +</template> diff --git a/app/assets/javascripts/boards/components/modal/header.js b/app/assets/javascripts/boards/components/modal/header.js deleted file mode 100644 index 5e511bb8935..00000000000 --- a/app/assets/javascripts/boards/components/modal/header.js +++ /dev/null @@ -1,79 +0,0 @@ -import Vue from 'vue'; -import modalFilters from './filters'; -import './tabs'; -import ModalStore from '../../stores/modal_store'; -import modalMixin from '../../mixins/modal_mixins'; - -gl.issueBoards.ModalHeader = Vue.extend({ - components: { - 'modal-tabs': gl.issueBoards.ModalTabs, - modalFilters, - }, - mixins: [modalMixin], - props: { - projectId: { - type: Number, - required: true, - }, - milestonePath: { - type: String, - required: true, - }, - labelPath: { - type: String, - required: true, - }, - }, - data() { - return ModalStore.store; - }, - computed: { - selectAllText() { - if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) { - return 'Select all'; - } - - return 'Deselect all'; - }, - showSearch() { - return this.activeTab === 'all' && !this.loading && this.issuesCount > 0; - }, - }, - methods: { - toggleAll() { - this.$refs.selectAllBtn.blur(); - - ModalStore.toggleAll(); - }, - }, - 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" /> - <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/header.vue b/app/assets/javascripts/boards/components/modal/header.vue new file mode 100644 index 00000000000..979fb4d7199 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/header.vue @@ -0,0 +1,82 @@ +<script> + import ModalFilters from './filters'; + import ModalTabs from './tabs.vue'; + import ModalStore from '../../stores/modal_store'; + import modalMixin from '../../mixins/modal_mixins'; + + export default { + components: { + ModalTabs, + ModalFilters, + }, + mixins: [modalMixin], + props: { + projectId: { + type: Number, + required: true, + }, + milestonePath: { + type: String, + required: true, + }, + labelPath: { + type: String, + required: true, + }, + }, + data() { + return ModalStore.store; + }, + computed: { + selectAllText() { + if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) { + return 'Select all'; + } + + return 'Deselect all'; + }, + showSearch() { + return this.activeTab === 'all' && !this.loading && this.issuesCount > 0; + }, + }, + methods: { + toggleAll() { + this.$refs.selectAllBtn.blur(); + + ModalStore.toggleAll(); + }, + }, + }; +</script> +<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"/> + <div + v-if="showSearch" + class="add-issues-search append-bottom-10"> + <modal-filters :store="filter" /> + <button + ref="selectAllBtn" + type="button" + class="btn btn-success btn-inverted prepend-left-10" + @click="toggleAll" + > + {{ selectAllText }} + </button> + </div> + </div> +</template> diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js deleted file mode 100644 index c10397eaaba..00000000000 --- a/app/assets/javascripts/boards/components/modal/index.js +++ /dev/null @@ -1,171 +0,0 @@ -/* global ListIssue */ - -import Vue from 'vue'; -import queryData from '~/boards/utils/query_data'; -import loadingIcon from '~/vue_shared/components/loading_icon.vue'; -import './header'; -import './list'; -import './footer'; -import EmptyState from './empty_state.vue'; -import ModalStore from '../../stores/modal_store'; - -gl.issueBoards.IssuesModal = Vue.extend({ - components: { - EmptyState, - 'modal-header': gl.issueBoards.ModalHeader, - 'modal-list': gl.issueBoards.ModalList, - 'modal-footer': gl.issueBoards.ModalFooter, - loadingIcon, - }, - props: { - newIssuePath: { - type: String, - required: true, - }, - emptyStateSvg: { - 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, - }, - }, - data() { - return ModalStore.store; - }, - computed: { - showList() { - if (this.activeTab === 'selected') { - return this.selectedIssues.length > 0; - } - - return this.issuesCount > 0; - }, - showEmptyState() { - if (!this.loading && this.issuesCount === 0) { - return true; - } - - return this.activeTab === 'selected' && this.selectedIssues.length === 0; - }, - }, - watch: { - page() { - this.loadIssues(); - }, - showAddIssuesModal() { - if (this.showAddIssuesModal && !this.issues.length) { - this.loading = true; - const loadingDone = () => { - this.loading = false; - }; - - this.loadIssues() - .then(loadingDone) - .catch(loadingDone); - } else if (!this.showAddIssuesModal) { - this.issues = []; - this.selectedIssues = []; - this.issuesCount = false; - } - }, - filter: { - handler() { - if (this.$el.tagName) { - this.page = 1; - this.filterLoading = true; - const loadingDone = () => { - this.filterLoading = false; - }; - - this.loadIssues(true) - .then(loadingDone) - .catch(loadingDone); - } - }, - deep: true, - }, - }, - created() { - this.page = 1; - }, - 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 => res.data) - .then((data) => { - if (clearIssues) { - this.issues = []; - } - - data.issues.forEach((issueObj) => { - const issue = new ListIssue(issueObj); - const foundSelectedIssue = ModalStore.findSelectedIssue(issue); - issue.selected = !!foundSelectedIssue; - - this.issues.push(issue); - }); - - this.loadingNewPage = false; - - if (!this.issuesCount) { - this.issuesCount = data.size; - } - }).catch(() => { - // TODO: handle request error - }); - }, - }, - 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 - :issue-link-base="issueLinkBase" - :root-path="rootPath" - :empty-state-svg="emptyStateSvg" - v-if="!loading && showList && !filterLoading"></modal-list> - <empty-state - v-if="showEmptyState" - :new-issue-path="newIssuePath" - :empty-state-svg="emptyStateSvg"></empty-state> - <section - class="add-issues-list text-center" - v-if="loading || filterLoading"> - <div class="add-issues-list-loading"> - <loading-icon /> - </div> - </section> - <modal-footer></modal-footer> - </div> - </div> - `, -}); diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue new file mode 100644 index 00000000000..33e72a6782e --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/index.vue @@ -0,0 +1,178 @@ +<script> + /* global ListIssue */ + import queryData from '~/boards/utils/query_data'; + import loadingIcon from '~/vue_shared/components/loading_icon.vue'; + import ModalHeader from './header.vue'; + import ModalList from './list.vue'; + import ModalFooter from './footer.vue'; + import EmptyState from './empty_state.vue'; + import ModalStore from '../../stores/modal_store'; + + export default { + components: { + EmptyState, + ModalHeader, + ModalList, + ModalFooter, + loadingIcon, + }, + props: { + newIssuePath: { + type: String, + required: true, + }, + emptyStateSvg: { + 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, + }, + }, + data() { + return ModalStore.store; + }, + computed: { + showList() { + if (this.activeTab === 'selected') { + return this.selectedIssues.length > 0; + } + + return this.issuesCount > 0; + }, + showEmptyState() { + if (!this.loading && this.issuesCount === 0) { + return true; + } + + return this.activeTab === 'selected' && this.selectedIssues.length === 0; + }, + }, + watch: { + page() { + this.loadIssues(); + }, + showAddIssuesModal() { + if (this.showAddIssuesModal && !this.issues.length) { + this.loading = true; + const loadingDone = () => { + this.loading = false; + }; + + this.loadIssues() + .then(loadingDone) + .catch(loadingDone); + } else if (!this.showAddIssuesModal) { + this.issues = []; + this.selectedIssues = []; + this.issuesCount = false; + } + }, + filter: { + handler() { + if (this.$el.tagName) { + this.page = 1; + this.filterLoading = true; + const loadingDone = () => { + this.filterLoading = false; + }; + + this.loadIssues(true) + .then(loadingDone) + .catch(loadingDone); + } + }, + deep: true, + }, + }, + created() { + this.page = 1; + }, + 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 => res.data) + .then(data => { + if (clearIssues) { + this.issues = []; + } + + data.issues.forEach(issueObj => { + const issue = new ListIssue(issueObj); + const foundSelectedIssue = ModalStore.findSelectedIssue(issue); + issue.selected = !!foundSelectedIssue; + + this.issues.push(issue); + }); + + this.loadingNewPage = false; + + if (!this.issuesCount) { + this.issuesCount = data.size; + } + }) + .catch(() => { + // TODO: handle request error + }); + }, + }, + }; +</script> +<template> + <div + v-if="showAddIssuesModal" + class="add-issues-modal"> + <div class="add-issues-container"> + <modal-header + :project-id="projectId" + :milestone-path="milestonePath" + :label-path="labelPath" + /> + <modal-list + v-if="!loading && showList && !filterLoading" + :issue-link-base="issueLinkBase" + :root-path="rootPath" + :empty-state-svg="emptyStateSvg" + /> + <empty-state + v-if="showEmptyState" + :new-issue-path="newIssuePath" + :empty-state-svg="emptyStateSvg" + /> + <section + v-if="loading || filterLoading" + class="add-issues-list text-center" + > + <div class="add-issues-list-loading"> + <loading-icon /> + </div> + </section> + <modal-footer/> + </div> + </div> +</template> diff --git a/app/assets/javascripts/boards/components/modal/list.js b/app/assets/javascripts/boards/components/modal/list.js deleted file mode 100644 index 11061c72a7b..00000000000 --- a/app/assets/javascripts/boards/components/modal/list.js +++ /dev/null @@ -1,159 +0,0 @@ -import Vue from 'vue'; -import bp from '../../../breakpoints'; -import ModalStore from '../../stores/modal_store'; - -gl.issueBoards.ModalList = Vue.extend({ - components: { - 'issue-card-inner': gl.issueBoards.IssueCardInner, - }, - props: { - issueLinkBase: { - type: String, - required: true, - }, - rootPath: { - type: String, - required: true, - }, - emptyStateSvg: { - type: String, - required: true, - }, - }, - data() { - return ModalStore.store; - }, - computed: { - loopIssues() { - if (this.activeTab === 'all') { - return this.issues; - } - - return this.selectedIssues; - }, - groupedIssues() { - const groups = []; - this.loopIssues.forEach((issue, i) => { - const index = i % this.columns; - - if (!groups[index]) { - groups.push([]); - } - - groups[index].push(issue); - }); - - return groups; - }, - }, - watch: { - activeTab() { - if (this.activeTab === 'all') { - ModalStore.purgeUnselectedIssues(); - } - }, - }, - mounted() { - this.scrollHandlerWrapper = this.scrollHandler.bind(this); - this.setColumnCountWrapper = this.setColumnCount.bind(this); - this.setColumnCount(); - - this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper); - window.addEventListener('resize', this.setColumnCountWrapper); - }, - beforeDestroy() { - this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper); - window.removeEventListener('resize', this.setColumnCountWrapper); - }, - methods: { - scrollHandler() { - const currentPage = Math.floor(this.issues.length / this.perPage); - - if ( - this.scrollTop() > this.scrollHeight() - 100 && - !this.loadingNewPage && - currentPage === this.page - ) { - this.loadingNewPage = true; - this.page += 1; - } - }, - toggleIssue(e, issue) { - if (e.target.tagName !== 'A') { - ModalStore.toggleIssue(issue); - } - }, - listHeight() { - return this.$refs.list.getBoundingClientRect().height; - }, - scrollHeight() { - return this.$refs.list.scrollHeight; - }, - scrollTop() { - return this.$refs.list.scrollTop + this.listHeight(); - }, - showIssue(issue) { - if (this.activeTab === 'all') return true; - - const index = ModalStore.selectedIssueIndex(issue); - - return index !== -1; - }, - setColumnCount() { - const breakpoint = bp.getBreakpointSize(); - - if (breakpoint === 'lg' || breakpoint === 'md') { - this.columns = 3; - } else if (breakpoint === 'sm') { - this.columns = 2; - } else { - this.columns = 1; - } - }, - }, - 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="svg-content"> - <img :src="emptyStateSvg"/> - </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="issue in group" - v-if="showIssue(issue)" - class="board-card-parent"> - <div - class="board-card" - :class="{ 'is-active': issue.selected }" - @click="toggleIssue($event, issue)"> - <issue-card-inner - :issue="issue" - :issue-link-base="issueLinkBase" - :root-path="rootPath"> - </issue-card-inner> - <span - :aria-label="'Issue #' + issue.id + ' selected'" - aria-checked="true" - v-if="issue.selected" - class="issue-card-selected text-center"> - <i class="fa fa-check"></i> - </span> - </div> - </div> - </div> - </section> - `, -}); diff --git a/app/assets/javascripts/boards/components/modal/list.vue b/app/assets/javascripts/boards/components/modal/list.vue new file mode 100644 index 00000000000..02ac36d7367 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/list.vue @@ -0,0 +1,162 @@ +<script> + import bp from '../../../breakpoints'; + import ModalStore from '../../stores/modal_store'; + import IssueCardInner from '../issue_card_inner.vue'; + + export default { + components: { + IssueCardInner, + }, + props: { + issueLinkBase: { + type: String, + required: true, + }, + rootPath: { + type: String, + required: true, + }, + emptyStateSvg: { + type: String, + required: true, + }, + }, + data() { + return ModalStore.store; + }, + computed: { + loopIssues() { + if (this.activeTab === 'all') { + return this.issues; + } + + return this.selectedIssues; + }, + groupedIssues() { + const groups = []; + this.loopIssues.forEach((issue, i) => { + const index = i % this.columns; + + if (!groups[index]) { + groups.push([]); + } + + groups[index].push(issue); + }); + + return groups; + }, + }, + watch: { + activeTab() { + if (this.activeTab === 'all') { + ModalStore.purgeUnselectedIssues(); + } + }, + }, + mounted() { + this.scrollHandlerWrapper = this.scrollHandler.bind(this); + this.setColumnCountWrapper = this.setColumnCount.bind(this); + this.setColumnCount(); + + this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper); + window.addEventListener('resize', this.setColumnCountWrapper); + }, + beforeDestroy() { + this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper); + window.removeEventListener('resize', this.setColumnCountWrapper); + }, + methods: { + scrollHandler() { + const currentPage = Math.floor(this.issues.length / this.perPage); + + if ( + this.scrollTop() > this.scrollHeight() - 100 && + !this.loadingNewPage && + currentPage === this.page + ) { + this.loadingNewPage = true; + this.page += 1; + } + }, + toggleIssue(e, issue) { + if (e.target.tagName !== 'A') { + ModalStore.toggleIssue(issue); + } + }, + listHeight() { + return this.$refs.list.getBoundingClientRect().height; + }, + scrollHeight() { + return this.$refs.list.scrollHeight; + }, + scrollTop() { + return this.$refs.list.scrollTop + this.listHeight(); + }, + showIssue(issue) { + if (this.activeTab === 'all') return true; + + const index = ModalStore.selectedIssueIndex(issue); + + return index !== -1; + }, + setColumnCount() { + const breakpoint = bp.getBreakpointSize(); + + if (breakpoint === 'lg' || breakpoint === 'md') { + this.columns = 3; + } else if (breakpoint === 'sm') { + this.columns = 2; + } else { + this.columns = 1; + } + }, + }, + }; +</script> +<template> + <section + ref="list" + class="add-issues-list add-issues-list-columns"> + <div + v-if="issuesCount > 0 && issues.length === 0" + class="empty-state add-issues-empty-state-filter text-center"> + <div + class="svg-content"> + <img :src="emptyStateSvg" /> + </div> + <div class="text-content"> + <h4> + There are no issues to show. + </h4> + </div> + </div> + <div + v-for="(group, index) in groupedIssues" + :key="index" + class="add-issues-list-column"> + <div + v-for="issue in group" + v-if="showIssue(issue)" + :key="issue.id" + class="board-card-parent"> + <div + :class="{ 'is-active': issue.selected }" + class="board-card" + @click="toggleIssue($event, issue)"> + <issue-card-inner + :issue="issue" + :issue-link-base="issueLinkBase" + :root-path="rootPath"/> + <span + v-if="issue.selected" + :aria-label="'Issue #' + issue.id + ' selected'" + aria-checked="true" + class="issue-card-selected text-center"> + <i class="fa fa-check"></i> + </span> + </div> + </div> + </div> + </section> +</template> diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js b/app/assets/javascripts/boards/components/modal/lists_dropdown.js deleted file mode 100644 index e644de2d4fc..00000000000 --- a/app/assets/javascripts/boards/components/modal/lists_dropdown.js +++ /dev/null @@ -1,54 +0,0 @@ -import Vue from 'vue'; -import ModalStore from '../../stores/modal_store'; - -gl.issueBoards.ModalFooterListsDropdown = Vue.extend({ - data() { - return { - modal: ModalStore.store, - state: gl.issueBoards.BoardsStore.state, - }; - }, - computed: { - selected() { - return this.modal.selectedList || this.state.lists[1]; - }, - }, - destroyed() { - this.modal.selectedList = null; - }, - template: ` - <div class="dropdown inline"> - <button - class="dropdown-menu-toggle" - type="button" - data-toggle="dropdown" - aria-expanded="false"> - <span - class="dropdown-label-box" - :style="{ backgroundColor: selected.label.color }"> - </span> - {{ selected.title }} - <i class="fa fa-chevron-down"></i> - </button> - <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"> - <ul> - <li - v-for="list in state.lists" - v-if="list.type == 'label'"> - <a - href="#" - role="button" - :class="{ 'is-active': list.id == selected.id }" - @click.prevent="modal.selectedList = list"> - <span - class="dropdown-label-box" - :style="{ backgroundColor: list.label.color }"> - </span> - {{ list.title }} - </a> - </li> - </ul> - </div> - </div> - `, -}); diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue new file mode 100644 index 00000000000..6a5a39099bd --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue @@ -0,0 +1,56 @@ +<script> +import ModalStore from '../../stores/modal_store'; + +export default { + data() { + return { + modal: ModalStore.store, + state: gl.issueBoards.BoardsStore.state, + }; + }, + computed: { + selected() { + return this.modal.selectedList || this.state.lists[1]; + }, + }, + destroyed() { + this.modal.selectedList = null; + }, +}; +</script> +<template> + <div class="dropdown inline"> + <button + class="dropdown-menu-toggle" + type="button" + data-toggle="dropdown" + aria-expanded="false"> + <span + :style="{ backgroundColor: selected.label.color }" + class="dropdown-label-box"> + </span> + {{ selected.title }} + <i class="fa fa-chevron-down"></i> + </button> + <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"> + <ul> + <li + v-for="(list, i) in state.lists" + v-if="list.type == 'label'" + :key="i"> + <a + :class="{ 'is-active': list.id == selected.id }" + href="#" + role="button" + @click.prevent="modal.selectedList = list"> + <span + :style="{ backgroundColor: list.label.color }" + class="dropdown-label-box"> + </span> + {{ list.title }} + </a> + </li> + </ul> + </div> + </div> +</template> diff --git a/app/assets/javascripts/boards/components/modal/tabs.js b/app/assets/javascripts/boards/components/modal/tabs.js deleted file mode 100644 index 9d331de8e22..00000000000 --- a/app/assets/javascripts/boards/components/modal/tabs.js +++ /dev/null @@ -1,46 +0,0 @@ -import Vue from 'vue'; -import ModalStore from '../../stores/modal_store'; -import modalMixin from '../../mixins/modal_mixins'; - -gl.issueBoards.ModalTabs = Vue.extend({ - mixins: [modalMixin], - data() { - return ModalStore.store; - }, - computed: { - selectedCount() { - return ModalStore.selectedCount(); - }, - }, - destroyed() { - this.activeTab = 'all'; - }, - template: ` - <div class="top-area prepend-top-10 append-bottom-10"> - <ul class="nav-links issues-state-filters"> - <li :class="{ 'active': activeTab == 'all' }"> - <a - href="#" - role="button" - @click.prevent="changeTab('all')"> - Open issues - <span class="badge badge-pill"> - {{ issuesCount }} - </span> - </a> - </li> - <li :class="{ 'active': activeTab == 'selected' }"> - <a - href="#" - role="button" - @click.prevent="changeTab('selected')"> - Selected issues - <span class="badge badge-pill"> - {{ selectedCount }} - </span> - </a> - </li> - </ul> - </div> - `, -}); diff --git a/app/assets/javascripts/boards/components/modal/tabs.vue b/app/assets/javascripts/boards/components/modal/tabs.vue new file mode 100644 index 00000000000..d926b080094 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/tabs.vue @@ -0,0 +1,49 @@ +<script> + import ModalStore from '../../stores/modal_store'; + import modalMixin from '../../mixins/modal_mixins'; + + export default { + mixins: [modalMixin], + data() { + return ModalStore.store; + }, + computed: { + selectedCount() { + return ModalStore.selectedCount(); + }, + }, + destroyed() { + this.activeTab = 'all'; + }, + }; +</script> +<template> + <div class="top-area prepend-top-10 append-bottom-10"> + <ul class="nav-links issues-state-filters"> + <li :class="{ 'active': activeTab == 'all' }"> + <a + href="#" + role="button" + @click.prevent="changeTab('all')" + > + Open issues + <span class="badge badge-pill"> + {{ issuesCount }} + </span> + </a> + </li> + <li :class="{ 'active': activeTab == 'selected' }"> + <a + href="#" + role="button" + @click.prevent="changeTab('selected')" + > + Selected issues + <span class="badge badge-pill"> + {{ selectedCount }} + </span> + </a> + </li> + </ul> + </div> +</template> diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js deleted file mode 100644 index 0a0820ec5fd..00000000000 --- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js +++ /dev/null @@ -1,73 +0,0 @@ -import Vue from 'vue'; -import Flash from '../../../flash'; -import { __ } from '../../../locale'; - -const Store = gl.issueBoards.BoardsStore; - -window.gl = window.gl || {}; -window.gl.issueBoards = window.gl.issueBoards || {}; - -gl.issueBoards.RemoveIssueBtn = Vue.extend({ - props: { - issue: { - type: Object, - required: true, - }, - list: { - type: Object, - required: true, - }, - }, - computed: { - updateUrl() { - return this.issue.path; - }, - }, - methods: { - removeIssue() { - const issue = this.issue; - const lists = issue.getLists(); - const listLabelIds = lists.map(list => list.label.id); - - let labelIds = issue.labels - .map(label => label.id) - .filter(id => !listLabelIds.includes(id)); - if (labelIds.length === 0) { - labelIds = ['']; - } - - const data = { - issue: { - label_ids: labelIds, - }, - }; - - // Post the remove data - Vue.http.patch(this.updateUrl, data).catch(() => { - Flash(__('Failed to remove issue from board, please try again.')); - - lists.forEach((list) => { - list.addIssue(issue); - }); - }); - - // Remove from the frontend store - lists.forEach((list) => { - list.removeIssue(issue); - }); - - Store.detail.issue = {}; - }, - }, - template: ` - <div - class="block list"> - <button - class="btn btn-default btn-block" - type="button" - @click="removeIssue"> - Remove from board - </button> - </div> - `, -}); diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue new file mode 100644 index 00000000000..55278626ffc --- /dev/null +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue @@ -0,0 +1,72 @@ +<script> + import Vue from 'vue'; + import Flash from '../../../flash'; + import { __ } from '../../../locale'; + + const Store = gl.issueBoards.BoardsStore; + + export default { + props: { + issue: { + type: Object, + required: true, + }, + list: { + type: Object, + required: true, + }, + }, + computed: { + updateUrl() { + return this.issue.path; + }, + }, + methods: { + removeIssue() { + const { issue } = this; + const lists = issue.getLists(); + const listLabelIds = lists.map(list => list.label.id); + + let labelIds = issue.labels.map(label => label.id).filter(id => !listLabelIds.includes(id)); + if (labelIds.length === 0) { + labelIds = ['']; + } + + const data = { + issue: { + label_ids: labelIds, + }, + }; + + // Post the remove data + Vue.http.patch(this.updateUrl, data).catch(() => { + Flash(__('Failed to remove issue from board, please try again.')); + + lists.forEach(list => { + list.addIssue(issue); + }); + }); + + // Remove from the frontend store + lists.forEach(list => { + list.removeIssue(issue); + }); + + Store.detail.issue = {}; + }, + }, + }; +</script> +<template> + <div + class="block list" + > + <button + class="btn btn-default btn-block" + type="button" + @click="removeIssue" + > + Remove from board + </button> + </div> +</template> diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 2d9141bf71c..200d1923635 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -25,7 +25,7 @@ import './filters/due_date_filters'; import './components/board'; import './components/board_sidebar'; import './components/new_list_dropdown'; -import './components/modal/index'; +import BoardAddIssuesModal from './components/modal/index.vue'; import '~/vue_shared/vue_resource_interceptor'; // eslint-disable-line import/first export default () => { @@ -49,7 +49,7 @@ export default () => { components: { 'board': gl.issueBoards.Board, 'board-sidebar': gl.issueBoards.BoardSidebar, - 'board-add-issues-modal': gl.issueBoards.IssuesModal, + BoardAddIssuesModal, }, data: { state: Store.state, @@ -121,7 +121,7 @@ export default () => { this.filterManager.updateTokens(); }, updateDetailIssue(newIssue) { - const sidebarInfoEndpoint = newIssue.sidebarInfoEndpoint; + const { sidebarInfoEndpoint } = newIssue; if (sidebarInfoEndpoint && newIssue.subscribed === undefined) { newIssue.setFetchingState('subscriptions', true); BoardService.getIssueInfo(sidebarInfoEndpoint) @@ -144,7 +144,7 @@ export default () => { Store.detail.issue = {}; }, toggleSubscription(id) { - const issue = Store.detail.issue; + const { issue } = Store.detail; if (issue.id === id && issue.toggleSubscriptionEndpoint) { issue.setFetchingState('subscriptions', true); BoardService.toggleIssueSubscription(issue.toggleSubscriptionEndpoint) diff --git a/app/assets/javascripts/boards/stores/modal_store.js b/app/assets/javascripts/boards/stores/modal_store.js index a4220cd840d..0d9ac367a70 100644 --- a/app/assets/javascripts/boards/stores/modal_store.js +++ b/app/assets/javascripts/boards/stores/modal_store.js @@ -26,7 +26,7 @@ class ModalStore { toggleIssue(issueObj) { const issue = issueObj; - const selected = issue.selected; + const { selected } = issue; issue.selected = !selected; diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index e42a3632e79..8139aa69fc7 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -81,7 +81,7 @@ export default class Clusters { } initApplications() { - const store = this.store; + const { store } = this; const el = document.querySelector('#js-cluster-applications'); this.applications = new Vue({ diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js index 2d180e9903a..410580b4c25 100644 --- a/app/assets/javascripts/commit/image_file.js +++ b/app/assets/javascripts/commit/image_file.js @@ -122,7 +122,7 @@ export default class ImageFile { return $('.swipe.view', this.file).each((function(_this) { return function(index, view) { var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref; - ref = _this.prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1]; + ref = _this.prepareFrames(view), [maxWidth, maxHeight] = ref; $swipeFrame = $('.swipe-frame', view); $swipeWrap = $('.swipe-wrap', view); $swipeBar = $('.swipe-bar', view); @@ -159,7 +159,7 @@ export default class ImageFile { return $('.onion-skin.view', this.file).each((function(_this) { return function(index, view) { var $frame, $track, $dragger, $frameAdded, framePadding, ref, dragging = false; - ref = _this.prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1]; + ref = _this.prepareFrames(view), [maxWidth, maxHeight] = ref; $frame = $('.onion-skin-frame', view); $frameAdded = $('.frame.added', view); $track = $('.drag-track', view); diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js index f77a5730b77..02aa507ba03 100644 --- a/app/assets/javascripts/create_merge_request_dropdown.js +++ b/app/assets/javascripts/create_merge_request_dropdown.js @@ -281,7 +281,7 @@ export default class CreateMergeRequestDropdown { if (event.target === this.branchInput) { target = 'branch'; - value = this.branchInput.value; + ({ value } = this.branchInput); } else if (event.target === this.refInput) { target = 'ref'; value = diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js index 40f7c2fe5f3..5528d2a542b 100644 --- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js +++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js @@ -111,7 +111,7 @@ const DiffNoteAvatars = Vue.extend({ }); }, addNoCommentClass() { - const notesCount = this.notesCount; + const { notesCount } = this; $(this.$el).closest('.js-avatar-container') .toggleClass('no-comment-btn', notesCount > 0) diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js index 66b20cc8739..2b893e35b6d 100644 --- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js +++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js @@ -73,7 +73,7 @@ const JumpToDiscussion = Vue.extend({ }).toArray(); }; - const discussions = this.discussions; + const { discussions } = this; if (activeTab === 'diffs') { discussionsSelector = '.diffs .notes[data-discussion-id]'; diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js index a9800a11644..7dcf3594471 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js @@ -18,7 +18,7 @@ import './components/new_issue_for_discussion'; export default () => { const projectPathHolder = document.querySelector('.merge-request') || document.querySelector('.commit-box'); - const projectPath = projectPathHolder.dataset.projectPath; + const { projectPath } = projectPathHolder.dataset; const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn, new-issue-for-discussion-btn'; diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 82ca10f4163..deddb61ca31 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -26,6 +26,10 @@ export default { type: String, required: true, }, + projectPath: { + type: String, + required: true, + }, shouldShow: { type: Boolean, required: false, @@ -94,18 +98,16 @@ export default { }, }, mounted() { - this.setEndpoint(this.endpoint); - this - .fetchDiffFiles() - .catch(() => { - createFlash(__('Something went wrong on our end. Please try again!')); - }); + this.setBaseConfig({ endpoint: this.endpoint, projectPath: this.projectPath }); + this.fetchDiffFiles().catch(() => { + createFlash(__('Fetching diff files failed. Please reload the page to try again!')); + }); }, created() { this.adjustView(); }, methods: { - ...mapActions(['setEndpoint', 'fetchDiffFiles']), + ...mapActions(['setBaseConfig', 'fetchDiffFiles']), setActive(filePath) { this.activeFile = filePath; }, diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue index adcd22f7876..48ba967285f 100644 --- a/app/assets/javascripts/diffs/components/diff_content.vue +++ b/app/assets/javascripts/diffs/components/diff_content.vue @@ -1,5 +1,7 @@ <script> -import { mapGetters } from 'vuex'; +import { mapGetters, mapState } from 'vuex'; +import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue'; +import { diffModes } from '~/ide/constants'; import InlineDiffView from './inline_diff_view.vue'; import ParallelDiffView from './parallel_diff_view.vue'; @@ -7,6 +9,7 @@ export default { components: { InlineDiffView, ParallelDiffView, + DiffViewer, }, props: { diffFile: { @@ -15,7 +18,18 @@ export default { }, }, computed: { + ...mapState({ + projectPath: state => state.diffs.projectPath, + endpoint: state => state.diffs.endpoint, + }), ...mapGetters(['isInlineView', 'isParallelView']), + diffMode() { + const diffModeKey = Object.keys(diffModes).find(key => this.diffFile[`${key}File`]); + return diffModes[diffModeKey] || diffModes.replaced; + }, + isTextFile() { + return this.diffFile.text; + }, }, }; </script> @@ -23,16 +37,26 @@ export default { <template> <div class="diff-content"> <div class="diff-viewer"> - <inline-diff-view - v-if="isInlineView" - :diff-file="diffFile" - :diff-lines="diffFile.highlightedDiffLines || []" - /> - <parallel-diff-view - v-if="isParallelView" - :diff-file="diffFile" - :diff-lines="diffFile.parallelDiffLines || []" - /> + <template v-if="isTextFile"> + <inline-diff-view + v-if="isInlineView" + :diff-file="diffFile" + :diff-lines="diffFile.highlightedDiffLines || []" + /> + <parallel-diff-view + v-else-if="isParallelView" + :diff-file="diffFile" + :diff-lines="diffFile.parallelDiffLines || []" + /> + </template> + <diff-viewer + v-else + :diff-mode="diffMode" + :new-path="diffFile.newPath" + :new-sha="diffFile.diffRefs.headSha" + :old-path="diffFile.oldPath" + :old-sha="diffFile.diffRefs.baseSha" + :project-path="projectPath"/> </div> </div> </template> diff --git a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue index 3193b18becb..7e50a0aed84 100644 --- a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue +++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue @@ -47,7 +47,7 @@ export default { methods: { ...mapActions(['toggleDiscussion']), getTooltipText(noteData) { - let note = noteData.note; + let { note } = noteData; if (note.length > LENGTH_OF_AVATAR_TOOLTIP) { note = truncate(note, LENGTH_OF_AVATAR_TOOLTIP); diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue index 05dca0cdd9a..a74ea4bfaaf 100644 --- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue +++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue @@ -4,14 +4,7 @@ import { s__ } from '~/locale'; import { mapState, mapGetters, mapActions } from 'vuex'; import Icon from '~/vue_shared/components/icon.vue'; import DiffGutterAvatars from './diff_gutter_avatars.vue'; -import { - MATCH_LINE_TYPE, - CONTEXT_LINE_TYPE, - OLD_NO_NEW_LINE_TYPE, - NEW_NO_NEW_LINE_TYPE, - LINE_POSITION_RIGHT, - UNFOLD_COUNT, -} from '../constants'; +import { LINE_POSITION_RIGHT, UNFOLD_COUNT } from '../constants'; import * as utils from '../store/utils'; export default { @@ -63,6 +56,21 @@ export default { required: false, default: false, }, + isMatchLine: { + type: Boolean, + required: false, + default: false, + }, + isMetaLine: { + type: Boolean, + required: false, + default: false, + }, + isContextLine: { + type: Boolean, + required: false, + default: false, + }, }, computed: { ...mapState({ @@ -70,15 +78,6 @@ export default { diffFiles: state => state.diffs.diffFiles, }), ...mapGetters(['isLoggedIn', 'discussionsByLineCode']), - isMatchLine() { - return this.lineType === MATCH_LINE_TYPE; - }, - isContextLine() { - return this.lineType === CONTEXT_LINE_TYPE; - }, - isMetaLine() { - return this.lineType === OLD_NO_NEW_LINE_TYPE || this.lineType === NEW_NO_NEW_LINE_TYPE; - }, lineHref() { return this.lineCode ? `#${this.lineCode}` : '#'; }, @@ -109,9 +108,9 @@ export default { }, }, methods: { - ...mapActions(['loadMoreLines']), + ...mapActions(['loadMoreLines', 'showCommentForm']), handleCommentButton() { - this.$emit('showCommentForm', { lineCode: this.lineCode }); + this.showCommentForm({ lineCode: this.lineCode }); }, handleLoadMoreLines() { if (this.isRequesting) { @@ -124,7 +123,7 @@ export default { const newLineNumber = this.metaData.newPos || 0; const offset = newLineNumber - oldLineNumber; const bottom = this.isBottom; - const fileHash = this.fileHash; + const { fileHash } = this; const view = this.diffViewType; let unfold = true; let lineNumber = newLineNumber - 1; diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue index 86f5e98194d..6943b462e86 100644 --- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue +++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue @@ -1,9 +1,12 @@ <script> +import $ from 'jquery'; import { mapState, mapGetters, mapActions } from 'vuex'; import createFlash from '~/flash'; import { s__ } from '~/locale'; import noteForm from '../../notes/components/note_form.vue'; import { getNoteFormData } from '../store/utils'; +import Autosave from '../../autosave'; +import { DIFF_NOTE_TYPE, NOTE_TYPE } from '../constants'; export default { components: { @@ -37,11 +40,28 @@ export default { noteableData: state => state.notes.noteableData, diffViewType: state => state.diffs.diffViewType, }), - ...mapGetters(['noteableType', 'getNotesDataByProp']), + ...mapGetters(['isLoggedIn', 'noteableType', 'getNoteableData', 'getNotesDataByProp']), + }, + mounted() { + if (this.isLoggedIn) { + const noteableData = this.getNoteableData; + const keys = [ + NOTE_TYPE, + this.noteableType, + noteableData.id, + noteableData.diff_head_sha, + DIFF_NOTE_TYPE, + noteableData.source_project_id, + this.line.lineCode, + ]; + + this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), keys); + } }, methods: { ...mapActions(['cancelCommentForm', 'saveNote', 'fetchDiscussions']), handleCancelCommentForm() { + this.autosave.reset(); this.cancelCommentForm({ lineCode: this.line.lineCode, }); @@ -82,6 +102,7 @@ export default { class="content discussion-form discussion-form-container discussion-notes" > <note-form + ref="noteForm" :is-editing="true" :line-code="line.lineCode" save-button-title="Comment" diff --git a/app/assets/javascripts/diffs/components/diff_table_cell.vue b/app/assets/javascripts/diffs/components/diff_table_cell.vue new file mode 100644 index 00000000000..68fe6787f9b --- /dev/null +++ b/app/assets/javascripts/diffs/components/diff_table_cell.vue @@ -0,0 +1,131 @@ +<script> +import { mapGetters } from 'vuex'; +import DiffLineGutterContent from './diff_line_gutter_content.vue'; +import { + MATCH_LINE_TYPE, + CONTEXT_LINE_TYPE, + EMPTY_CELL_TYPE, + OLD_LINE_TYPE, + OLD_NO_NEW_LINE_TYPE, + NEW_NO_NEW_LINE_TYPE, + LINE_HOVER_CLASS_NAME, + LINE_UNFOLD_CLASS_NAME, +} from '../constants'; + +export default { + components: { + DiffLineGutterContent, + }, + props: { + line: { + type: Object, + required: true, + }, + diffFile: { + type: Object, + required: true, + }, + showCommentButton: { + type: Boolean, + required: false, + default: false, + }, + linePosition: { + type: String, + required: false, + default: '', + }, + lineType: { + type: String, + required: false, + default: '', + }, + isContentLine: { + type: Boolean, + required: false, + default: false, + }, + isBottom: { + type: Boolean, + required: false, + default: false, + }, + isHover: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + ...mapGetters(['isLoggedIn', 'isInlineView']), + normalizedLine() { + if (this.isInlineView) { + return this.line; + } + + return this.lineType === OLD_LINE_TYPE ? this.line.left : this.line.right; + }, + isMatchLine() { + return this.normalizedLine.type === MATCH_LINE_TYPE; + }, + isContextLine() { + return this.normalizedLine.type === CONTEXT_LINE_TYPE; + }, + isMetaLine() { + return ( + this.normalizedLine.type === OLD_NO_NEW_LINE_TYPE || + this.normalizedLine.type === NEW_NO_NEW_LINE_TYPE || + this.normalizedLine.type === EMPTY_CELL_TYPE + ); + }, + classNameMap() { + const { type } = this.normalizedLine; + + return { + [type]: type, + [LINE_UNFOLD_CLASS_NAME]: this.isMatchLine, + [LINE_HOVER_CLASS_NAME]: + this.isLoggedIn && + this.isHover && + !this.isMatchLine && + !this.isContextLine && + !this.isMetaLine, + }; + }, + lineNumber() { + const { lineType, normalizedLine } = this; + + return lineType === OLD_LINE_TYPE ? normalizedLine.oldLine : normalizedLine.newLine; + }, + }, +}; +</script> + +<template> + <td + v-if="isContentLine" + :class="lineType" + class="line_content" + v-html="normalizedLine.richText" + > + </td> + <td + v-else + :class="classNameMap" + > + <diff-line-gutter-content + :file-hash="diffFile.fileHash" + :line-type="normalizedLine.type" + :line-code="normalizedLine.lineCode" + :line-position="linePosition" + :line-number="lineNumber" + :meta-data="normalizedLine.metaData" + :show-comment-button="showCommentButton" + :context-lines-path="diffFile.contextLinesPath" + :is-bottom="isBottom" + :is-match-line="isMatchLine" + :is-context-line="isContentLine" + :is-meta-line="isMetaLine" + /> + </td> +</template> diff --git a/app/assets/javascripts/diffs/components/diff_table_row.vue b/app/assets/javascripts/diffs/components/diff_table_row.vue new file mode 100644 index 00000000000..8716fdaf44d --- /dev/null +++ b/app/assets/javascripts/diffs/components/diff_table_row.vue @@ -0,0 +1,191 @@ +<script> +import $ from 'jquery'; +import { mapGetters } from 'vuex'; +import DiffTableCell from './diff_table_cell.vue'; +import { + NEW_LINE_TYPE, + OLD_LINE_TYPE, + CONTEXT_LINE_TYPE, + CONTEXT_LINE_CLASS_NAME, + OLD_NO_NEW_LINE_TYPE, + PARALLEL_DIFF_VIEW_TYPE, + NEW_NO_NEW_LINE_TYPE, + LINE_POSITION_LEFT, + LINE_POSITION_RIGHT, +} from '../constants'; + +export default { + components: { + DiffTableCell, + }, + props: { + diffFile: { + type: Object, + required: true, + }, + line: { + type: Object, + required: true, + }, + isBottom: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + isHover: false, + isLeftHover: false, + isRightHover: false, + }; + }, + computed: { + ...mapGetters(['isInlineView', 'isParallelView']), + isContextLine() { + return this.line.left + ? this.line.left.type === CONTEXT_LINE_TYPE + : this.line.type === CONTEXT_LINE_TYPE; + }, + classNameMap() { + return { + [this.line.type]: this.line.type, + [CONTEXT_LINE_CLASS_NAME]: this.isContextLine, + [PARALLEL_DIFF_VIEW_TYPE]: this.isParallelView, + }; + }, + inlineRowId() { + const { lineCode, oldLine, newLine } = this.line; + + return lineCode || `${this.diffFile.fileHash}_${oldLine}_${newLine}`; + }, + parallelViewLeftLineType() { + if (this.line.right.type === NEW_NO_NEW_LINE_TYPE) { + return OLD_NO_NEW_LINE_TYPE; + } + + return this.line.left.type; + }, + }, + created() { + this.newLineType = NEW_LINE_TYPE; + this.oldLineType = OLD_LINE_TYPE; + this.linePositionLeft = LINE_POSITION_LEFT; + this.linePositionRight = LINE_POSITION_RIGHT; + }, + methods: { + handleMouseMove(e) { + const isHover = e.type === 'mouseover'; + + if (this.isInlineView) { + this.isHover = isHover; + } else { + const hoveringCell = e.target.closest('td'); + const allCellsInHoveringRow = Array.from(e.currentTarget.children); + const hoverIndex = allCellsInHoveringRow.indexOf(hoveringCell); + + if (hoverIndex >= 2) { + this.isRightHover = isHover; + } else { + this.isLeftHover = isHover; + } + } + }, + // Prevent text selecting on both sides of parallel diff view + // Backport of the same code from legacy diff notes. + handleParallelLineMouseDown(e) { + const line = $(e.currentTarget); + const table = line.closest('table'); + + table.removeClass('left-side-selected right-side-selected'); + const [lineClass] = ['left-side', 'right-side'].filter(name => line.hasClass(name)); + + if (lineClass) { + table.addClass(`${lineClass}-selected`); + } + }, + }, +}; +</script> + +<template> + <tr + v-if="isInlineView" + :id="inlineRowId" + :class="classNameMap" + class="line_holder" + @mouseover="handleMouseMove" + @mouseout="handleMouseMove" + > + <diff-table-cell + :diff-file="diffFile" + :line="line" + :line-type="oldLineType" + :is-bottom="isBottom" + :is-hover="isHover" + :show-comment-button="true" + class="diff-line-num old_line" + /> + <diff-table-cell + :diff-file="diffFile" + :line="line" + :line-type="newLineType" + :is-bottom="isBottom" + :is-hover="isHover" + class="diff-line-num new_line" + /> + <diff-table-cell + :class="line.type" + :diff-file="diffFile" + :line="line" + :is-content-line="true" + /> + </tr> + + <tr + v-else + :class="classNameMap" + class="line_holder" + @mouseover="handleMouseMove" + @mouseout="handleMouseMove" + > + <diff-table-cell + :diff-file="diffFile" + :line="line" + :line-type="oldLineType" + :line-position="linePositionLeft" + :is-bottom="isBottom" + :is-hover="isLeftHover" + :show-comment-button="true" + class="diff-line-num old_line" + /> + <diff-table-cell + :id="line.left.lineCode" + :diff-file="diffFile" + :line="line" + :is-content-line="true" + :line-type="parallelViewLeftLineType" + class="line_content parallel left-side" + @mousedown.native="handleParallelLineMouseDown" + /> + <diff-table-cell + :diff-file="diffFile" + :line="line" + :line-type="newLineType" + :line-position="linePositionRight" + :is-bottom="isBottom" + :is-hover="isRightHover" + :show-comment-button="true" + class="diff-line-num new_line" + /> + <diff-table-cell + :id="line.right.lineCode" + :diff-file="diffFile" + :line="line" + :is-content-line="true" + :line-type="line.right.type" + class="line_content parallel right-side" + @mousedown.native="handleParallelLineMouseDown" + /> + </tr> +</template> diff --git a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue new file mode 100644 index 00000000000..0e935f1d68e --- /dev/null +++ b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue @@ -0,0 +1,82 @@ +<script> +import { mapState, mapGetters } from 'vuex'; +import diffDiscussions from './diff_discussions.vue'; +import diffLineNoteForm from './diff_line_note_form.vue'; + +export default { + components: { + diffDiscussions, + diffLineNoteForm, + }, + props: { + line: { + type: Object, + required: true, + }, + diffFile: { + type: Object, + required: true, + }, + diffLines: { + type: Array, + required: true, + }, + lineIndex: { + type: Number, + required: true, + }, + }, + computed: { + ...mapState({ + diffLineCommentForms: state => state.diffs.diffLineCommentForms, + }), + ...mapGetters(['discussionsByLineCode']), + isDiscussionExpanded() { + if (!this.discussions.length) { + return false; + } + + return this.discussions.every(discussion => discussion.expanded); + }, + hasCommentForm() { + return this.diffLineCommentForms[this.line.lineCode]; + }, + discussions() { + return this.discussionsByLineCode[this.line.lineCode] || []; + }, + shouldRender() { + return this.isDiscussionExpanded || this.hasCommentForm; + }, + className() { + return this.discussions.length ? '' : 'js-temp-notes-holder'; + }, + }, +}; +</script> + +<template> + <tr + v-if="shouldRender" + :class="className" + class="notes_holder" + > + <td + class="notes_line" + colspan="2" + ></td> + <td class="notes_content"> + <div class="content"> + <diff-discussions + :discussions="discussions" + /> + <diff-line-note-form + v-if="diffLineCommentForms[line.lineCode]" + :diff-file="diffFile" + :diff-lines="diffLines" + :line="line" + :note-target-line="diffLines[lineIndex]" + /> + </div> + </td> + </tr> +</template> diff --git a/app/assets/javascripts/diffs/components/inline_diff_view.vue b/app/assets/javascripts/diffs/components/inline_diff_view.vue index 0ed3dc7f3ad..e72f85df77a 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_view.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_view.vue @@ -1,34 +1,12 @@ <script> import diffContentMixin from '../mixins/diff_content'; -import { - MATCH_LINE_TYPE, - CONTEXT_LINE_TYPE, - OLD_NO_NEW_LINE_TYPE, - NEW_NO_NEW_LINE_TYPE, - LINE_HOVER_CLASS_NAME, - LINE_UNFOLD_CLASS_NAME, -} from '../constants'; +import inlineDiffCommentRow from './inline_diff_comment_row.vue'; export default { - mixins: [diffContentMixin], - methods: { - handleMouse(lineCode, isOver) { - this.hoveredLineCode = isOver ? lineCode : null; - }, - getLineClass(line) { - const isSameLine = this.hoveredLineCode && this.hoveredLineCode === line.lineCode; - const isMatchLine = line.type === MATCH_LINE_TYPE; - const isContextLine = line.type === CONTEXT_LINE_TYPE; - const isMetaLine = line.type === OLD_NO_NEW_LINE_TYPE || line.type === NEW_NO_NEW_LINE_TYPE; - - return { - [line.type]: line.type, - [LINE_UNFOLD_CLASS_NAME]: isMatchLine, - [LINE_HOVER_CLASS_NAME]: - this.isLoggedIn && isSameLine && !isMatchLine && !isContextLine && !isMetaLine, - }; - }, + components: { + inlineDiffCommentRow, }, + mixins: [diffContentMixin], }; </script> @@ -36,81 +14,24 @@ export default { <table :class="userColorScheme" :data-commit-id="commitId" - class="code diff-wrap-lines js-syntax-highlight text-file"> + class="code diff-wrap-lines js-syntax-highlight text-file js-diff-inline-view"> <tbody> <template v-for="(line, index) in normalizedDiffLines" > - <tr - :id="line.lineCode || `${fileHash}_${line.oldLine}_${line.newLine}`" + <diff-table-row + :diff-file="diffFile" + :line="line" + :is-bottom="index + 1 === diffLinesLength" :key="line.lineCode" - :class="getRowClass(line)" - class="line_holder" - @mouseover="handleMouse(line.lineCode, true)" - @mouseout="handleMouse(line.lineCode, false)" - > - <td - :class="getLineClass(line)" - class="diff-line-num old_line" - > - <diff-line-gutter-content - :file-hash="fileHash" - :line-type="line.type" - :line-code="line.lineCode" - :line-number="line.oldLine" - :meta-data="line.metaData" - :show-comment-button="true" - :context-lines-path="diffFile.contextLinesPath" - :is-bottom="index + 1 === diffLinesLength" - @showCommentForm="handleShowCommentForm" - /> - </td> - <td - :class="getLineClass(line)" - class="diff-line-num new_line" - > - <diff-line-gutter-content - :file-hash="fileHash" - :line-type="line.type" - :line-code="line.lineCode" - :line-number="line.newLine" - :meta-data="line.metaData" - :is-bottom="index + 1 === diffLinesLength" - :context-lines-path="diffFile.contextLinesPath" - /> - </td> - <td - :class="line.type" - class="line_content" - v-html="line.richText" - > - </td> - </tr> - <tr - v-if="isDiscussionExpanded(line.lineCode) || diffLineCommentForms[line.lineCode]" + /> + <inline-diff-comment-row + :diff-file="diffFile" + :diff-lines="normalizedDiffLines" + :line="line" + :line-index="index" :key="index" - :class="discussionsByLineCode[line.lineCode] ? '' : 'js-temp-notes-holder'" - class="notes_holder" - > - <td - class="notes_line" - colspan="2" - ></td> - <td class="notes_content"> - <div class="content"> - <diff-discussions - :discussions="discussionsByLineCode[line.lineCode] || []" - /> - <diff-line-note-form - v-if="diffLineCommentForms[line.lineCode]" - :diff-file="diffFile" - :diff-lines="diffLines" - :line="line" - :note-target-line="diffLines[index]" - /> - </div> - </td> - </tr> + /> </template> </tbody> </table> diff --git a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue new file mode 100644 index 00000000000..5f33ec7a3c2 --- /dev/null +++ b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue @@ -0,0 +1,129 @@ +<script> +import { mapState, mapGetters } from 'vuex'; +import diffDiscussions from './diff_discussions.vue'; +import diffLineNoteForm from './diff_line_note_form.vue'; + +export default { + components: { + diffDiscussions, + diffLineNoteForm, + }, + props: { + line: { + type: Object, + required: true, + }, + diffFile: { + type: Object, + required: true, + }, + diffLines: { + type: Array, + required: true, + }, + lineIndex: { + type: Number, + required: true, + }, + }, + computed: { + ...mapState({ + diffLineCommentForms: state => state.diffs.diffLineCommentForms, + }), + ...mapGetters(['discussionsByLineCode']), + leftLineCode() { + return this.line.left.lineCode; + }, + rightLineCode() { + return this.line.right.lineCode; + }, + hasDiscussion() { + const discussions = this.discussionsByLineCode; + + return discussions[this.leftLineCode] || discussions[this.rightLineCode]; + }, + hasExpandedDiscussionOnLeft() { + const discussions = this.discussionsByLineCode[this.leftLineCode]; + + return discussions ? discussions.every(discussion => discussion.expanded) : false; + }, + hasExpandedDiscussionOnRight() { + const discussions = this.discussionsByLineCode[this.rightLineCode]; + + return discussions ? discussions.every(discussion => discussion.expanded) : false; + }, + hasAnyExpandedDiscussion() { + return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight; + }, + shouldRenderDiscussionsRow() { + const hasDiscussion = this.hasDiscussion && this.hasAnyExpandedDiscussion; + const hasCommentFormOnLeft = this.diffLineCommentForms[this.leftLineCode]; + const hasCommentFormOnRight = this.diffLineCommentForms[this.rightLineCode]; + + return hasDiscussion || hasCommentFormOnLeft || hasCommentFormOnRight; + }, + shouldRenderDiscussionsOnLeft() { + return this.discussionsByLineCode[this.leftLineCode] && this.hasExpandedDiscussionOnLeft; + }, + shouldRenderDiscussionsOnRight() { + return ( + this.discussionsByLineCode[this.rightLineCode] && + this.hasExpandedDiscussionOnRight && + this.line.right.type + ); + }, + className() { + return this.hasDiscussion ? '' : 'js-temp-notes-holder'; + }, + }, +}; +</script> + +<template> + <tr + v-if="shouldRenderDiscussionsRow" + :class="className" + class="notes_holder" + > + <td class="notes_line old"></td> + <td class="notes_content parallel old"> + <div + v-if="shouldRenderDiscussionsOnLeft" + class="content" + > + <diff-discussions + :discussions="discussionsByLineCode[leftLineCode]" + /> + </div> + <diff-line-note-form + v-if="diffLineCommentForms[leftLineCode] && + diffLineCommentForms[leftLineCode]" + :diff-file="diffFile" + :diff-lines="diffLines" + :line="line.left" + :note-target-line="diffLines[lineIndex].left" + position="left" + /> + </td> + <td class="notes_line new"></td> + <td class="notes_content parallel new"> + <div + v-if="shouldRenderDiscussionsOnRight" + class="content" + > + <diff-discussions + :discussions="discussionsByLineCode[rightLineCode]" + /> + </div> + <diff-line-note-form + v-if="diffLineCommentForms[rightLineCode] && + diffLineCommentForms[rightLineCode] && line.right.type" + :diff-file="diffFile" + :diff-lines="diffLines" + :line="line.right" + :note-target-line="diffLines[lineIndex].right" + position="right" + /> + </td> + </tr> +</template> diff --git a/app/assets/javascripts/diffs/components/parallel_diff_view.vue b/app/assets/javascripts/diffs/components/parallel_diff_view.vue index 2ddf8e6c6ed..ed92b4ee249 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_view.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_view.vue @@ -1,17 +1,12 @@ <script> import diffContentMixin from '../mixins/diff_content'; -import { - EMPTY_CELL_TYPE, - MATCH_LINE_TYPE, - CONTEXT_LINE_TYPE, - OLD_NO_NEW_LINE_TYPE, - NEW_NO_NEW_LINE_TYPE, - LINE_HOVER_CLASS_NAME, - LINE_UNFOLD_CLASS_NAME, - LINE_POSITION_RIGHT, -} from '../constants'; +import parallelDiffCommentRow from './parallel_diff_comment_row.vue'; +import { EMPTY_CELL_TYPE } from '../constants'; export default { + components: { + parallelDiffCommentRow, + }, mixins: [diffContentMixin], computed: { parallelDiffLines() { @@ -26,77 +21,6 @@ export default { }); }, }, - methods: { - hasDiscussion(line) { - const discussions = this.discussionsByLineCode; - const hasDiscussion = discussions[line.left.lineCode] || discussions[line.right.lineCode]; - - return hasDiscussion; - }, - getClassName(line, position) { - const { type, lineCode } = line[position]; - const isMatchLine = type === MATCH_LINE_TYPE; - const isContextLine = !isMatchLine && type !== EMPTY_CELL_TYPE && type !== CONTEXT_LINE_TYPE; - const isMetaLine = type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE; - const isSameLine = this.hoveredLineCode && this.hoveredLineCode === lineCode; - const isSameSection = position === this.hoveredSection; - - return { - [type]: type, - [LINE_UNFOLD_CLASS_NAME]: isMatchLine, - [LINE_HOVER_CLASS_NAME]: - this.isLoggedIn && isContextLine && isSameLine && isSameSection && !isMetaLine, - }; - }, - handleMouse(e, line, isHover) { - if (isHover) { - const cell = e.target.closest('td'); - - if (this.$refs.leftLines.indexOf(cell) > -1) { - this.hoveredLineCode = line.left.lineCode; - this.hoveredSection = 'left'; - } else if (this.$refs.rightLines.indexOf(cell) > -1) { - this.hoveredLineCode = line.right.lineCode; - this.hoveredSection = 'right'; - } - } else { - this.hoveredLineCode = null; - this.hoveredSection = null; - } - }, - shouldRenderDiscussionsRow(line) { - const hasDiscussion = this.hasDiscussion(line) && this.hasAnyExpandedDiscussion(line); - const hasCommentFormOnLeft = this.diffLineCommentForms[line.left.lineCode]; - const hasCommentFormOnRight = this.diffLineCommentForms[line.right.lineCode]; - - return hasDiscussion || hasCommentFormOnLeft || hasCommentFormOnRight; - }, - shouldRenderDiscussions(line, position) { - const { lineCode } = line[position]; - let render = this.discussionsByLineCode[lineCode] && this.isDiscussionExpanded(lineCode); - - // Avoid rendering context line discussions on the right side in parallel view - if (position === LINE_POSITION_RIGHT) { - render = render && line.right.type; - } - - return render; - }, - hasAnyExpandedDiscussion(line) { - const isLeftExpanded = this.isDiscussionExpanded(line.left.lineCode); - const isRightExpanded = this.isDiscussionExpanded(line.right.lineCode); - - return isLeftExpanded || isRightExpanded; - }, - getLineCode(line, side) { - const lineCode = side.lineCode; - if (lineCode) { - return lineCode; - } - - return `${this.fileHash}_${line.left.oldLine}_${line.right.newLine}`; - }, - }, }; </script> @@ -104,119 +28,26 @@ export default { <div :class="userColorScheme" :data-commit-id="commitId" - class="code diff-wrap-lines js-syntax-highlight text-file"> + class="code diff-wrap-lines js-syntax-highlight text-file" + > <table> <tbody> <template v-for="(line, index) in parallelDiffLines" > - <tr + <diff-table-row + :diff-file="diffFile" + :line="line" + :is-bottom="index + 1 === diffLinesLength" :key="index" - :class="getRowClass(line)" - class="line_holder parallel" - @mouseover="handleMouse($event, line, true)" - @mouseout="handleMouse($event, line, false)" - > - <td - ref="leftLines" - :class="getClassName(line, 'left')" - class="diff-line-num old_line" - > - <diff-line-gutter-content - :file-hash="fileHash" - :line-type="line.left.type" - :line-code="line.left.lineCode" - :line-number="line.left.oldLine" - :meta-data="line.left.metaData" - :show-comment-button="true" - :context-lines-path="diffFile.contextLinesPath" - :is-bottom="index + 1 === diffLinesLength" - line-position="left" - @showCommentForm="handleShowCommentForm" - /> - </td> - <td - ref="leftLines" - :class="getClassName(line, 'left')" - :id="getLineCode(line, line.left)" - class="line_content parallel left-side" - v-html="line.left.richText" - > - </td> - <td - ref="rightLines" - :class="getClassName(line, 'right')" - class="diff-line-num new_line" - > - <diff-line-gutter-content - :file-hash="fileHash" - :line-type="line.right.type" - :line-code="line.right.lineCode" - :line-number="line.right.newLine" - :meta-data="line.right.metaData" - :show-comment-button="true" - :context-lines-path="diffFile.contextLinesPath" - :is-bottom="index + 1 === diffLinesLength" - line-position="right" - @showCommentForm="handleShowCommentForm" - /> - </td> - <td - ref="rightLines" - :class="getClassName(line, 'right')" - :id="getLineCode(line, line.right)" - class="line_content parallel right-side" - v-html="line.right.richText" - > - </td> - </tr> - <tr - v-if="shouldRenderDiscussionsRow(line)" + /> + <parallel-diff-comment-row :key="line.left.lineCode || line.right.lineCode" - :class="hasDiscussion(line) ? '' : 'js-temp-notes-holder'" - class="notes_holder" - > - <td class="notes_line old"></td> - <td class="notes_content parallel old"> - <div - v-if="shouldRenderDiscussions(line, 'left')" - class="content" - > - <diff-discussions - :discussions="discussionsByLineCode[line.left.lineCode]" - /> - </div> - <diff-line-note-form - v-if="diffLineCommentForms[line.left.lineCode] && - diffLineCommentForms[line.left.lineCode]" - :diff-file="diffFile" - :diff-lines="diffLines" - :line="line.left" - :note-target-line="diffLines[index].left" - position="left" - /> - </td> - <td class="notes_line new"></td> - <td class="notes_content parallel new"> - <div - v-if="shouldRenderDiscussions(line, 'right')" - class="content" - > - <diff-discussions - :discussions="discussionsByLineCode[line.right.lineCode]" - /> - </div> - <diff-line-note-form - v-if="diffLineCommentForms[line.right.lineCode] && - diffLineCommentForms[line.right.lineCode] && line.right.type" - :diff-file="diffFile" - :diff-lines="diffLines" - :line="line.right" - :note-target-line="diffLines[index].right" - position="right" - /> - </td> - </tr> + :line="line" + :diff-file="diffFile" + :diff-lines="parallelDiffLines" + :line-index="index" + /> </template> </tbody> </table> diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js index 1a7478b307e..2fa8367f528 100644 --- a/app/assets/javascripts/diffs/constants.js +++ b/app/assets/javascripts/diffs/constants.js @@ -7,12 +7,15 @@ export const CONTEXT_LINE_TYPE = 'context'; export const EMPTY_CELL_TYPE = 'empty-cell'; export const COMMENT_FORM_TYPE = 'commentForm'; export const DIFF_NOTE_TYPE = 'DiffNote'; +export const NOTE_TYPE = 'Note'; export const NEW_LINE_TYPE = 'new'; export const OLD_LINE_TYPE = 'old'; export const TEXT_DIFF_POSITION_TYPE = 'text'; export const LINE_POSITION_LEFT = 'left'; export const LINE_POSITION_RIGHT = 'right'; +export const LINE_SIDE_LEFT = 'left-side'; +export const LINE_SIDE_RIGHT = 'right-side'; export const DIFF_VIEW_COOKIE_NAME = 'diff_view'; export const LINE_HOVER_CLASS_NAME = 'is-over'; diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js index f6840f87034..aae89109c27 100644 --- a/app/assets/javascripts/diffs/index.js +++ b/app/assets/javascripts/diffs/index.js @@ -16,6 +16,7 @@ export default function initDiffsApp(store) { return { endpoint: dataset.endpoint, + projectPath: dataset.projectPath, currentUser: convertObjectPropsToCamelCase(JSON.parse(dataset.currentUserData), { deep: true, }), @@ -31,6 +32,7 @@ export default function initDiffsApp(store) { props: { endpoint: this.endpoint, currentUser: this.currentUser, + projectPath: this.projectPath, shouldShow: this.activeTab === 'diffs', }, }); diff --git a/app/assets/javascripts/diffs/mixins/diff_content.js b/app/assets/javascripts/diffs/mixins/diff_content.js index bef06ad2b52..ebb511d3a7e 100644 --- a/app/assets/javascripts/diffs/mixins/diff_content.js +++ b/app/assets/javascripts/diffs/mixins/diff_content.js @@ -1,9 +1,9 @@ -import { mapState, mapGetters, mapActions } from 'vuex'; +import { mapGetters } from 'vuex'; import diffDiscussions from '../components/diff_discussions.vue'; import diffLineGutterContent from '../components/diff_line_gutter_content.vue'; import diffLineNoteForm from '../components/diff_line_note_form.vue'; +import diffTableRow from '../components/diff_table_row.vue'; import { trimFirstCharOfLineContent } from '../store/utils'; -import { CONTEXT_LINE_TYPE, CONTEXT_LINE_CLASS_NAME } from '../constants'; export default { props: { @@ -16,22 +16,14 @@ export default { required: true, }, }, - data() { - return { - hoveredLineCode: null, - hoveredSection: null, - }; - }, components: { diffDiscussions, + diffTableRow, diffLineNoteForm, diffLineGutterContent, }, computed: { - ...mapState({ - diffLineCommentForms: state => state.diffs.diffLineCommentForms, - }), - ...mapGetters(['discussionsByLineCode', 'isLoggedIn', 'commit']), + ...mapGetters(['commit']), commitId() { return this.commit && this.commit.id; }, @@ -41,15 +33,15 @@ export default { normalizedDiffLines() { return this.diffLines.map(line => { if (line.richText) { - return this.trimFirstChar(line); + return trimFirstCharOfLineContent(line); } if (line.left) { - Object.assign(line, { left: this.trimFirstChar(line.left) }); + Object.assign(line, { left: trimFirstCharOfLineContent(line.left) }); } if (line.right) { - Object.assign(line, { right: this.trimFirstChar(line.right) }); + Object.assign(line, { right: trimFirstCharOfLineContent(line.right) }); } return line; @@ -62,28 +54,4 @@ export default { return this.diffFile.fileHash; }, }, - methods: { - ...mapActions(['showCommentForm', 'cancelCommentForm']), - getRowClass(line) { - const isContextLine = line.left - ? line.left.type === CONTEXT_LINE_TYPE - : line.type === CONTEXT_LINE_TYPE; - - return { - [line.type]: line.type, - [CONTEXT_LINE_CLASS_NAME]: isContextLine, - }; - }, - trimFirstChar(line) { - return trimFirstCharOfLineContent(line); - }, - handleShowCommentForm(params) { - this.showCommentForm({ lineCode: params.lineCode }); - }, - isDiscussionExpanded(lineCode) { - const discussions = this.discussionsByLineCode[lineCode]; - - return discussions ? discussions.every(discussion => discussion.expanded) : false; - }, - }, }; diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index f8089b314d3..bf188a44022 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -10,8 +10,9 @@ import { DIFF_VIEW_COOKIE_NAME, } from '../constants'; -export const setEndpoint = ({ commit }, endpoint) => { - commit(types.SET_ENDPOINT, endpoint); +export const setBaseConfig = ({ commit }, options) => { + const { endpoint, projectPath } = options; + commit(types.SET_BASE_CONFIG, { endpoint, projectPath }); }; export const setLoadingState = ({ commit }, state) => { @@ -86,7 +87,7 @@ export const expandAllFiles = ({ commit }) => { }; export default { - setEndpoint, + setBaseConfig, setLoadingState, fetchDiffFiles, setInlineDiffViewType, diff --git a/app/assets/javascripts/diffs/store/modules/index.js b/app/assets/javascripts/diffs/store/modules/index.js index 882a098c977..94caa131506 100644 --- a/app/assets/javascripts/diffs/store/modules/index.js +++ b/app/assets/javascripts/diffs/store/modules/index.js @@ -13,6 +13,7 @@ export default { state: { isLoading: true, endpoint: '', + basePath: '', commit: null, diffFiles: [], mergeRequestDiffs: [], diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js index a65b205b8e7..63e9239dce4 100644 --- a/app/assets/javascripts/diffs/store/mutation_types.js +++ b/app/assets/javascripts/diffs/store/mutation_types.js @@ -1,4 +1,4 @@ -export const SET_ENDPOINT = 'SET_ENDPOINT'; +export const SET_BASE_CONFIG = 'SET_BASE_CONFIG'; export const SET_LOADING = 'SET_LOADING'; export const SET_DIFF_DATA = 'SET_DIFF_DATA'; export const SET_DIFF_FILES = 'SET_DIFF_FILES'; diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index fd9ea73e33d..339a33f8b71 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -5,8 +5,9 @@ import { findDiffFile, addLineReferences, removeMatchLine, addContextLines } fro import * as types from './mutation_types'; export default { - [types.SET_ENDPOINT](state, endpoint) { - Object.assign(state, { endpoint }); + [types.SET_BASE_CONFIG](state, options) { + const { endpoint, projectPath } = options; + Object.assign(state, { endpoint, projectPath }); }, [types.SET_LOADING](state, isLoading) { @@ -73,7 +74,7 @@ export default { [types.EXPAND_ALL_FILES](state) { const diffFiles = []; - state.diffFiles.forEach((file) => { + state.diffFiles.forEach(file => { diffFiles.push({ ...file, collapsed: false, diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index 866e91057ec..5ecdccf63ad 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -292,7 +292,7 @@ if (this.model && this.model.last_deployment && this.model.last_deployment.deployable) { - const deployable = this.model.last_deployment.deployable; + const { deployable } = this.model.last_deployment; return `${deployable.name} #${deployable.id}`; } return ''; diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js index 5f2989ab854..5ce9225a4bb 100644 --- a/app/assets/javascripts/environments/stores/environments_store.js +++ b/app/assets/javascripts/environments/stores/environments_store.js @@ -146,7 +146,7 @@ export default class EnvironmentsStore { * @return {Array} */ updateEnvironmentProp(environment, prop, newValue) { - const environments = this.state.environments; + const { environments } = this.state; const updatedEnvironments = environments.map((env) => { const updateEnv = Object.assign({}, env); @@ -161,7 +161,7 @@ export default class EnvironmentsStore { } getOpenFolders() { - const environments = this.state.environments; + const { environments } = this.state; return environments.filter(env => env.isFolder && env.isOpen); } diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js index 9bc36c1f9b6..27fff488603 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js @@ -35,7 +35,7 @@ export default class DropdownUtils { // Remove the symbol for filter if (value[0] === filterSymbol) { - symbol = value[0]; + [symbol] = value; value = value.slice(1); } @@ -162,7 +162,7 @@ export default class DropdownUtils { // Determines the full search query (visual tokens + input) static getSearchQuery(untilInput = false) { - const container = FilteredSearchContainer.container; + const { container } = FilteredSearchContainer; const tokens = [].slice.call(container.querySelectorAll('.tokens-container li')); const values = []; @@ -220,7 +220,7 @@ export default class DropdownUtils { } static getInputSelectionPosition(input) { - const selectionStart = input.selectionStart; + const { selectionStart } = input; let inputValue = input.value; // Replace all spaces inside quote marks with underscores // (will continue to match entire string until an end quote is found if any) diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index d7e1de18d09..296571606d6 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -159,7 +159,7 @@ export default class FilteredSearchDropdownManager { load(key, firstLoad = false) { const mappingKey = this.mapping[key]; const glClass = mappingKey.gl; - const element = mappingKey.element; + const { element } = mappingKey; let forceShowList = false; if (!mappingKey.reference) { diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index cf5ba1e1771..81286c54c4c 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -235,7 +235,7 @@ export default class FilteredSearchManager { checkForEnter(e) { if (e.keyCode === 38 || e.keyCode === 40) { - const selectionStart = this.filteredSearchInput.selectionStart; + const { selectionStart } = this.filteredSearchInput; e.preventDefault(); this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart); @@ -496,7 +496,7 @@ export default class FilteredSearchManager { // Replace underscore with hyphen in the sanitizedkey. // e.g. 'my_reaction' => 'my-reaction' sanitizedKey = sanitizedKey.replace('_', '-'); - const symbol = match.symbol; + const { symbol } = match; let quotationsToUse = ''; if (sanitizedValue.indexOf(' ') !== -1) { diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index 600024c21c3..56fe1ab4e90 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -101,7 +101,7 @@ export default class FilteredSearchVisualTokens { static updateLabelTokenColor(tokenValueContainer, tokenValue) { const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search'); - const baseEndpoint = filteredSearchInput.dataset.baseEndpoint; + const { baseEndpoint } = filteredSearchInput.dataset; const labelsEndpoint = FilteredSearchVisualTokens.getEndpointWithQueryParams( `${baseEndpoint}/labels.json`, filteredSearchInput.dataset.endpointQueryParams, @@ -215,7 +215,7 @@ export default class FilteredSearchVisualTokens { static addFilterVisualToken(tokenName, tokenValue, canEdit) { const { lastVisualToken, isLastVisualTokenValid } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); - const addVisualTokenElement = FilteredSearchVisualTokens.addVisualTokenElement; + const { addVisualTokenElement } = FilteredSearchVisualTokens; if (isLastVisualTokenValid) { addVisualTokenElement(tokenName, tokenValue, false, canEdit); diff --git a/app/assets/javascripts/filtered_search/recent_searches_root.js b/app/assets/javascripts/filtered_search/recent_searches_root.js index f9338b82acf..c1efa9c86f4 100644 --- a/app/assets/javascripts/filtered_search/recent_searches_root.js +++ b/app/assets/javascripts/filtered_search/recent_searches_root.js @@ -29,7 +29,7 @@ class RecentSearchesRoot { } render() { - const state = this.store.state; + const { state } = this.store; this.vm = new Vue({ el: this.wrapperElement, components: { diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 9de57db48fd..09186a865e4 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -7,6 +7,16 @@ function sanitize(str) { return str.replace(/<(?:.|\n)*?>/gm, ''); } +export const defaultAutocompleteConfig = { + emojis: true, + members: true, + issues: true, + mergeRequests: true, + epics: false, + milestones: true, + labels: true, +}; + class GfmAutoComplete { constructor(dataSources) { this.dataSources = dataSources || {}; @@ -14,14 +24,7 @@ class GfmAutoComplete { this.isLoadingData = {}; } - setup(input, enableMap = { - emojis: true, - members: true, - issues: true, - milestones: true, - mergeRequests: true, - labels: true, - }) { + setup(input, enableMap = defaultAutocompleteConfig) { // Add GFM auto-completion to all input fields, that accept GFM input. this.input = input || $('.js-gfm-input'); this.enableMap = enableMap; @@ -77,7 +80,7 @@ class GfmAutoComplete { let tpl = '/${name} '; let referencePrefix = null; if (value.params.length > 0) { - referencePrefix = value.params[0][0]; + [[referencePrefix]] = value.params; if (/^[@%~]/.test(referencePrefix)) { tpl += '<%- referencePrefix %>'; } @@ -455,7 +458,7 @@ class GfmAutoComplete { static isLoading(data) { let dataToInspect = data; if (data && data.length > 0) { - dataToInspect = data[0]; + [dataToInspect] = data; } const loadingState = GfmAutoComplete.defaultLoadingData[0]; diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 45889c2d604..8d231e6c405 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -613,7 +613,7 @@ GitLabDropdown = (function() { }; GitLabDropdown.prototype.renderItem = function(data, group, index) { - var field, fieldName, html, selected, text, url, value, rowHidden; + var field, html, selected, text, url, value, rowHidden; if (!this.options.renderRow) { value = this.options.id ? this.options.id(data) : data.id; @@ -651,7 +651,7 @@ GitLabDropdown = (function() { html = this.options.renderRow.call(this.options, data, this); } else { if (!selected) { - fieldName = this.options.fieldName; + const { fieldName } = this.options; if (value) { field = this.dropdown.parent().find(`input[name='${fieldName}'][value='${value}']`); @@ -705,7 +705,8 @@ GitLabDropdown = (function() { GitLabDropdown.prototype.highlightTextMatches = function(text, term) { const occurrences = fuzzaldrinPlus.match(text, term); - const indexOf = [].indexOf; + const { indexOf } = []; + return text.split('').map(function(character, i) { if (indexOf.call(occurrences, i) !== -1) { return "<b>" + character + "</b>"; @@ -721,9 +722,9 @@ GitLabDropdown = (function() { }; GitLabDropdown.prototype.rowClicked = function(el) { - var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking; + var field, groupName, isInput, selectedIndex, selectedObject, value, isMarking; - fieldName = this.options.fieldName; + const { fieldName } = this.options; isInput = $(this.el).is('input'); if (this.renderedData) { groupName = el.data('group'); diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index 9f5eba353d7..f802971a3ca 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -1,14 +1,14 @@ import $ from 'jquery'; import autosize from 'autosize'; -import GfmAutoComplete from './gfm_auto_complete'; +import GfmAutoComplete, * as GFMConfig from './gfm_auto_complete'; import dropzoneInput from './dropzone_input'; import { addMarkdownListeners, removeMarkdownListeners } from './lib/utils/text_markdown'; export default class GLForm { - constructor(form, enableGFM = false) { + constructor(form, enableGFM = {}) { this.form = form; this.textarea = this.form.find('textarea.js-gfm-input'); - this.enableGFM = enableGFM; + this.enableGFM = Object.assign({}, GFMConfig.defaultAutocompleteConfig, enableGFM); // Before we start, we should clean up any previous data for this form this.destroy(); // Setup the form @@ -34,14 +34,7 @@ export default class GLForm { // remove notify commit author checkbox for non-commit notes gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion')); this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources); - this.autoComplete.setup(this.form.find('.js-gfm-input'), { - emojis: true, - members: this.enableGFM, - issues: this.enableGFM, - milestones: this.enableGFM, - mergeRequests: this.enableGFM, - labels: this.enableGFM, - }); + this.autoComplete.setup(this.form.find('.js-gfm-input'), this.enableGFM); dropzoneInput(this.form); autosize(this.textarea); } diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index 57eaac72906..83a9008a94b 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -29,7 +29,7 @@ export default () => { groupsApp, }, data() { - const dataset = this.$options.el.dataset; + const { dataset } = this.$options.el; const hideProjects = dataset.hideProjects === 'true'; const store = new GroupsStore(hideProjects); const service = new GroupsService(dataset.endpoint); @@ -42,7 +42,7 @@ export default () => { }; }, beforeMount() { - const dataset = this.$options.el.dataset; + const { dataset } = this.$options.el; let groupFilterList = null; const form = document.querySelector(dataset.formSel); const filter = document.querySelector(dataset.filterSel); diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue index b4f3778d946..eb7cb9745ec 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue @@ -10,7 +10,7 @@ export default { }, computed: { ...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']), - ...mapGetters(['currentProject']), + ...mapGetters(['currentProject', 'currentBranch']), commitToCurrentBranchText() { return sprintf( __('Commit to %{branchName} branch'), @@ -22,17 +22,30 @@ export default { return this.changedFiles.length > 0 && this.stagedFiles.length > 0; }, }, + watch: { + disableMergeRequestRadio() { + this.updateSelectedCommitAction(); + }, + }, mounted() { - if (this.disableMergeRequestRadio) { - this.updateCommitAction(consts.COMMIT_TO_CURRENT_BRANCH); - } + this.updateSelectedCommitAction(); }, methods: { ...mapActions('commit', ['updateCommitAction']), + updateSelectedCommitAction() { + if (this.currentBranch && !this.currentBranch.can_push) { + this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH); + } else if (this.disableMergeRequestRadio) { + this.updateCommitAction(consts.COMMIT_TO_CURRENT_BRANCH); + } + }, }, commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH, commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH, commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR, + currentBranchPermissionsTooltip: __( + "This option is disabled as you don't have write permissions for the current branch", + ), }; </script> @@ -40,9 +53,11 @@ export default { <div class="append-bottom-15 ide-commit-radios"> <radio-group :value="$options.commitToCurrentBranch" - :checked="true" + :disabled="currentBranch && !currentBranch.can_push" + :title="$options.currentBranchPermissionsTooltip" > <span + class="ide-radio-label" v-html="commitToCurrentBranchText" > </span> @@ -56,6 +71,7 @@ export default { v-if="currentProject.merge_requests_enabled" :value="$options.commitToNewBranchMR" :label="__('Create a new branch and merge request')" + :title="__('This option is disabled while you still have unstaged changes')" :show-input="true" :disabled="disableMergeRequestRadio" /> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue index 14c74687ab4..ee8eb206980 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue @@ -24,7 +24,7 @@ export default { ...mapState(['changedFiles', 'stagedFiles', 'currentActivityView', 'lastCommitMsg']), ...mapState('commit', ['commitMessage', 'submitCommitLoading']), ...mapGetters(['hasChanges']), - ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']), + ...mapGetters('commit', ['discardDraftButtonDisabled', 'preBuiltCommitMessage']), overviewText() { return sprintf( __( @@ -36,6 +36,9 @@ export default { }, ); }, + commitButtonText() { + return this.stagedFiles.length ? __('Commit') : __('Stage & Commit'); + }, }, watch: { currentActivityView() { @@ -136,14 +139,14 @@ export default { </transition> <commit-message-field :text="commitMessage" + :placeholder="preBuiltCommitMessage" @input="updateCommitMessage" /> <div class="clearfix prepend-top-15"> <actions /> <loading-button :loading="submitCommitLoading" - :disabled="commitButtonDisabled" - :label="__('Commit')" + :label="commitButtonText" container-class="btn btn-success btn-sm float-left" @click="commitChanges" /> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue index 5cda7967130..ee21eeda3cd 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue @@ -89,14 +89,14 @@ export default { <template> <div class="multi-file-commit-list-item position-relative"> - <button + <div v-tooltip :title="tooltipTitle" :class="{ 'is-active': isActive }" - type="button" class="multi-file-commit-list-path w-100 border-0 ml-0 mr-0" + role="button" @dblclick="fileAction" @click="openFileInEditor" > @@ -107,7 +107,7 @@ export default { :css-classes="iconClass" />{{ file.name }} </span> - </button> + </div> <component :is="actionComponent" :path="file.path" diff --git a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue index 40496c80a46..37ca108fafc 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue @@ -16,6 +16,10 @@ export default { type: String, required: true, }, + placeholder: { + type: String, + required: true, + }, }, data() { return { @@ -114,7 +118,7 @@ export default { </div> <textarea ref="textarea" - :placeholder="__('Write a commit message...')" + :placeholder="placeholder" :value="text" class="note-textarea ide-commit-message-textarea" name="commit-message" diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue index 35ab3fd11df..969e2aa61c4 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue @@ -1,6 +1,5 @@ <script> import { mapActions, mapState, mapGetters } from 'vuex'; -import { __ } from '~/locale'; import tooltip from '~/vue_shared/directives/tooltip'; export default { @@ -32,14 +31,17 @@ export default { required: false, default: false, }, + title: { + type: String, + required: false, + default: '', + }, }, computed: { ...mapState('commit', ['commitAction']), ...mapGetters('commit', ['newBranchName']), tooltipTitle() { - return this.disabled - ? __('This option is disabled while you still have unstaged changes') - : ''; + return this.disabled ? this.title : ''; }, }, methods: { diff --git a/app/assets/javascripts/ide/components/error_message.vue b/app/assets/javascripts/ide/components/error_message.vue new file mode 100644 index 00000000000..e7408264c80 --- /dev/null +++ b/app/assets/javascripts/ide/components/error_message.vue @@ -0,0 +1,69 @@ +<script> +import { mapActions } from 'vuex'; +import LoadingIcon from '../../vue_shared/components/loading_icon.vue'; + +export default { + components: { + LoadingIcon, + }, + props: { + message: { + type: Object, + required: true, + }, + }, + data() { + return { + isLoading: false, + }; + }, + methods: { + ...mapActions(['setErrorMessage']), + clickAction() { + if (this.isLoading) return; + + this.isLoading = true; + + this.$store + .dispatch(this.message.action, this.message.actionPayload) + .then(() => { + this.isLoading = false; + }) + .catch(() => { + this.isLoading = false; + }); + }, + clickFlash() { + if (!this.message.action) { + this.setErrorMessage(null); + } + }, + }, +}; +</script> + +<template> + <div + class="flash-container flash-container-page" + @click="clickFlash" + > + <div class="flash-alert"> + <span + v-html="message.text" + > + </span> + <button + v-if="message.action" + type="button" + class="flash-action text-white p-0 border-top-0 border-right-0 border-left-0 bg-transparent" + @click.stop.prevent="clickAction" + > + {{ message.actionText }} + <loading-icon + v-show="isLoading" + inline + /> + </button> + </div> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/file_finder/item.vue b/app/assets/javascripts/ide/components/file_finder/item.vue index a4cf3edb981..f5252ce7706 100644 --- a/app/assets/javascripts/ide/components/file_finder/item.vue +++ b/app/assets/javascripts/ide/components/file_finder/item.vue @@ -30,7 +30,7 @@ export default { }, computed: { pathWithEllipsis() { - const path = this.file.path; + const { path } = this.file; return path.length < MAX_PATH_LENGTH ? path diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index f5f7f967a92..9f016e0338f 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -7,6 +7,7 @@ import IdeStatusBar from './ide_status_bar.vue'; import RepoEditor from './repo_editor.vue'; import FindFile from './file_finder/index.vue'; import RightPane from './panes/right.vue'; +import ErrorMessage from './error_message.vue'; const originalStopCallback = Mousetrap.stopCallback; @@ -18,6 +19,7 @@ export default { RepoEditor, FindFile, RightPane, + ErrorMessage, }, computed: { ...mapState([ @@ -28,6 +30,7 @@ export default { 'fileFindVisible', 'emptyStateSvgPath', 'currentProjectId', + 'errorMessage', ]), ...mapGetters(['activeFile', 'hasChanges']), }, @@ -72,6 +75,10 @@ export default { <template> <article class="ide"> + <error-message + v-if="errorMessage" + :message="errorMessage" + /> <div class="ide-view" > diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue index 1814924be39..677b282bd61 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue @@ -23,6 +23,7 @@ let { result } = target; if (!isText) { + // eslint-disable-next-line prefer-destructuring result = result.split('base64,')[1]; } diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue index dedc2988618..5cd2c9ce188 100644 --- a/app/assets/javascripts/ide/components/panes/right.vue +++ b/app/assets/javascripts/ide/components/panes/right.vue @@ -69,7 +69,7 @@ export default { > <icon :size="16" - name="pipeline" + name="rocket" /> </button> </li> diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue index c2c678ff0be..50ab242ba2a 100644 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -28,7 +28,7 @@ export default { ]), ...mapState('commit', ['commitMessage', 'submitCommitLoading']), ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges', 'activeFile']), - ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']), + ...mapGetters('commit', ['discardDraftButtonDisabled']), showStageUnstageArea() { return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal); }, diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue index c34547fcc60..f490a3a2a39 100644 --- a/app/assets/javascripts/ide/components/repo_file.vue +++ b/app/assets/javascripts/ide/components/repo_file.vue @@ -95,24 +95,53 @@ export default { return this.file.changed || this.file.tempFile || this.file.staged; }, }, + mounted() { + if (this.hasPathAtCurrentRoute()) { + this.scrollIntoView(true); + } + }, updated() { if (this.file.type === 'blob' && this.file.active) { - this.$el.scrollIntoView({ - behavior: 'smooth', - block: 'nearest', - }); + this.scrollIntoView(); } }, methods: { ...mapActions(['toggleTreeOpen']), clickFile() { // Manual Action if a tree is selected/opened - if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) { + if (this.isTree && this.hasUrlAtCurrentRoute()) { this.toggleTreeOpen(this.file.path); } router.push(`/project${this.file.url}`); }, + scrollIntoView(isInit = false) { + const block = isInit && this.isTree ? 'center' : 'nearest'; + + this.$el.scrollIntoView({ + behavior: 'smooth', + block, + }); + }, + hasPathAtCurrentRoute() { + if (!this.$router || !this.$router.currentRoute) { + return false; + } + + // - strip route up to "/-/" and ending "/" + const routePath = this.$router.currentRoute.path + .replace(/^.*?[/]-[/]/g, '') + .replace(/[/]$/g, ''); + + // - strip ending "/" + const filePath = this.file.path + .replace(/[/]$/g, ''); + + return filePath === routePath; + }, + hasUrlAtCurrentRoute() { + return this.$router.currentRoute.path === `/project${this.file.url}`; + }, }, }; </script> diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue index 1ad52c1bd83..03772ae4a4c 100644 --- a/app/assets/javascripts/ide/components/repo_tab.vue +++ b/app/assets/javascripts/ide/components/repo_tab.vue @@ -44,6 +44,8 @@ export default { methods: { ...mapActions(['closeFile', 'updateDelayViewerUpdated', 'openPendingTab']), clickFile(tab) { + if (tab.active) return; + this.updateDelayViewerUpdated(true); if (tab.pending) { diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js index b52618f4fde..cc8dbb942d8 100644 --- a/app/assets/javascripts/ide/ide_router.js +++ b/app/assets/javascripts/ide/ide_router.js @@ -95,14 +95,6 @@ router.beforeEach((to, from, next) => { } }) .catch(e => { - flash( - 'Error while loading the branch files. Please try again.', - 'alert', - document, - null, - false, - true, - ); throw e; }); } else if (to.params.mrid) { diff --git a/app/assets/javascripts/ide/lib/diff/diff_worker.js b/app/assets/javascripts/ide/lib/diff/diff_worker.js index f09930e8158..78b2eab6399 100644 --- a/app/assets/javascripts/ide/lib/diff/diff_worker.js +++ b/app/assets/javascripts/ide/lib/diff/diff_worker.js @@ -2,7 +2,7 @@ import { computeDiff } from './diff'; // eslint-disable-next-line no-restricted-globals self.addEventListener('message', (e) => { - const data = e.data; + const { data } = e; // eslint-disable-next-line no-restricted-globals self.postMessage({ diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js index da9de25302a..5e642067141 100644 --- a/app/assets/javascripts/ide/services/index.js +++ b/app/assets/javascripts/ide/services/index.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import VueResource from 'vue-resource'; +import axios from '~/lib/utils/axios_utils'; import Api from '~/api'; Vue.use(VueResource); @@ -69,11 +70,7 @@ export default { }, getFiles(projectUrl, branchId) { const url = `${projectUrl}/files/${branchId}`; - return Vue.http.get(url, { - params: { - format: 'json', - }, - }); + return axios.get(url, { params: { format: 'json' } }); }, lastCommitPipelines({ getters }) { const commitSha = getters.lastCommit.id; diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index 3dc365eaead..5e91fa915ff 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -175,6 +175,9 @@ export const setRightPane = ({ commit }, view) => { export const setLinks = ({ commit }, links) => commit(types.SET_LINKS, links); +export const setErrorMessage = ({ commit }, errorMessage) => + commit(types.SET_ERROR_MESSAGE, errorMessage); + export * from './actions/tree'; export * from './actions/file'; export * from './actions/project'; diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index 74f9c112f5a..29995a29d1a 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -8,7 +8,7 @@ import { setPageTitle } from '../utils'; import { viewerTypes } from '../../constants'; export const closeFile = ({ commit, state, dispatch }, file) => { - const path = file.path; + const { path } = file; const indexOfClosedFile = state.openFiles.findIndex(f => f.key === file.key); const fileWasActive = file.active; diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index 0b99bce4a8e..ab5cd8e4742 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -1,7 +1,10 @@ +import _ from 'underscore'; import flash from '~/flash'; -import { __ } from '~/locale'; +import { __, sprintf } from '~/locale'; import service from '../../services'; +import api from '../../../api'; import * as types from '../mutation_types'; +import router from '../../ide_router'; export const getProjectData = ({ commit, state }, { namespace, projectId, force = false } = {}) => new Promise((resolve, reject) => { @@ -32,7 +35,10 @@ export const getProjectData = ({ commit, state }, { namespace, projectId, force } }); -export const getBranchData = ({ commit, state }, { projectId, branchId, force = false } = {}) => +export const getBranchData = ( + { commit, dispatch, state }, + { projectId, branchId, force = false } = {}, +) => new Promise((resolve, reject) => { if ( typeof state.projects[`${projectId}`] === 'undefined' || @@ -51,15 +57,19 @@ export const getBranchData = ({ commit, state }, { projectId, branchId, force = commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id }); resolve(data); }) - .catch(() => { - flash( - __('Error loading branch data. Please try again.'), - 'alert', - document, - null, - false, - true, - ); + .catch(e => { + if (e.response.status === 404) { + dispatch('showBranchNotFoundError', branchId); + } else { + flash( + __('Error loading branch data. Please try again.'), + 'alert', + document, + null, + false, + true, + ); + } reject(new Error(`Branch not loaded - ${projectId}/${branchId}`)); }); } else { @@ -80,3 +90,37 @@ export const refreshLastCommitData = ({ commit }, { projectId, branchId } = {}) .catch(() => { flash(__('Error loading last commit.'), 'alert', document, null, false, true); }); + +export const createNewBranchFromDefault = ({ state, dispatch, getters }, branch) => + api + .createBranch(state.currentProjectId, { + ref: getters.currentProject.default_branch, + branch, + }) + .then(() => { + dispatch('setErrorMessage', null); + router.push(`${router.currentRoute.path}?${Date.now()}`); + }) + .catch(() => { + dispatch('setErrorMessage', { + text: __('An error occured creating the new branch.'), + action: 'createNewBranchFromDefault', + actionText: __('Please try again'), + actionPayload: branch, + }); + }); + +export const showBranchNotFoundError = ({ dispatch }, branchId) => { + dispatch('setErrorMessage', { + text: sprintf( + __("Branch %{branchName} was not found in this project's repository."), + { + branchName: `<strong>${_.escape(branchId)}</strong>`, + }, + false, + ), + action: 'createNewBranchFromDefault', + actionText: __('Create branch'), + actionPayload: branchId, + }); +}; diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js index cc5116413f7..dcdd900fc7e 100644 --- a/app/assets/javascripts/ide/stores/actions/tree.js +++ b/app/assets/javascripts/ide/stores/actions/tree.js @@ -1,5 +1,6 @@ import { normalizeHeaders } from '~/lib/utils/common_utils'; import flash from '~/flash'; +import { __ } from '../../../locale'; import service from '../../services'; import * as types from '../mutation_types'; import { findEntry } from '../utils'; @@ -9,6 +10,17 @@ export const toggleTreeOpen = ({ commit }, path) => { commit(types.TOGGLE_TREE_OPEN, path); }; +export const showTreeEntry = ({ commit, dispatch, state }, path) => { + const entry = state.entries[path]; + const parentPath = entry ? entry.parentPath : ''; + + if (parentPath) { + commit(types.SET_TREE_OPEN, parentPath); + + dispatch('showTreeEntry', parentPath); + } +}; + export const handleTreeEntryAction = ({ commit, dispatch }, row) => { if (row.type === 'tree') { dispatch('toggleTreeOpen', row.path); @@ -21,6 +33,8 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => { } else { dispatch('getFileData', { path: row.path }); } + + dispatch('showTreeEntry', row.path); }; export const getLastCommitData = ({ state, commit, dispatch }, tree = state) => { @@ -49,16 +63,19 @@ export const getLastCommitData = ({ state, commit, dispatch }, tree = state) => .catch(() => flash('Error fetching log data.', 'alert', document, null, false, true)); }; -export const getFiles = ({ state, commit }, { projectId, branchId } = {}) => +export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = {}) => new Promise((resolve, reject) => { - if (!state.trees[`${projectId}/${branchId}`]) { + if ( + !state.trees[`${projectId}/${branchId}`] || + (state.trees[`${projectId}/${branchId}`].tree && + state.trees[`${projectId}/${branchId}`].tree.length === 0) + ) { const selectedProject = state.projects[projectId]; commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` }); service .getFiles(selectedProject.web_url, branchId) - .then(res => res.json()) - .then(data => { + .then(({ data }) => { const worker = new FilesDecoratorWorker(); worker.addEventListener('message', e => { const { entries, treeList } = e.data; @@ -86,7 +103,18 @@ export const getFiles = ({ state, commit }, { projectId, branchId } = {}) => }); }) .catch(e => { - flash('Error loading tree data. Please try again.', 'alert', document, null, false, true); + if (e.response.status === 404) { + dispatch('showBranchNotFoundError', branchId); + } else { + flash( + __('Error loading tree data. Please try again.'), + 'alert', + document, + null, + false, + true, + ); + } reject(e); }); } else { diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js index b239a605371..5ce268b0d05 100644 --- a/app/assets/javascripts/ide/stores/getters.js +++ b/app/assets/javascripts/ide/stores/getters.js @@ -82,10 +82,13 @@ export const getStagedFilesCountForPath = state => path => getChangesCountForFiles(state.stagedFiles, path); export const lastCommit = (state, getters) => { - const branch = getters.currentProject && getters.currentProject.branches[state.currentBranchId]; + const branch = getters.currentProject && getters.currentBranch; return branch ? branch.commit : null; }; +export const currentBranch = (state, getters) => + getters.currentProject && getters.currentProject.branches[state.currentBranchId]; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index 7219abc4185..69b6fe2985b 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -103,17 +103,24 @@ export const updateFilesAfterCommit = ({ commit, dispatch, rootState }, { data } export const commitChanges = ({ commit, state, getters, dispatch, rootState, rootGetters }) => { const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH; - const payload = createCommitPayload({ - branch: getters.branchName, - newBranch, - state, - rootState, - }); + const stageFilesPromise = rootState.stagedFiles.length + ? Promise.resolve() + : dispatch('stageAllChanges', null, { root: true }); commit(types.UPDATE_LOADING, true); - return service - .commit(rootState.currentProjectId, payload) + return stageFilesPromise + .then(() => { + const payload = createCommitPayload({ + branch: getters.branchName, + newBranch, + getters, + state, + rootState, + }); + + return service.commit(rootState.currentProjectId, payload); + }) .then(({ data }) => { commit(types.UPDATE_LOADING, false); diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js index d01060201f2..3db4b2f903e 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/getters.js +++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js @@ -1,3 +1,4 @@ +import { sprintf, n__ } from '../../../../locale'; import * as consts from './constants'; const BRANCH_SUFFIX_COUNT = 5; @@ -5,9 +6,6 @@ const BRANCH_SUFFIX_COUNT = 5; export const discardDraftButtonDisabled = state => state.commitMessage === '' || state.submitCommitLoading; -export const commitButtonDisabled = (state, getters, rootState) => - getters.discardDraftButtonDisabled || !rootState.stagedFiles.length; - export const newBranchName = (state, _, rootState) => `${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr( -BRANCH_SUFFIX_COUNT, @@ -28,5 +26,18 @@ export const branchName = (state, getters, rootState) => { return rootState.currentBranchId; }; +export const preBuiltCommitMessage = (state, _, rootState) => { + if (state.commitMessage) return state.commitMessage; + + const files = (rootState.stagedFiles.length + ? rootState.stagedFiles + : rootState.changedFiles + ).reduce((acc, val) => acc.concat(val.path), []); + + return sprintf(n__('Update %{files}', 'Update %{files} files', files.length), { + files: files.join(', '), + }); +}; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index 99b315ac4db..555802e1811 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -28,6 +28,7 @@ export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN'; // Tree mutation types export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA'; export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN'; +export const SET_TREE_OPEN = 'SET_TREE_OPEN'; export const SET_LAST_COMMIT_URL = 'SET_LAST_COMMIT_URL'; export const CREATE_TREE = 'CREATE_TREE'; export const REMOVE_ALL_CHANGES_FILES = 'REMOVE_ALL_CHANGES_FILES'; @@ -71,3 +72,5 @@ export const SET_RIGHT_PANE = 'SET_RIGHT_PANE'; export const CLEAR_PROJECTS = 'CLEAR_PROJECTS'; export const RESET_OPEN_FILES = 'RESET_OPEN_FILES'; + +export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE'; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index 48f1da4eccf..702be2140e2 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -163,6 +163,9 @@ export default { [types.RESET_OPEN_FILES](state) { Object.assign(state, { openFiles: [] }); }, + [types.SET_ERROR_MESSAGE](state, errorMessage) { + Object.assign(state, { errorMessage }); + }, ...projectMutations, ...mergeRequestMutation, ...fileMutations, diff --git a/app/assets/javascripts/ide/stores/mutations/tree.js b/app/assets/javascripts/ide/stores/mutations/tree.js index 1176c040fb9..2cf34af9274 100644 --- a/app/assets/javascripts/ide/stores/mutations/tree.js +++ b/app/assets/javascripts/ide/stores/mutations/tree.js @@ -6,6 +6,11 @@ export default { opened: !state.entries[path].opened, }); }, + [types.SET_TREE_OPEN](state, path) { + Object.assign(state.entries[path], { + opened: true, + }); + }, [types.CREATE_TREE](state, { treePath }) { Object.assign(state, { trees: Object.assign({}, state.trees, { diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js index 4aac4696075..be229b2c723 100644 --- a/app/assets/javascripts/ide/stores/state.js +++ b/app/assets/javascripts/ide/stores/state.js @@ -25,4 +25,5 @@ export default () => ({ fileFindVisible: false, rightPane: null, links: {}, + errorMessage: null, }); diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index 10368a4d97c..9e6b86dd844 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -105,9 +105,9 @@ export const setPageTitle = title => { document.title = title; }; -export const createCommitPayload = ({ branch, newBranch, state, rootState }) => ({ +export const createCommitPayload = ({ branch, getters, newBranch, state, rootState }) => ({ branch, - commit_message: state.commitMessage, + commit_message: state.commitMessage || getters.preBuiltCommitMessage, actions: rootState.stagedFiles.map(f => ({ action: f.tempFile ? 'create' : 'update', file_path: f.path, diff --git a/app/assets/javascripts/image_diff/helpers/dom_helper.js b/app/assets/javascripts/image_diff/helpers/dom_helper.js index 12d56714b34..a319bcccb8f 100644 --- a/app/assets/javascripts/image_diff/helpers/dom_helper.js +++ b/app/assets/javascripts/image_diff/helpers/dom_helper.js @@ -2,7 +2,8 @@ export function setPositionDataAttribute(el, options) { // Update position data attribute so that the // new comment form can use this data for ajax request const { x, y, width, height } = options; - const position = el.dataset.position; + const { position } = el.dataset; + const positionObject = Object.assign({}, JSON.parse(position), { x, y, diff --git a/app/assets/javascripts/image_diff/helpers/utils_helper.js b/app/assets/javascripts/image_diff/helpers/utils_helper.js index 28d9a969143..beec99e6934 100644 --- a/app/assets/javascripts/image_diff/helpers/utils_helper.js +++ b/app/assets/javascripts/image_diff/helpers/utils_helper.js @@ -40,8 +40,7 @@ export function getTargetSelection(event) { const x = event.offsetX; const y = event.offsetY; - const width = imageEl.width; - const height = imageEl.height; + const { width, height } = imageEl; const actualWidth = imageEl.naturalWidth; const actualHeight = imageEl.naturalHeight; diff --git a/app/assets/javascripts/init_notes.js b/app/assets/javascripts/init_notes.js index 882aedfcc76..3c71258e53b 100644 --- a/app/assets/javascripts/init_notes.js +++ b/app/assets/javascripts/init_notes.js @@ -7,10 +7,10 @@ export default () => { notesIds, now, diffView, - autocomplete, + enableGFM, } = JSON.parse(dataEl.innerHTML); // Create a singleton so that we don't need to assign // into the window object, we can just access the current isntance with Notes.instance - Notes.initialize(notesUrl, notesIds, now, diffView, autocomplete); + Notes.initialize(notesUrl, notesIds, now, diffView, enableGFM); }; diff --git a/app/assets/javascripts/issuable/auto_width_dropdown_select.js b/app/assets/javascripts/issuable/auto_width_dropdown_select.js index b2c2de9e5de..07cf1eff279 100644 --- a/app/assets/javascripts/issuable/auto_width_dropdown_select.js +++ b/app/assets/javascripts/issuable/auto_width_dropdown_select.js @@ -10,7 +10,7 @@ class AutoWidthDropdownSelect { } init() { - const dropdownClass = this.dropdownClass; + const { dropdownClass } = this; this.$selectElement.select2({ dropdownCssClass: dropdownClass, ...AutoWidthDropdownSelect.selectOptions(this.dropdownClass), diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js index f2939ad4dbe..0db7b95636c 100644 --- a/app/assets/javascripts/jobs/job_details_bundle.js +++ b/app/assets/javascripts/jobs/job_details_bundle.js @@ -4,7 +4,7 @@ import jobHeader from './components/header.vue'; import detailsBlock from './components/sidebar_details_block.vue'; export default () => { - const dataset = document.getElementById('js-job-details-vue').dataset; + const { dataset } = document.getElementById('js-job-details-vue'); const mediator = new JobMediator({ endpoint: dataset.endpoint }); mediator.fetchJob(); diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index dfc3f7a94c8..37a45d1d1a2 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -56,7 +56,7 @@ export default class LabelsSelect { .map(function () { return this.value; }).get(); - const handleClick = options.handleClick; + const { handleClick } = options; $sidebarLabelTooltip.tooltip(); @@ -215,7 +215,7 @@ export default class LabelsSelect { } else { if (label.color != null) { - color = label.color[0]; + [color] = label.color; } } if (color) { @@ -243,7 +243,8 @@ export default class LabelsSelect { var $dropdownParent = $dropdown.parent(); var $dropdownInputField = $dropdownParent.find('.dropdown-input-field'); var isSelected = el !== null ? el.hasClass('is-active') : false; - var title = selected.title; + + var { title } = selected; var selectedLabels = this.selected; if ($dropdownInputField.length && $dropdownInputField.val().length) { @@ -382,7 +383,7 @@ export default class LabelsSelect { })); } else { - var labels = gl.issueBoards.BoardsStore.detail.issue.labels; + var { labels } = gl.issueBoards.BoardsStore.detail.issue; labels = labels.filter(function (selectedLabel) { return selectedLabel.id !== label.id; }); diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 68f92c7f08a..6b7550efff8 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -164,7 +164,7 @@ export const scrollToElement = element => { if (!(element instanceof $)) { $el = $(element); } - const top = $el.offset().top; + const { top } = $el.offset(); return $('body, html').animate( { @@ -189,12 +189,25 @@ export const getParameterByName = (name, urlToParse) => { return decodeURIComponent(results[2].replace(/\+/g, ' ')); }; +const handleSelectedRange = (range) => { + const container = range.commonAncestorContainer; + // add context to fragment if needed + if (container.tagName === 'OL') { + const parentContainer = document.createElement(container.tagName); + parentContainer.appendChild(range.cloneContents()); + return parentContainer; + } + return range.cloneContents(); +}; + export const getSelectedFragment = () => { const selection = window.getSelection(); if (selection.rangeCount === 0) return null; const documentFragment = document.createDocumentFragment(); + for (let i = 0; i < selection.rangeCount; i += 1) { - documentFragment.appendChild(selection.getRangeAt(i).cloneContents()); + const range = selection.getRangeAt(i); + documentFragment.appendChild(handleSelectedRange(range)); } if (documentFragment.textContent.length === 0) return null; @@ -203,9 +216,7 @@ export const getSelectedFragment = () => { export const insertText = (target, text) => { // Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas - const selectionStart = target.selectionStart; - const selectionEnd = target.selectionEnd; - const value = target.value; + const { selectionStart, selectionEnd, value } = target; const textBefore = value.substring(0, selectionStart); const textAfter = value.substring(selectionEnd, value.length); @@ -245,7 +256,8 @@ export const nodeMatchesSelector = (node, selector) => { // IE11 doesn't support `node.matches(selector)` - let parentNode = node.parentNode; + let { parentNode } = node; + if (!parentNode) { parentNode = document.createElement('div'); // eslint-disable-next-line no-param-reassign @@ -281,6 +293,8 @@ export const normalizeCRLFHeaders = headers => { headersArray.forEach(header => { const keyValue = header.split(': '); + + // eslint-disable-next-line prefer-destructuring headersObject[keyValue[0]] = keyValue[1]; }); diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js index f086d962221..afbab59055b 100644 --- a/app/assets/javascripts/lib/utils/number_utils.js +++ b/app/assets/javascripts/lib/utils/number_utils.js @@ -13,7 +13,7 @@ export function formatRelevantDigits(number) { let relevantDigits = 0; let formattedNumber = ''; if (!Number.isNaN(Number(number))) { - digitsLeft = number.toString().split('.')[0]; + [digitsLeft] = number.toString().split('.'); switch (digitsLeft.length) { case 1: relevantDigits = 3; diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js index 70f185e3656..1501296ac4f 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js @@ -156,7 +156,7 @@ import Cookies from 'js-cookie'; return 0; } - const files = this.state.conflictsData.files; + const { files } = this.state.conflictsData; let count = 0; files.forEach((file) => { @@ -313,7 +313,7 @@ import Cookies from 'js-cookie'; }, isReadyToCommit() { - const files = this.state.conflictsData.files; + const { files } = this.state.conflictsData; const hasCommitMessage = $.trim(this.state.conflictsData.commitMessage).length; let unresolved = 0; diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js index 491858c3602..7badd68089c 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js @@ -12,7 +12,7 @@ import syntaxHighlight from '../syntax_highlight'; export default function initMergeConflicts() { const INTERACTIVE_RESOLVE_MODE = 'interactive'; const conflictsEl = document.querySelector('#conflicts'); - const mergeConflictsStore = gl.mergeConflicts.mergeConflictsStore; + const { mergeConflictsStore } = gl.mergeConflicts; const mergeConflictsService = new MergeConflictsService({ conflictsPath: conflictsEl.dataset.conflictsPath, resolveConflictsPath: conflictsEl.dataset.resolveConflictsPath, diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 83d326ef68f..329d4303132 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -64,7 +64,7 @@ import Notes from './notes'; /* eslint-enable max-len */ // Store the `location` object, allowing for easier stubbing in tests -let location = window.location; +let { location } = window; export default class MergeRequestTabs { constructor({ action, setUrl, stubLocation } = {}) { @@ -279,7 +279,7 @@ export default class MergeRequestTabs { mountPipelinesView() { const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); - const CommitPipelinesTable = gl.CommitPipelinesTable; + const { CommitPipelinesTable } = gl; this.commitPipelinesTable = new CommitPipelinesTable({ propsData: { endpoint: pipelineTableViewEl.dataset.endpoint, diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js index ed3a27dd68b..cee39fd0559 100644 --- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js +++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js @@ -41,10 +41,10 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom } else { const unusedColors = _.difference(defaultColorOrder, usedColors); if (unusedColors.length > 0) { - pick = unusedColors[0]; + [pick] = unusedColors; } else { usedColors = []; - pick = defaultColorOrder[0]; + [pick] = defaultColorOrder; } } usedColors.push(pick); diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js index 6a8591692f1..94da1be4066 100644 --- a/app/assets/javascripts/network/branch_graph.js +++ b/app/assets/javascripts/network/branch_graph.js @@ -101,8 +101,8 @@ export default (function() { }; BranchGraph.prototype.buildGraph = function() { - var cuday, cumonth, day, j, len, mm, r, ref; - r = this.r; + var cuday, cumonth, day, j, len, mm, ref; + const { r } = this; cuday = 0; cumonth = ""; r.rect(0, 0, 40, this.barHeight).attr({ @@ -121,7 +121,7 @@ export default (function() { font: "12px Monaco, monospace", fill: "#BBB" }); - cuday = day[0]; + [cuday] = day; } if (cumonth !== day[1]) { // Months @@ -129,6 +129,8 @@ export default (function() { font: "12px Monaco, monospace", fill: "#EEE" }); + + // eslint-disable-next-line prefer-destructuring cumonth = day[1]; } } @@ -169,8 +171,8 @@ export default (function() { }; BranchGraph.prototype.bindEvents = function() { - var element; - element = this.element; + const { element } = this; + return $(element).scroll((function(_this) { return function(event) { return _this.renderPartialGraph(); @@ -207,11 +209,13 @@ export default (function() { }; BranchGraph.prototype.appendLabel = function(x, y, commit) { - var label, r, rect, shortrefs, text, textbox, triangle; + var label, rect, shortrefs, text, textbox, triangle; + if (!commit.refs) { return; } - r = this.r; + + const { r } = this; shortrefs = commit.refs; // Truncate if longer than 15 chars if (shortrefs.length > 17) { @@ -242,11 +246,8 @@ export default (function() { }; BranchGraph.prototype.appendAnchor = function(x, y, commit) { - var anchor, options, r, top; - r = this.r; - top = this.top; - options = this.options; - anchor = r.circle(x, y, 10).attr({ + const { r, top, options } = this; + const anchor = r.circle(x, y, 10).attr({ fill: "#000", opacity: 0, cursor: "pointer" @@ -262,14 +263,15 @@ export default (function() { }; BranchGraph.prototype.drawDot = function(x, y, commit) { - var avatar_box_x, avatar_box_y, r; - r = this.r; + const { r } = this; r.circle(x, y, 3).attr({ fill: this.colors[commit.space], stroke: "none" }); - avatar_box_x = this.offsetX + this.unitSpace * this.mspace + 10; - avatar_box_y = y - 10; + + const avatar_box_x = this.offsetX + this.unitSpace * this.mspace + 10; + const avatar_box_y = y - 10; + r.rect(avatar_box_x, avatar_box_y, 20, 20).attr({ stroke: this.colors[commit.space], "stroke-width": 2 @@ -282,10 +284,10 @@ export default (function() { }; BranchGraph.prototype.drawLines = function(x, y, commit) { - var arrow, color, i, j, len, offset, parent, parentCommit, parentX1, parentX2, parentY, r, ref, results, route; - r = this.r; - ref = commit.parents; - results = []; + var arrow, color, i, len, offset, parent, parentCommit, parentX1, parentX2, parentY, route; + const { r } = this; + const ref = commit.parents; + const results = []; for (i = 0, len = ref.length; i < len; i += 1) { parent = ref[i]; @@ -331,11 +333,10 @@ export default (function() { }; BranchGraph.prototype.markCommit = function(commit) { - var r, x, y; if (commit.id === this.options.commit_id) { - r = this.r; - x = this.offsetX + this.unitSpace * (this.mspace - commit.space); - y = this.offsetY + this.unitTime * commit.time; + const { r } = this; + const x = this.offsetX + this.unitSpace * (this.mspace - commit.space); + const y = this.offsetY + this.unitTime * commit.time; r.path(["M", x + 5, y, "L", x + 15, y + 4, "L", x + 15, y - 4, "Z"]).attr({ fill: "#000", "fill-opacity": .5, diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js index 41ba5b28a1b..205d9766656 100644 --- a/app/assets/javascripts/new_branch_form.js +++ b/app/assets/javascripts/new_branch_form.js @@ -52,7 +52,7 @@ export default class NewBranchForm { validate() { var errorMessage, errors, formatter, unique, validator; - const indexOf = [].indexOf; + const { indexOf } = []; this.branchNameError.empty(); unique = function(values, value) { diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 2f752d2dcd6..48cda28a1ae 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -20,6 +20,7 @@ import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_c import axios from './lib/utils/axios_utils'; import { getLocationHash } from './lib/utils/url_utility'; import Flash from './flash'; +import { defaultAutocompleteConfig } from './gfm_auto_complete'; import CommentTypeToggle from './comment_type_toggle'; import GLForm from './gl_form'; import loadAwardsHandler from './awards_handler'; @@ -45,7 +46,7 @@ const MAX_VISIBLE_COMMIT_LIST_COUNT = 3; const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm; export default class Notes { - static initialize(notes_url, note_ids, last_fetched_at, view, enableGFM = true) { + static initialize(notes_url, note_ids, last_fetched_at, view, enableGFM) { if (!this.instance) { this.instance = new Notes(notes_url, note_ids, last_fetched_at, view, enableGFM); } @@ -55,7 +56,7 @@ export default class Notes { return this.instance; } - constructor(notes_url, note_ids, last_fetched_at, view, enableGFM = true) { + constructor(notes_url, note_ids, last_fetched_at, view, enableGFM = defaultAutocompleteConfig) { this.updateTargetButtons = this.updateTargetButtons.bind(this); this.updateComment = this.updateComment.bind(this); this.visibilityChange = this.visibilityChange.bind(this); @@ -94,7 +95,7 @@ export default class Notes { this.cleanBinding(); this.addBinding(); this.setPollingInterval(); - this.setupMainTargetNoteForm(); + this.setupMainTargetNoteForm(enableGFM); this.taskList = new TaskList({ dataType: 'note', fieldName: 'note', @@ -310,7 +311,7 @@ export default class Notes { }, }) .then(({ data }) => { - const notes = data.notes; + const { notes } = data; this.last_fetched_at = data.last_fetched_at; this.setPollingInterval(data.notes.length); $.each(notes, (i, note) => this.renderNote(note)); @@ -598,14 +599,14 @@ export default class Notes { * * Sets some hidden fields in the form. */ - setupMainTargetNoteForm() { + setupMainTargetNoteForm(enableGFM) { var form; // find the form form = $('.js-new-note-form'); // Set a global clone of the form for later cloning this.formClone = form.clone(); // show the form - this.setupNoteForm(form); + this.setupNoteForm(form, enableGFM); // fix classes form.removeClass('js-new-note-form'); form.addClass('js-main-target-form'); @@ -633,9 +634,9 @@ export default class Notes { * setup GFM auto complete * show the form */ - setupNoteForm(form) { + setupNoteForm(form, enableGFM = defaultAutocompleteConfig) { var textarea, key; - this.glForm = new GLForm(form, this.enableGFM); + this.glForm = new GLForm(form, enableGFM); textarea = form.find('.js-note-text'); key = [ 'Note', diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index a62696b39b4..a4e3faa5d75 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -194,7 +194,7 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" class="btn btn-cancel note-edit-cancel js-close-discussion-note-form" type="button" @click="cancelHandler()"> - Cancel + {{ __('Discard draft') }} </button> </div> </form> diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 17b5e8d1ae8..98f8b9af168 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -72,7 +72,7 @@ export default { }, mounted() { this.fetchNotes(); - const parentElement = this.$el.parentElement; + const { parentElement } = this.$el; if (parentElement && parentElement.classList.contains('js-vue-notes-event')) { parentElement.addEventListener('toggleAward', event => { diff --git a/app/assets/javascripts/notes/services/notes_service.js b/app/assets/javascripts/notes/services/notes_service.js index ee7628840cf..f5dce94caad 100644 --- a/app/assets/javascripts/notes/services/notes_service.js +++ b/app/assets/javascripts/notes/services/notes_service.js @@ -28,7 +28,7 @@ export default { }, poll(data = {}) { const endpoint = data.notesData.notesPath; - const lastFetchedAt = data.lastFetchedAt; + const { lastFetchedAt } = data; const options = { headers: { 'X-Last-Fetched-At': lastFetchedAt ? `${lastFetchedAt}` : undefined, diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue index cc2805a1901..d6aa4bb95d2 100644 --- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue +++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue @@ -96,7 +96,7 @@ this.enteredUsername = ''; }, onSecondaryAction() { - const form = this.$refs.form; + const { form } = this.$refs; form.action = this.blockUserUrl; this.$refs.method.value = 'put'; diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js index 6fc43af2623..ff19b9a9c30 100644 --- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js +++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js @@ -61,7 +61,7 @@ export default class Todos { e.stopPropagation(); e.preventDefault(); - const target = e.target; + const { target } = e; target.setAttribute('disabled', true); target.classList.add('disabled'); diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js index ae72c8cb4d5..6c1788dc160 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js @@ -80,10 +80,11 @@ export default (function() { }; ContributorsStatGraph.prototype.redraw_authors = function() { - var author_commits, x_domain; $("ol").html(""); - x_domain = ContributorsGraph.prototype.x_domain; - author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain); + + const { x_domain } = ContributorsGraph.prototype; + const author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain); + return _.each(author_commits, (function(_this) { return function(d) { _this.redraw_author_commit_info(d); @@ -102,7 +103,7 @@ export default (function() { }; ContributorsStatGraph.prototype.change_date_header = function() { - const x_domain = ContributorsGraph.prototype.x_domain; + const { x_domain } = ContributorsGraph.prototype; const formattedDateRange = sprintf( s__('ContributorsPage|%{startDate} – %{endDate}'), { diff --git a/app/assets/javascripts/pages/projects/init_form.js b/app/assets/javascripts/pages/projects/init_form.js index 0b6c5c1d30b..9f20a3e4e46 100644 --- a/app/assets/javascripts/pages/projects/init_form.js +++ b/app/assets/javascripts/pages/projects/init_form.js @@ -3,5 +3,5 @@ import GLForm from '~/gl_form'; export default function ($formEl) { new ZenMode(); // eslint-disable-line no-new - new GLForm($formEl, true); // eslint-disable-line no-new + new GLForm($formEl); // eslint-disable-line no-new } diff --git a/app/assets/javascripts/pages/projects/issues/form.js b/app/assets/javascripts/pages/projects/issues/form.js index 14fddbc9a05..b2b8e5d2300 100644 --- a/app/assets/javascripts/pages/projects/issues/form.js +++ b/app/assets/javascripts/pages/projects/issues/form.js @@ -10,7 +10,7 @@ import IssuableTemplateSelectors from '~/templates/issuable_template_selectors'; export default () => { new ShortcutsNavigation(); - new GLForm($('.issue-form'), true); + new GLForm($('.issue-form')); new IssuableForm($('.issue-form')); new LabelsSelect(); new MilestoneSelect(); diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js index 406fc32f9a2..3a3c21f2202 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js +++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js @@ -12,7 +12,7 @@ import IssuableTemplateSelectors from '~/templates/issuable_template_selectors'; export default () => { new Diff(); new ShortcutsNavigation(); - new GLForm($('.merge-request-form'), true); + new GLForm($('.merge-request-form')); new IssuableForm($('.merge-request-form')); new LabelsSelect(); new MilestoneSelect(); diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js index 37ef77c8e43..1faa59fb45b 100644 --- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js @@ -28,7 +28,7 @@ document.addEventListener('DOMContentLoaded', () => { const autoDevOpsExtraSettings = document.querySelector('.js-extra-settings'); autoDevOpsSettings.addEventListener('click', event => { - const target = event.target; + const { target } = event; if (target.classList.contains('js-toggle-extra-settings')) { autoDevOpsExtraSettings.classList.toggle( 'hidden', diff --git a/app/assets/javascripts/pages/projects/tags/new/index.js b/app/assets/javascripts/pages/projects/tags/new/index.js index 8d0edf7e06c..b3158f7e939 100644 --- a/app/assets/javascripts/pages/projects/tags/new/index.js +++ b/app/assets/javascripts/pages/projects/tags/new/index.js @@ -5,6 +5,6 @@ import GLForm from '../../../../gl_form'; document.addEventListener('DOMContentLoaded', () => { new ZenMode(); // eslint-disable-line no-new - new GLForm($('.tag-form'), true); // eslint-disable-line no-new + new GLForm($('.tag-form')); // eslint-disable-line no-new new RefSelectDropdown($('.js-branch-select')); // eslint-disable-line no-new }); diff --git a/app/assets/javascripts/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js index 0295653cb29..0a0fe3fc137 100644 --- a/app/assets/javascripts/pages/projects/wikis/index.js +++ b/app/assets/javascripts/pages/projects/wikis/index.js @@ -12,7 +12,7 @@ document.addEventListener('DOMContentLoaded', () => { new Wikis(); // eslint-disable-line no-new new ShortcutsWiki(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new - new GLForm($('.wiki-form'), true); // eslint-disable-line no-new + new GLForm($('.wiki-form')); // eslint-disable-line no-new const deleteWikiButton = document.getElementById('delete-wiki-button'); diff --git a/app/assets/javascripts/pages/snippets/form.js b/app/assets/javascripts/pages/snippets/form.js index 72d05da1069..758bbafead3 100644 --- a/app/assets/javascripts/pages/snippets/form.js +++ b/app/assets/javascripts/pages/snippets/form.js @@ -3,6 +3,13 @@ import GLForm from '~/gl_form'; import ZenMode from '~/zen_mode'; export default () => { - new GLForm($('.snippet-form'), false); // eslint-disable-line no-new + // eslint-disable-next-line no-new + new GLForm($('.snippet-form'), { + members: false, + issues: false, + mergeRequests: false, + milestones: false, + labels: false, + }); new ZenMode(); // eslint-disable-line no-new }; diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index b49a16a87e6..cf3ff48e608 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -10,7 +10,7 @@ import eventHub from './event_hub'; Vue.use(Translate); export default () => { - const dataset = document.querySelector('.js-pipeline-details-vue').dataset; + const { dataset } = document.querySelector('.js-pipeline-details-vue'); const mediator = new PipelinesMediator({ endpoint: dataset.endpoint }); diff --git a/app/assets/javascripts/pipelines/services/pipelines_service.js b/app/assets/javascripts/pipelines/services/pipelines_service.js index 59c8b9c58e5..8317d3f4510 100644 --- a/app/assets/javascripts/pipelines/services/pipelines_service.js +++ b/app/assets/javascripts/pipelines/services/pipelines_service.js @@ -19,7 +19,7 @@ export default class PipelinesService { getPipelines(data = {}) { const { scope, page } = data; - const CancelToken = axios.CancelToken; + const { CancelToken } = axios; this.cancelationSource = CancelToken.source(); diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js index 45670584679..0e973cab4d2 100644 --- a/app/assets/javascripts/preview_markdown.js +++ b/app/assets/javascripts/preview_markdown.js @@ -43,7 +43,7 @@ MarkdownPreview.prototype.showPreview = function ($form) { this.fetchMarkdownPreview(mdText, url, (function (response) { var body; if (response.body.length > 0) { - body = response.body; + ({ body } = response); } else { body = this.emptyMessage; } diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js index c6d809d84a6..f641b23e519 100644 --- a/app/assets/javascripts/profile/gl_crop.js +++ b/app/assets/javascripts/profile/gl_crop.js @@ -47,7 +47,8 @@ import _ from 'underscore'; var _this; _this = this; this.fileInput.on('change', function(e) { - return _this.onFileInputChange(e, this); + _this.onFileInputChange(e, this); + this.value = null; }); this.pickImageEl.on('click', this.onPickImageClick); this.modalCrop.on('shown.bs.modal', this.onModalShow); @@ -85,11 +86,10 @@ import _ from 'underscore'; cropBoxResizable: false, toggleDragModeOnDblclick: false, built: function() { - var $image, container, cropBoxHeight, cropBoxWidth; - $image = $(this); - container = $image.cropper('getContainerData'); - cropBoxWidth = _this.cropBoxWidth; - cropBoxHeight = _this.cropBoxHeight; + const $image = $(this); + const container = $image.cropper('getContainerData'); + const { cropBoxWidth, cropBoxHeight } = _this; + return $image.cropper('setCropBoxData', { width: cropBoxWidth, height: cropBoxHeight, @@ -136,7 +136,7 @@ import _ from 'underscore'; } dataURLtoBlob(dataURL) { - var array, binary, i, k, len, v; + var array, binary, i, len, v; binary = atob(dataURL.split(',')[1]); array = []; diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index bcdb3f739fe..05485e352dc 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -88,7 +88,7 @@ export default class ProjectFindFile { // render result renderList(filePaths, searchText) { - var blobItemUrl, filePath, html, i, j, len, matches, results; + var blobItemUrl, filePath, html, i, len, matches, results; this.element.find(".tree-table > tbody").empty(); results = []; diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue index c772fca14bb..a4c7c143e56 100644 --- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue +++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue @@ -47,7 +47,7 @@ }, methods: { successCallback(res) { - const pipelines = res.data.pipelines; + const { pipelines } = res.data; if (pipelines.length > 0) { // The pipeline entity always keeps the latest pipeline info on the `details.status` this.ciStatus = pipelines[0].details.status; diff --git a/app/assets/javascripts/projects_dropdown/index.js b/app/assets/javascripts/projects_dropdown/index.js index e1ca70c51a6..6056f12aa4f 100644 --- a/app/assets/javascripts/projects_dropdown/index.js +++ b/app/assets/javascripts/projects_dropdown/index.js @@ -31,7 +31,7 @@ document.addEventListener('DOMContentLoaded', () => { projectsDropdownApp, }, data() { - const dataset = this.$options.el.dataset; + const { dataset } = this.$options.el; const store = new ProjectsStore(); const service = new ProjectsService(dataset.userName); diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js index 7c61c070a35..b601b19e7be 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_create.js +++ b/app/assets/javascripts/protected_branches/protected_branch_create.js @@ -1,11 +1,8 @@ import $ from 'jquery'; -import _ from 'underscore'; import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown'; import CreateItemDropdown from '../create_item_dropdown'; import AccessorUtilities from '../lib/utils/accessor'; -const PB_LOCAL_STORAGE_KEY = 'protected-branches-defaults'; - export default class ProtectedBranchCreate { constructor() { this.$form = $('.js-new-protected-branch'); @@ -43,8 +40,6 @@ export default class ProtectedBranchCreate { onSelect: this.onSelectCallback, getData: ProtectedBranchCreate.getProtectedBranches, }); - - this.loadPreviousSelection($allowedToMergeDropdown.data('glDropdown'), $allowedToPushDropdown.data('glDropdown')); } // This will run after clicked callback @@ -59,39 +54,10 @@ export default class ProtectedBranchCreate { $allowedToPushInput.length ); - this.savePreviousSelection($allowedToMergeInput.val(), $allowedToPushInput.val()); this.$form.find('input[type="submit"]').prop('disabled', completedForm); } static getProtectedBranches(term, callback) { callback(gon.open_branches); } - - loadPreviousSelection(mergeDropdown, pushDropdown) { - let mergeIndex = 0; - let pushIndex = 0; - if (this.isLocalStorageAvailable) { - const savedDefaults = JSON.parse(window.localStorage.getItem(PB_LOCAL_STORAGE_KEY)); - if (savedDefaults != null) { - mergeIndex = _.findLastIndex(mergeDropdown.fullData.roles, { - id: parseInt(savedDefaults.mergeSelection, 0), - }); - pushIndex = _.findLastIndex(pushDropdown.fullData.roles, { - id: parseInt(savedDefaults.pushSelection, 0), - }); - } - } - mergeDropdown.selectRowAtIndex(mergeIndex); - pushDropdown.selectRowAtIndex(pushIndex); - } - - savePreviousSelection(mergeSelection, pushSelection) { - if (this.isLocalStorageAvailable) { - const branchDefaults = { - mergeSelection, - pushSelection, - }; - window.localStorage.setItem(PB_LOCAL_STORAGE_KEY, JSON.stringify(branchDefaults)); - } - } } diff --git a/app/assets/javascripts/registry/index.js b/app/assets/javascripts/registry/index.js index 6fb125192b2..e15cd94a915 100644 --- a/app/assets/javascripts/registry/index.js +++ b/app/assets/javascripts/registry/index.js @@ -10,7 +10,7 @@ export default () => new Vue({ registryApp, }, data() { - const dataset = document.querySelector(this.$options.el).dataset; + const { dataset } = document.querySelector(this.$options.el); return { endpoint: dataset.endpoint, }; diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js index c0de03373d8..a78aa90b7b5 100644 --- a/app/assets/javascripts/registry/stores/actions.js +++ b/app/assets/javascripts/registry/stores/actions.js @@ -20,7 +20,7 @@ export const fetchList = ({ commit }, { repo, page }) => { commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo); return Vue.http.get(repo.tagsPath, { params: { page } }).then(response => { - const headers = response.headers; + const { headers } = response; return response.json().then(resp => { commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo); diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index 2f4e4881f24..5b2e0468784 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -289,7 +289,7 @@ export default class SearchAutocomplete { } // If the dropdown is closed, we'll open it - if (!this.dropdown.hasClass('open')) { + if (!this.dropdown.hasClass('show')) { this.loadingSuggestions = false; this.dropdownToggle.dropdown('toggle'); return this.searchInput.removeClass('disabled'); @@ -424,9 +424,9 @@ export default class SearchAutocomplete { } disableAutocomplete() { - if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) { + if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('show')) { this.searchInput.addClass('disabled'); - this.dropdown.removeClass('open').trigger('hidden.bs.dropdown'); + this.dropdown.removeClass('show').trigger('hidden.bs.dropdown'); this.restoreMenu(); } } diff --git a/app/assets/javascripts/shared/milestones/form.js b/app/assets/javascripts/shared/milestones/form.js index 2f974d6ff9d..060f374310c 100644 --- a/app/assets/javascripts/shared/milestones/form.js +++ b/app/assets/javascripts/shared/milestones/form.js @@ -6,5 +6,13 @@ import GLForm from '../../gl_form'; export default (initGFM = true) => { new ZenMode(); // eslint-disable-line no-new new DueDateSelectors(); // eslint-disable-line no-new - new GLForm($('.milestone-form'), initGFM); // eslint-disable-line no-new + // eslint-disable-next-line no-new + new GLForm($('.milestone-form'), { + emojis: initGFM, + members: initGFM, + issues: initGFM, + mergeRequests: initGFM, + milestones: initGFM, + labels: initGFM, + }); }; diff --git a/app/assets/javascripts/smart_interval.js b/app/assets/javascripts/smart_interval.js index 77ab7c964e6..5e385400747 100644 --- a/app/assets/javascripts/smart_interval.js +++ b/app/assets/javascripts/smart_interval.js @@ -42,8 +42,7 @@ export default class SmartInterval { /* public */ start() { - const cfg = this.cfg; - const state = this.state; + const { cfg, state } = this; if (cfg.immediateExecution && !this.isLoading) { cfg.immediateExecution = false; @@ -100,7 +99,7 @@ export default class SmartInterval { /* private */ initInterval() { - const cfg = this.cfg; + const { cfg } = this; if (!cfg.lazyStart) { this.start(); @@ -151,7 +150,7 @@ export default class SmartInterval { } incrementInterval() { - const cfg = this.cfg; + const { cfg } = this; const currentInterval = this.getCurrentInterval(); if (cfg.hiddenInterval && !this.isPageVisible()) return; let nextInterval = currentInterval * cfg.incrementByFactorOf; @@ -166,7 +165,7 @@ export default class SmartInterval { isPageVisible() { return this.state.pageVisibility === 'visible'; } stopTimer() { - const state = this.state; + const { state } = this; state.intervalId = window.clearInterval(state.intervalId); } diff --git a/app/assets/javascripts/test_utils/simulate_drag.js b/app/assets/javascripts/test_utils/simulate_drag.js index e39213cb098..a5c18042ce7 100644 --- a/app/assets/javascripts/test_utils/simulate_drag.js +++ b/app/assets/javascripts/test_utils/simulate_drag.js @@ -38,14 +38,14 @@ function simulateEvent(el, type, options = {}) { function isLast(target) { const el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el; - const children = el.children; + const { children } = el; return children.length - 1 === target.index; } function getTarget(target) { const el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el; - const children = el.children; + const { children } = el; return ( children[target.index] || diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js index 96af6d2fcca..78fd7ad441f 100644 --- a/app/assets/javascripts/u2f/authenticate.js +++ b/app/assets/javascripts/u2f/authenticate.js @@ -11,7 +11,6 @@ export default class U2FAuthenticate { constructor(container, form, u2fParams, fallbackButton, fallbackUI) { this.u2fUtils = null; this.container = container; - this.renderNotSupported = this.renderNotSupported.bind(this); this.renderAuthenticated = this.renderAuthenticated.bind(this); this.renderError = this.renderError.bind(this); this.renderInProgress = this.renderInProgress.bind(this); @@ -41,7 +40,6 @@ export default class U2FAuthenticate { this.signRequests = u2fParams.sign_requests.map(request => _(request).omit('challenge')); this.templates = { - notSupported: '#js-authenticate-u2f-not-supported', setup: '#js-authenticate-u2f-setup', inProgress: '#js-authenticate-u2f-in-progress', error: '#js-authenticate-u2f-error', @@ -55,7 +53,7 @@ export default class U2FAuthenticate { this.u2fUtils = utils; this.renderInProgress(); }) - .catch(() => this.renderNotSupported()); + .catch(() => this.switchToFallbackUI()); } authenticate() { @@ -96,10 +94,6 @@ export default class U2FAuthenticate { this.fallbackButton.classList.add('hidden'); } - renderNotSupported() { - return this.renderTemplate('notSupported'); - } - switchToFallbackUI() { this.fallbackButton.classList.add('hidden'); this.container[0].classList.add('hidden'); diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 7abe7a6be5f..e3d7645040d 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -250,7 +250,6 @@ function UsersSelect(currentUser, els, options = {}) { let anyUser; let index; - let j; let len; let name; let obj; @@ -501,7 +500,7 @@ function UsersSelect(currentUser, els, options = {}) { if (this.multiSelect) { selected = getSelected().find(u => user.id === u); - const fieldName = this.fieldName; + const { fieldName } = this; const field = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "'][value='" + user.id + "']"); if (field.length) { @@ -553,7 +552,7 @@ function UsersSelect(currentUser, els, options = {}) { minimumInputLength: 0, query: function(query) { return _this.users(query.term, options, function(users) { - var anyUser, data, emailUser, index, j, len, name, nullUser, obj, ref; + var anyUser, data, emailUser, index, len, name, nullUser, obj, ref; data = { results: users }; diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index e455c4d2cb5..09477da40b5 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -191,7 +191,7 @@ export default { if (data.ci_status === this.mr.ciStatus) return; if (!data.pipeline) return; - const label = data.pipeline.details.status.label; + const { label } = data.pipeline.details.status; const title = `Pipeline ${label}`; const message = `Pipeline ${label} for "${data.title}"`; @@ -211,7 +211,7 @@ export default { // `params` should be an Array contains a Boolean, like `[true]` // Passing parameter as Boolean didn't work. eventHub.$on('SetBranchRemoveFlag', (params) => { - this.mr.isRemovingSourceBranch = params[0]; + [this.mr.isRemovingSourceBranch] = params; }); eventHub.$on('FailedToMerge', (mergeError) => { diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue index 6851029018a..133bdbb54f7 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue @@ -42,7 +42,7 @@ export default { }, methods: { onImgLoad() { - const contentImg = this.$refs.contentImg; + const { contentImg } = this.$refs; if (contentImg) { this.isZoomable = diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue index 09e0094054d..a10deb93f0f 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue @@ -4,7 +4,7 @@ import { __ } from '~/locale'; import $ from 'jquery'; import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; -const CancelToken = axios.CancelToken; +const { CancelToken } = axios; let axiosSource; export default { diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue index 2c47f5b9b35..d3cbe3c7e74 100644 --- a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue @@ -45,11 +45,15 @@ export default { return DownloadDiffViewer; } }, + basePath() { + // We might get the project path from rails with the relative url already setup + return this.projectPath.indexOf('/') === 0 ? '' : `${gon.relative_url_root}/`; + }, fullOldPath() { - return `${gon.relative_url_root}/${this.projectPath}/raw/${this.oldSha}/${this.oldPath}`; + return `${this.basePath}${this.projectPath}/raw/${this.oldSha}/${this.oldPath}`; }, fullNewPath() { - return `${gon.relative_url_root}/${this.projectPath}/raw/${this.newSha}/${this.newPath}`; + return `${this.basePath}${this.projectPath}/raw/${this.newSha}/${this.newPath}`; }, }, }; diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 7d26390d9bc..fba67681777 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -62,7 +62,14 @@ /* GLForm class handles all the toolbar buttons */ - return new GLForm($(this.$refs['gl-form']), this.enableAutocomplete); + return new GLForm($(this.$refs['gl-form']), { + emojis: this.enableAutocomplete, + members: this.enableAutocomplete, + issues: this.enableAutocomplete, + mergeRequests: this.enableAutocomplete, + milestones: this.enableAutocomplete, + labels: this.enableAutocomplete, + }); }, beforeDestroy() { const glForm = $(this.$refs['gl-form']).data('glForm'); diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.vue b/app/assets/javascripts/vue_shared/components/table_pagination.vue index 2370e59d017..8e9621c956f 100644 --- a/app/assets/javascripts/vue_shared/components/table_pagination.vue +++ b/app/assets/javascripts/vue_shared/components/table_pagination.vue @@ -55,7 +55,7 @@ }, getItems() { const total = this.pageInfo.totalPages; - const page = this.pageInfo.page; + const { page } = this.pageInfo; const items = []; if (page > 1) { |