diff options
Diffstat (limited to 'app')
415 files changed, 6315 insertions, 4725 deletions
diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js index de4566bb119..05de970e387 100644 --- a/app/assets/javascripts/activities.js +++ b/app/assets/javascripts/activities.js @@ -6,10 +6,12 @@ import Pager from './pager'; import { localTimeAgo } from './lib/utils/datetime_utility'; export default class Activities { - constructor() { - Pager.init(20, true, false, data => data, this.updateTooltips); + constructor(container = '') { + this.container = container; - $('.event-filter-link').on('click', (e) => { + Pager.init(20, true, false, data => data, this.updateTooltips, this.container); + + $('.event-filter-link').on('click', e => { e.preventDefault(); this.toggleFilter(e.currentTarget); this.reloadActivities(); @@ -22,7 +24,7 @@ export default class Activities { reloadActivities() { $('.content_list').html(''); - Pager.init(20, true, false, data => data, this.updateTooltips); + Pager.init(20, true, false, data => data, this.updateTooltips, this.container); } toggleFilter(sender) { diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js index 3cc89ff1955..ec27ae8c291 100644 --- a/app/assets/javascripts/blob_edit/blob_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_bundle.js @@ -13,7 +13,7 @@ export default () => { if (editBlobForm.length) { const urlRoot = editBlobForm.data('relativeUrlRoot'); const assetsPath = editBlobForm.data('assetsPrefix'); - const filePath = editBlobForm.data('blobFilename') + const filePath = editBlobForm.data('blobFilename'); const currentAction = $('.js-file-title').data('currentAction'); const projectId = editBlobForm.data('project-id'); diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index 75477ebb3b3..fb6e5291a61 100644 --- a/app/assets/javascripts/boards/components/board.js +++ b/app/assets/javascripts/boards/components/board.js @@ -42,7 +42,7 @@ export default Vue.extend({ required: true, }, }, - data () { + data() { return { detailIssue: boardsStore.detail, filter: boardsStore.filter, @@ -53,26 +53,28 @@ export default Vue.extend({ const { issuesSize } = this.list; return `${n__('%d issue', '%d issues', issuesSize)}`; }, + isNewIssueShown() { + return this.list.type === 'backlog' || (!this.disabled && this.list.type !== 'closed'); + }, }, watch: { filter: { handler() { this.list.page = 1; - this.list.getIssues(true) - .catch(() => { - // TODO: handle request error - }); + this.list.getIssues(true).catch(() => { + // TODO: handle request error + }); }, deep: true, - } + }, }, - mounted () { + mounted() { this.sortableOptions = getBoardSortableDefaultOptions({ disabled: this.disabled, group: 'boards', draggable: '.is-draggable', handle: '.js-board-handle', - onEnd: (e) => { + onEnd: e => { sortableEnd(); if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) { @@ -83,14 +85,15 @@ export default Vue.extend({ boardsStore.moveList(list, order); }); } - } + }, }); this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions); }, created() { if (this.list.isExpandable && AccessorUtilities.isLocalStorageAccessSafe()) { - const isCollapsed = localStorage.getItem(`boards.${this.boardId}.${this.list.type}.expanded`) === 'false'; + const isCollapsed = + localStorage.getItem(`boards.${this.boardId}.${this.list.type}.expanded`) === 'false'; this.list.isExpanded = !isCollapsed; } @@ -104,7 +107,10 @@ export default Vue.extend({ this.list.isExpanded = !this.list.isExpanded; if (AccessorUtilities.isLocalStorageAccessSafe()) { - localStorage.setItem(`boards.${this.boardId}.${this.list.type}.expanded`, this.list.isExpanded); + localStorage.setItem( + `boards.${this.boardId}.${this.list.type}.expanded`, + this.list.isExpanded, + ); } } }, diff --git a/app/assets/javascripts/boards/components/board_blank_state.vue b/app/assets/javascripts/boards/components/board_blank_state.vue index 38aaec73d7d..561a4636ef5 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.vue +++ b/app/assets/javascripts/boards/components/board_blank_state.vue @@ -32,18 +32,18 @@ export default { boardsStore.state.lists = _.sortBy(boardsStore.state.lists, 'position'); // Save the labels - gl.boardService.generateDefaultLists() + gl.boardService + .generateDefaultLists() .then(res => res.data) - .then((data) => { - data.forEach((listObj) => { + .then(data => { + data.forEach(listObj => { const list = boardsStore.findList('title', listObj.title); list.id = listObj.id; list.label.id = listObj.label.id; - list.getIssues() - .catch(() => { - // TODO: handle request error - }); + list.getIssues().catch(() => { + // TODO: handle request error + }); }); }) .catch(() => { @@ -57,7 +57,6 @@ export default { clearBlankState: boardsStore.removeBlankState.bind(boardsStore), }, }; - </script> <template> diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index 843498f0d06..2f31316aa76 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -1,77 +1,77 @@ <script> - /* eslint-disable vue/require-default-prop */ - import IssueCardInner from './issue_card_inner.vue'; - import eventHub from '../eventhub'; - import boardsStore from '../stores/boards_store'; +/* eslint-disable vue/require-default-prop */ +import IssueCardInner from './issue_card_inner.vue'; +import eventHub from '../eventhub'; +import boardsStore from '../stores/boards_store'; - export default { - name: 'BoardsIssueCard', - components: { - IssueCardInner, +export default { + name: 'BoardsIssueCard', + components: { + IssueCardInner, + }, + props: { + list: { + 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, - }, + issue: { + type: Object, + default: () => ({}), }, - data() { - return { - showDetail: false, - detailIssue: boardsStore.detail, - }; + issueLinkBase: { + type: String, + default: '', }, - computed: { - issueDetailVisible() { - return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id; - }, + disabled: { + type: Boolean, + default: false, }, - methods: { - mouseDown() { - this.showDetail = true; - }, - mouseMove() { - this.showDetail = false; - }, - showIssue(e) { - if (e.target.classList.contains('js-no-trigger')) return; + index: { + type: Number, + default: 0, + }, + rootPath: { + type: String, + default: '', + }, + groupId: { + type: Number, + }, + }, + data() { + return { + showDetail: false, + detailIssue: boardsStore.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) { - this.showDetail = false; + if (this.showDetail) { + this.showDetail = false; - if (boardsStore.detail.issue && boardsStore.detail.issue.id === this.issue.id) { - eventHub.$emit('clearDetailIssue'); - } else { - eventHub.$emit('newDetailIssue', this.issue); - boardsStore.detail.list = this.list; - } + if (boardsStore.detail.issue && boardsStore.detail.issue.id === this.issue.id) { + eventHub.$emit('clearDetailIssue'); + } else { + eventHub.$emit('newDetailIssue', this.issue); + boardsStore.detail.list = this.list; } - }, + } }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue index 030288a1c9d..ee3dc38bca6 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.vue +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -1,6 +1,6 @@ <script> import $ from 'jquery'; -import { Button } from '@gitlab-org/gitlab-ui'; +import { GlButton } from '@gitlab-org/gitlab-ui'; import eventHub from '../eventhub'; import ProjectSelect from './project_select.vue'; import ListIssue from '../models/issue'; @@ -10,7 +10,7 @@ export default { name: 'BoardNewIssue', components: { ProjectSelect, - 'gl-button': Button, + GlButton, }, props: { groupId: { @@ -62,7 +62,8 @@ export default { eventHub.$emit(`scroll-board-list-${this.list.id}`); this.cancel(); - return this.list.newIssue(issue) + return this.list + .newIssue(issue) .then(() => { // Need this because our jQuery very kindly disables buttons on ALL form submissions $(this.$refs.submitButton).enable(); diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index 62666954de0..e637e1f1223 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -38,7 +38,7 @@ export default Vue.extend({ }; }, computed: { - showSidebar () { + showSidebar() { return Object.keys(this.issue).length; }, milestoneTitle() { @@ -51,18 +51,20 @@ export default Vue.extend({ return this.issue.labels && this.issue.labels.length; }, labelDropdownTitle() { - return this.hasLabels ? sprintf(__('%{firstLabel} +%{labelCount} more'), { - firstLabel: this.issue.labels[0].title, - labelCount: this.issue.labels.length - 1 - }) : __('Label'); + return this.hasLabels + ? sprintf(__('%{firstLabel} +%{labelCount} more'), { + firstLabel: this.issue.labels[0].title, + labelCount: this.issue.labels.length - 1, + }) + : __('Label'); }, selectedLabels() { return this.hasLabels ? this.issue.labels.map(l => l.title).join(',') : ''; - } + }, }, watch: { detail: { - handler () { + handler() { if (this.issue.id !== this.detail.issue.id) { $('.block.assignee') .find('input:not(.js-vue)[name="issue[assignee_ids][]"]') @@ -71,17 +73,19 @@ export default Vue.extend({ }); $('.js-issue-board-sidebar', this.$el).each((i, el) => { - $(el).data('glDropdown').clearMenu(); + $(el) + .data('glDropdown') + .clearMenu(); }); } this.issue = this.detail.issue; this.list = this.detail.list; }, - deep: true + deep: true, }, }, - created () { + created() { // Get events from glDropdown eventHub.$on('sidebar.removeAssignee', this.removeAssignee); eventHub.$on('sidebar.addAssignee', this.addAssignee); @@ -94,7 +98,7 @@ export default Vue.extend({ eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees); eventHub.$off('sidebar.saveAssignees', this.saveAssignees); }, - mounted () { + mounted() { new IssuableContext(this.currentUser); new MilestoneSelect(); new DueDateSelectors(); @@ -102,29 +106,30 @@ export default Vue.extend({ new Sidebar(); }, methods: { - closeSidebar () { + closeSidebar() { this.detail.issue = {}; }, - assignSelf () { + assignSelf() { // Notify gl dropdown that we are now assigning to current user this.$refs.assigneeBlock.dispatchEvent(new Event('assignYourself')); this.addAssignee(this.currentUser); this.saveAssignees(); }, - removeAssignee (a) { + removeAssignee(a) { boardsStore.detail.issue.removeAssignee(a); }, - addAssignee (a) { + addAssignee(a) { boardsStore.detail.issue.addAssignee(a); }, - removeAllAssignees () { + removeAllAssignees() { boardsStore.detail.issue.removeAllAssignees(); }, - saveAssignees () { + saveAssignees() { this.loadingAssignees = true; - boardsStore.detail.issue.update() + boardsStore.detail.issue + .update() .then(() => { this.loadingAssignees = false; }) diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue index aa98f35786e..d956777a86b 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.vue +++ b/app/assets/javascripts/boards/components/issue_card_inner.vue @@ -1,142 +1,142 @@ <script> - import $ from 'jquery'; - import Icon from '~/vue_shared/components/icon.vue'; - import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; - import eventHub from '../eventhub'; - import tooltip from '../../vue_shared/directives/tooltip'; - import boardsStore from '../stores/boards_store'; +import $ from 'jquery'; +import Icon from '~/vue_shared/components/icon.vue'; +import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; +import eventHub from '../eventhub'; +import tooltip from '../../vue_shared/directives/tooltip'; +import boardsStore from '../stores/boards_store'; - export default { - components: { - UserAvatarLink, - Icon, - }, - 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, - }; +export default { + components: { + UserAvatarLink, + Icon, + }, + directives: { + tooltip, + }, + props: { + issue: { + type: Object, + required: true, }, - 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; - } + 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.issue.assignees.length > this.numberOverLimit; - }, - issueId() { - if (this.issue.iid) { - return `#${this.issue.iid}`; - } + return `+${this.numberOverLimit}`; + }, + shouldRenderCounter() { + if (this.issue.assignees.length <= this.maxRender) { 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; + 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 = boardsStore.filter.path.split('&'); - const labelTitle = encodeURIComponent(label.title); - const param = `label_name[]=${labelTitle}`; - const labelIndex = filterPath.indexOf(param); - $(e.currentTarget).tooltip('hide'); + const filterPath = boardsStore.filter.path.split('&'); + const labelTitle = encodeURIComponent(label.title); + const param = `label_name[]=${labelTitle}`; + const labelIndex = filterPath.indexOf(param); + $(e.currentTarget).tooltip('hide'); - if (labelIndex === -1) { - filterPath.push(param); - } else { - filterPath.splice(labelIndex, 1); - } + if (labelIndex === -1) { + filterPath.push(param); + } else { + filterPath.splice(labelIndex, 1); + } - boardsStore.filter.path = filterPath.join('&'); + boardsStore.filter.path = filterPath.join('&'); - boardsStore.updateFiltersUrl(); + boardsStore.updateFiltersUrl(); - eventHub.$emit('updateTokens'); - }, - labelStyle(label) { - return { - backgroundColor: label.color, - color: label.textColor, - }; - }, - }, - }; + eventHub.$emit('updateTokens'); + }, + labelStyle(label) { + return { + backgroundColor: label.color, + color: label.textColor, + }; + }, + }, +}; </script> <template> <div> diff --git a/app/assets/javascripts/boards/components/modal/empty_state.vue b/app/assets/javascripts/boards/components/modal/empty_state.vue index dbd69f84526..795ba864545 100644 --- a/app/assets/javascripts/boards/components/modal/empty_state.vue +++ b/app/assets/javascripts/boards/components/modal/empty_state.vue @@ -20,7 +20,7 @@ export default { computed: { contents() { const obj = { - title: 'You haven\'t added any issues to your project yet', + title: "You haven't added any issues to your project yet", content: ` An issue can be a bug, a todo or a feature request that needs to be discussed in a project. Besides, issues are searchable and filterable. @@ -28,7 +28,7 @@ export default { }; if (this.activeTab === 'selected') { - obj.title = 'You haven\'t selected any issues yet'; + obj.title = "You haven't selected any issues yet"; obj.content = ` Go back to <strong>Open issues</strong> and select some issues to add to your board. diff --git a/app/assets/javascripts/boards/components/modal/footer.vue b/app/assets/javascripts/boards/components/modal/footer.vue index 268ca6bca13..d51597ed22d 100644 --- a/app/assets/javascripts/boards/components/modal/footer.vue +++ b/app/assets/javascripts/boards/components/modal/footer.vue @@ -42,19 +42,17 @@ export default { const req = this.buildUpdateRequest(list); // Post the data to the backend - gl.boardService - .bulkUpdate(issueIds, req) - .catch(() => { - Flash(__('Failed to update issues, please try again.')); + gl.boardService.bulkUpdate(issueIds, req).catch(() => { + Flash(__('Failed to update issues, please try again.')); - selectedIssues.forEach((issue) => { - list.removeIssue(issue); - list.issuesSize -= 1; - }); + selectedIssues.forEach(issue => { + list.removeIssue(issue); + list.issuesSize -= 1; }); + }); // Add the issues on the frontend - selectedIssues.forEach((issue) => { + selectedIssues.forEach(issue => { list.addIssue(issue); list.issuesSize += 1; }); diff --git a/app/assets/javascripts/boards/components/modal/header.vue b/app/assets/javascripts/boards/components/modal/header.vue index 979fb4d7199..fc6cefa89a9 100644 --- a/app/assets/javascripts/boards/components/modal/header.vue +++ b/app/assets/javascripts/boards/components/modal/header.vue @@ -1,52 +1,52 @@ <script> - import ModalFilters from './filters'; - import ModalTabs from './tabs.vue'; - import ModalStore from '../../stores/modal_store'; - import modalMixin from '../../mixins/modal_mixins'; +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, +export default { + components: { + ModalTabs, + ModalFilters, + }, + mixins: [modalMixin], + props: { + projectId: { + type: Number, + required: true, }, - mixins: [modalMixin], - props: { - projectId: { - type: Number, - required: true, - }, - milestonePath: { - type: String, - required: true, - }, - labelPath: { - type: String, - required: true, - }, + milestonePath: { + type: String, + required: true, }, - data() { - return ModalStore.store; + labelPath: { + type: String, + required: true, }, - computed: { - selectAllText() { - if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) { - return 'Select all'; - } + }, + 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; - }, + return 'Deselect all'; }, - methods: { - toggleAll() { - this.$refs.selectAllBtn.blur(); + showSearch() { + return this.activeTab === 'all' && !this.loading && this.issuesCount > 0; + }, + }, + methods: { + toggleAll() { + this.$refs.selectAllBtn.blur(); - ModalStore.toggleAll(); - }, + ModalStore.toggleAll(); }, - }; + }, +}; </script> <template> <div> diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue index 0c4c709324d..40949cc0656 100644 --- a/app/assets/javascripts/boards/components/modal/index.vue +++ b/app/assets/javascripts/boards/components/modal/index.vue @@ -1,143 +1,144 @@ <script> - /* global ListIssue */ - import { urlParamsToObject } from '~/lib/utils/common_utils'; - 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'; +/* global ListIssue */ +import { urlParamsToObject } from '~/lib/utils/common_utils'; +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, +export default { + components: { + EmptyState, + ModalHeader, + ModalList, + ModalFooter, + }, + props: { + newIssuePath: { + type: String, + required: true, }, - 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, - }, + emptyStateSvg: { + type: String, + required: true, }, - data() { - return ModalStore.store; + issueLinkBase: { + type: String, + required: true, }, - computed: { - showList() { - if (this.activeTab === 'selected') { - return this.selectedIssues.length > 0; - } + 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.issuesCount > 0; + }, + showEmptyState() { + if (!this.loading && this.issuesCount === 0) { + return true; + } - return this.activeTab === 'selected' && this.selectedIssues.length === 0; - }, + return this.activeTab === 'selected' && this.selectedIssues.length === 0; }, - watch: { - page() { - this.loadIssues(); - }, - showAddIssuesModal() { - if (this.showAddIssuesModal && !this.issues.length) { - this.loading = true; + }, + 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.loading = false; + this.filterLoading = false; }; - this.loadIssues() + this.loadIssues(true) .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, - }, + deep: true, }, - created() { - this.page = 1; - }, - methods: { - loadIssues(clearIssues = false) { - if (!this.showAddIssuesModal) return false; + }, + created() { + this.page = 1; + }, + methods: { + loadIssues(clearIssues = false) { + if (!this.showAddIssuesModal) return false; - return gl.boardService.getBacklog({ + return gl.boardService + .getBacklog({ ...urlParamsToObject(this.filter.path), page: this.page, per: this.perPage, }) - .then(res => res.data) - .then(data => { - if (clearIssues) { - this.issues = []; - } + .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; + data.issues.forEach(issueObj => { + const issue = new ListIssue(issueObj); + const foundSelectedIssue = ModalStore.findSelectedIssue(issue); + issue.selected = !!foundSelectedIssue; - this.issues.push(issue); - }); + this.issues.push(issue); + }); - this.loadingNewPage = false; + this.loadingNewPage = false; - if (!this.issuesCount) { - this.issuesCount = data.size; - } - }) - .catch(() => { - // TODO: handle request error - }); - }, + if (!this.issuesCount) { + this.issuesCount = data.size; + } + }) + .catch(() => { + // TODO: handle request error + }); }, - }; + }, +}; </script> <template> <div diff --git a/app/assets/javascripts/boards/components/modal/list.vue b/app/assets/javascripts/boards/components/modal/list.vue index c93fd9f415c..e11f398e70d 100644 --- a/app/assets/javascripts/boards/components/modal/list.vue +++ b/app/assets/javascripts/boards/components/modal/list.vue @@ -1,120 +1,120 @@ <script> - import Icon from '~/vue_shared/components/icon.vue'; - import bp from '../../../breakpoints'; - import ModalStore from '../../stores/modal_store'; - import IssueCardInner from '../issue_card_inner.vue'; +import Icon from '~/vue_shared/components/icon.vue'; +import bp from '../../../breakpoints'; +import ModalStore from '../../stores/modal_store'; +import IssueCardInner from '../issue_card_inner.vue'; - export default { - components: { - IssueCardInner, - Icon, +export default { + components: { + IssueCardInner, + Icon, + }, + props: { + issueLinkBase: { + type: String, + required: true, }, - props: { - issueLinkBase: { - type: String, - required: true, - }, - rootPath: { - type: String, - required: true, - }, - emptyStateSvg: { - type: String, - required: true, - }, + rootPath: { + type: String, + required: true, }, - data() { - return ModalStore.store; + emptyStateSvg: { + type: String, + required: true, }, - computed: { - loopIssues() { - if (this.activeTab === 'all') { - return this.issues; - } + }, + 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; + return this.selectedIssues; + }, + groupedIssues() { + const groups = []; + this.loopIssues.forEach((issue, i) => { + const index = i % this.columns; - if (!groups[index]) { - groups.push([]); - } + if (!groups[index]) { + groups.push([]); + } - groups[index].push(issue); - }); + groups[index].push(issue); + }); - return groups; - }, + return groups; }, - watch: { - activeTab() { - if (this.activeTab === 'all') { - ModalStore.purgeUnselectedIssues(); - } - }, + }, + watch: { + activeTab() { + if (this.activeTab === 'all') { + ModalStore.purgeUnselectedIssues(); + } }, - mounted() { - this.scrollHandlerWrapper = this.scrollHandler.bind(this); - this.setColumnCountWrapper = this.setColumnCount.bind(this); - this.setColumnCount(); + }, + 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); + 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; + } }, - beforeDestroy() { - this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper); - window.removeEventListener('resize', this.setColumnCountWrapper); + toggleIssue(e, issue) { + if (e.target.tagName !== 'A') { + ModalStore.toggleIssue(issue); + } }, - 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; + 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); + const index = ModalStore.selectedIssueIndex(issue); - return index !== -1; - }, - setColumnCount() { - const breakpoint = bp.getBreakpointSize(); + 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; - } - }, + if (breakpoint === 'lg' || breakpoint === 'md') { + this.columns = 3; + } else if (breakpoint === 'sm') { + this.columns = 2; + } else { + this.columns = 1; + } }, - }; + }, +}; </script> <template> <section diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue index 3baac08d411..20665f903d5 100644 --- a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue @@ -1,12 +1,12 @@ <script> -import { Link } from '@gitlab-org/gitlab-ui'; +import { GlLink } from '@gitlab-org/gitlab-ui'; import Icon from '~/vue_shared/components/icon.vue'; import ModalStore from '../../stores/modal_store'; import boardsStore from '../../stores/boards_store'; export default { components: { - 'gl-link': Link, + GlLink, Icon, }, data() { diff --git a/app/assets/javascripts/boards/components/modal/tabs.vue b/app/assets/javascripts/boards/components/modal/tabs.vue index d926b080094..5d661590e8e 100644 --- a/app/assets/javascripts/boards/components/modal/tabs.vue +++ b/app/assets/javascripts/boards/components/modal/tabs.vue @@ -1,21 +1,21 @@ <script> - import ModalStore from '../../stores/modal_store'; - import modalMixin from '../../mixins/modal_mixins'; +import ModalStore from '../../stores/modal_store'; +import modalMixin from '../../mixins/modal_mixins'; - export default { - mixins: [modalMixin], - data() { - return ModalStore.store; +export default { + mixins: [modalMixin], + data() { + return ModalStore.store; + }, + computed: { + selectedCount() { + return ModalStore.selectedCount(); }, - computed: { - selectedCount() { - return ModalStore.selectedCount(); - }, - }, - destroyed() { - this.activeTab = 'all'; - }, - }; + }, + destroyed() { + this.activeTab = 'all'; + }, +}; </script> <template> <div class="top-area prepend-top-10 append-bottom-10"> diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js index 2c2045f8901..f7016561f93 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js @@ -6,36 +6,41 @@ import _ from 'underscore'; import CreateLabelDropdown from '../../create_label'; import boardsStore from '../stores/boards_store'; -$(document).off('created.label').on('created.label', (e, label) => { - boardsStore.new({ - title: label.title, - position: boardsStore.state.lists.length - 2, - list_type: 'label', - label: { - id: label.id, +$(document) + .off('created.label') + .on('created.label', (e, label) => { + boardsStore.new({ title: label.title, - color: label.color, - }, + position: boardsStore.state.lists.length - 2, + list_type: 'label', + label: { + id: label.id, + title: label.title, + color: label.color, + }, + }); }); -}); export default function initNewListDropdown() { - $('.js-new-board-list').each(function () { + $('.js-new-board-list').each(function() { const $this = $(this); - new CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespacePath'), $this.data('projectPath')); + new CreateLabelDropdown( + $this.closest('.dropdown').find('.dropdown-new-label'), + $this.data('namespacePath'), + $this.data('projectPath'), + ); $this.glDropdown({ data(term, callback) { - axios.get($this.attr('data-list-labels-path')) - .then(({ data }) => { - callback(data); - }); + axios.get($this.attr('data-list-labels-path')).then(({ data }) => { + callback(data); + }); }, - renderRow (label) { + renderRow(label) { const active = boardsStore.findList('title', label.title); const $li = $('<li />'); const $a = $('<a />', { - class: (active ? `is-active js-board-list-${active.id}` : ''), + class: active ? `is-active js-board-list-${active.id}` : '', text: label.title, href: '#', }); @@ -53,7 +58,7 @@ export default function initNewListDropdown() { selectable: true, multiSelect: true, containerSelector: '.js-tab-container-labels .dropdown-page-one .dropdown-content', - clicked (options) { + clicked(options) { const { e } = options; const label = options.selectedObj; e.preventDefault(); diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue index 4e8fe16160a..0f01a2a6c09 100644 --- a/app/assets/javascripts/boards/components/project_select.vue +++ b/app/assets/javascripts/boards/components/project_select.vue @@ -46,7 +46,7 @@ export default { selectable: true, data: (term, callback) => { this.loading = true; - return Api.groupProjects(this.groupId, term, {}, projects => { + return Api.groupProjects(this.groupId, term, { with_issues_enabled: true }, projects => { this.loading = false; callback(projects); }); @@ -54,7 +54,9 @@ export default { renderRow(project) { return ` <li> - <a href='#' class='dropdown-menu-link' data-project-id="${project.id}" data-project-name="${project.name}"> + <a href='#' class='dropdown-menu-link' data-project-id="${ + project.id + }" data-project-name="${project.name}"> ${_.escape(project.name)} </a> </li> diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue index b8f2e324d43..d681e6a431c 100644 --- a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue @@ -1,79 +1,77 @@ <script> - import Vue from 'vue'; - import Flash from '../../../flash'; - import { __ } from '../../../locale'; - import boardsStore from '../../stores/boards_store'; +import Vue from 'vue'; +import Flash from '../../../flash'; +import { __ } from '../../../locale'; +import boardsStore from '../../stores/boards_store'; - export default Vue.extend({ - props: { - issue: { - type: Object, - required: true, - }, - list: { - type: Object, - required: true, - }, +export default Vue.extend({ + props: { + issue: { + type: Object, + required: true, }, - computed: { - updateUrl() { - return this.issue.path; - }, + list: { + type: Object, + required: true, }, - methods: { - removeIssue() { - const { issue } = this; - const lists = issue.getLists(); - const req = this.buildPatchRequest(issue, lists); - - const data = { - issue: this.seedPatchRequest(issue, req), - }; + }, + computed: { + updateUrl() { + return this.issue.path; + }, + }, + methods: { + removeIssue() { + const { issue } = this; + const lists = issue.getLists(); + const req = this.buildPatchRequest(issue, lists); - if (data.issue.label_ids.length === 0) { - data.issue.label_ids = ['']; - } + const data = { + issue: this.seedPatchRequest(issue, req), + }; - // Post the remove data - Vue.http.patch(this.updateUrl, data).catch(() => { - Flash(__('Failed to remove issue from board, please try again.')); + if (data.issue.label_ids.length === 0) { + data.issue.label_ids = ['']; + } - lists.forEach(list => { - list.addIssue(issue); - }); - }); + // Post the remove data + Vue.http.patch(this.updateUrl, data).catch(() => { + Flash(__('Failed to remove issue from board, please try again.')); - // Remove from the frontend store lists.forEach(list => { - list.removeIssue(issue); + list.addIssue(issue); }); + }); - boardsStore.detail.issue = {}; - }, - /** - * Build the default patch request. - */ - buildPatchRequest(issue, lists) { - const listLabelIds = lists.map(list => list.label.id); + // Remove from the frontend store + lists.forEach(list => { + list.removeIssue(issue); + }); - const labelIds = issue.labels - .map(label => label.id) - .filter(id => !listLabelIds.includes(id)); + boardsStore.detail.issue = {}; + }, + /** + * Build the default patch request. + */ + buildPatchRequest(issue, lists) { + const listLabelIds = lists.map(list => list.label.id); + + const labelIds = issue.labels.map(label => label.id).filter(id => !listLabelIds.includes(id)); - return { - label_ids: labelIds, - }; - }, - /** - * Seed the given patch request. - * - * (This is overridden in EE) - */ - seedPatchRequest(issue, req) { - return req; - }, + return { + label_ids: labelIds, + }; + }, + /** + * Seed the given patch request. + * + * (This is overridden in EE) + */ + seedPatchRequest(issue, req) { + return req; }, - }); + }, +}); </script> <template> <div diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index acf41e5689e..c14d69c5d18 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -32,7 +32,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager { const tokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token'); // Remove all the tokens as they will be replaced by the search manager - [].forEach.call(tokens, (el) => { + [].forEach.call(tokens, el => { el.parentNode.removeChild(el); }); @@ -50,7 +50,10 @@ export default class FilteredSearchBoards extends FilteredSearchManager { canEdit(tokenName, tokenValue) { if (this.cantEdit.includes(tokenName)) return false; - return this.cantEditWithValue.findIndex(token => token.name === tokenName && - token.value === tokenValue) === -1; + return ( + this.cantEditWithValue.findIndex( + token => token.name === tokenName && token.value === tokenValue, + ) === -1 + ); } } diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 91861f2f9ee..61a3072ac27 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -32,9 +32,9 @@ export default () => { const $boardApp = document.getElementById('board-app'); // check for browser back and trigger a hard reload to circumvent browser caching. - window.addEventListener('pageshow', (event) => { - const isNavTypeBackForward = window.performance && - window.performance.navigation.type === NavigationType.TYPE_BACK_FORWARD; + window.addEventListener('pageshow', event => { + const isNavTypeBackForward = + window.performance && window.performance.navigation.type === NavigationType.TYPE_BACK_FORWARD; if (event.persisted || isNavTypeBackForward) { window.location.reload(); diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js b/app/assets/javascripts/boards/mixins/sortable_default_options.js index c9cde4effb9..983b28d2e67 100644 --- a/app/assets/javascripts/boards/mixins/sortable_default_options.js +++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js @@ -4,7 +4,8 @@ import $ from 'jquery'; import sortableConfig from '../../sortable/sortable_config'; export function sortableStart() { - $('.has-tooltip').tooltip('hide') + $('.has-tooltip') + .tooltip('hide') .tooltip('disable'); document.body.classList.add('is-dragging'); } @@ -15,7 +16,8 @@ export function sortableEnd() { } export function getBoardSortableDefaultOptions(obj) { - const touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch; + const touchEnabled = + 'ontouchstart' in window || (window.DocumentTouch && document instanceof DocumentTouch); const defaultSortOptions = Object.assign({}, sortableConfig, { filter: '.board-delete, .btn', @@ -26,6 +28,8 @@ export function getBoardSortableDefaultOptions(obj) { onEnd: sortableEnd, }); - Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; }); + Object.keys(obj).forEach(key => { + defaultSortOptions[key] = obj[key]; + }); return defaultSortOptions; } diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js index 52d04389b88..bb3b2865934 100644 --- a/app/assets/javascripts/boards/models/issue.js +++ b/app/assets/javascripts/boards/models/issue.js @@ -9,7 +9,7 @@ import IssueProject from './project'; import boardsStore from '../stores/boards_store'; class ListIssue { - constructor (obj, defaultAvatar) { + constructor(obj, defaultAvatar) { this.id = obj.id; this.iid = obj.iid; this.title = obj.title; @@ -39,54 +39,54 @@ class ListIssue { this.milestone = new ListMilestone(obj.milestone); } - obj.labels.forEach((label) => { + obj.labels.forEach(label => { this.labels.push(new ListLabel(label)); }); this.assignees = obj.assignees.map(a => new ListAssignee(a, defaultAvatar)); } - addLabel (label) { + addLabel(label) { if (!this.findLabel(label)) { this.labels.push(new ListLabel(label)); } } - findLabel (findLabel) { + findLabel(findLabel) { return this.labels.filter(label => label.title === findLabel.title)[0]; } - removeLabel (removeLabel) { + removeLabel(removeLabel) { if (removeLabel) { this.labels = this.labels.filter(label => removeLabel.title !== label.title); } } - removeLabels (labels) { + removeLabels(labels) { labels.forEach(this.removeLabel.bind(this)); } - addAssignee (assignee) { + addAssignee(assignee) { if (!this.findAssignee(assignee)) { this.assignees.push(new ListAssignee(assignee)); } } - findAssignee (findAssignee) { + findAssignee(findAssignee) { return this.assignees.filter(assignee => assignee.id === findAssignee.id)[0]; } - removeAssignee (removeAssignee) { + removeAssignee(removeAssignee) { if (removeAssignee) { this.assignees = this.assignees.filter(assignee => assignee.id !== removeAssignee.id); } } - removeAllAssignees () { + removeAllAssignees() { this.assignees = []; } - getLists () { + getLists() { return boardsStore.state.lists.filter(list => list.findIssue(this.id)); } @@ -102,14 +102,14 @@ class ListIssue { this.isLoading[key] = value; } - update () { + update() { const data = { issue: { milestone_id: this.milestone ? this.milestone.id : null, due_date: this.dueDate, - assignee_ids: this.assignees.length > 0 ? this.assignees.map((u) => u.id) : [0], - label_ids: this.labels.map((label) => label.id) - } + assignee_ids: this.assignees.length > 0 ? this.assignees.map(u => u.id) : [0], + label_ids: this.labels.map(label => label.id), + }, }; if (!data.issue.label_ids.length) { diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index 3161f1da8c9..dd3feedbc0e 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -234,11 +234,11 @@ class List { }); } - getTypeInfo (type) { + getTypeInfo(type) { return TYPES[type] || {}; } - onNewIssueResponse (issue, data) { + onNewIssueResponse(issue, data) { issue.id = data.id; issue.iid = data.iid; issue.project = data.project; diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js index 029b0971f2c..3de6eb056c2 100644 --- a/app/assets/javascripts/boards/services/board_service.js +++ b/app/assets/javascripts/boards/services/board_service.js @@ -19,7 +19,9 @@ export default class BoardService { } static generateIssuePath(boardId, id) { - return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${id ? `/${id}` : ''}`; + return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${ + id ? `/${id}` : '' + }`; } all() { @@ -54,7 +56,9 @@ export default class BoardService { getIssuesForList(id, filter = {}) { const data = { id }; - Object.keys(filter).forEach((key) => { data[key] = filter[key]; }); + Object.keys(filter).forEach(key => { + data[key] = filter[key]; + }); return axios.get(mergeUrlParams(data, this.generateIssuesPath(id))); } @@ -75,7 +79,9 @@ export default class BoardService { } getBacklog(data) { - return axios.get(mergeUrlParams(data, `${gon.relative_url_root}/-/boards/${this.boardId}/issues.json`)); + return axios.get( + mergeUrlParams(data, `${gon.relative_url_root}/-/boards/${this.boardId}/issues.json`), + ); } bulkUpdate(issueIds, extraData = {}) { diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 471955747fd..eefe14a1d79 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -20,20 +20,20 @@ const boardsStore = { issue: {}, list: {}, }, - create () { + create() { this.state.lists = []; this.filter.path = getUrlParamsArray().join('&'); this.detail = { issue: {}, }; }, - addList (listObj, defaultAvatar) { + addList(listObj, defaultAvatar) { const list = new List(listObj, defaultAvatar); this.state.lists.push(list); return list; }, - new (listObj) { + new(listObj) { const list = this.addList(listObj); const backlogList = this.findList('type', 'backlog', 'backlog'); @@ -50,44 +50,44 @@ const boardsStore = { }); this.removeBlankState(); }, - updateNewListDropdown (listId) { + updateNewListDropdown(listId) { $(`.js-board-list-${listId}`).removeClass('is-active'); }, - shouldAddBlankState () { + shouldAddBlankState() { // Decide whether to add the blank state - return !(this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'closed')[0]); + return !this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'closed')[0]; }, - addBlankState () { + addBlankState() { if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return; this.addList({ id: 'blank', list_type: 'blank', title: 'Welcome to your Issue Board!', - position: 0 + position: 0, }); this.state.lists = _.sortBy(this.state.lists, 'position'); }, - removeBlankState () { + removeBlankState() { this.removeList('blank'); Cookies.set('issue_board_welcome_hidden', 'true', { expires: 365 * 10, - path: '' + path: '', }); }, - welcomeIsHidden () { + welcomeIsHidden() { return Cookies.get('issue_board_welcome_hidden') === 'true'; }, - removeList (id, type = 'blank') { + removeList(id, type = 'blank') { const list = this.findList('id', id, type); if (!list) return; this.state.lists = this.state.lists.filter(list => list.id !== id); }, - moveList (listFrom, orderLists) { + moveList(listFrom, orderLists) { orderLists.forEach((id, i) => { const list = this.findList('id', parseInt(id, 10)); @@ -95,22 +95,25 @@ const boardsStore = { }); listFrom.update(); }, - moveIssueToList (listFrom, listTo, issue, newIndex) { + moveIssueToList(listFrom, listTo, issue, newIndex) { const issueTo = listTo.findIssue(issue.id); const issueLists = issue.getLists(); const listLabels = issueLists.map(listIssue => listIssue.label); if (!issueTo) { // Check if target list assignee is already present in this issue - if ((listTo.type === 'assignee' && listFrom.type === 'assignee') && - issue.findAssignee(listTo.assignee)) { + if ( + listTo.type === 'assignee' && + listFrom.type === 'assignee' && + issue.findAssignee(listTo.assignee) + ) { const targetIssue = listTo.findIssue(issue.id); targetIssue.removeAssignee(listFrom.assignee); } else if (listTo.type === 'milestone') { const currentMilestone = issue.milestone; const currentLists = this.state.lists - .filter(list => (list.type === 'milestone' && list.id !== listTo.id)) - .filter(list => list.issues.some(listIssue => issue.id === listIssue.id)); + .filter(list => list.type === 'milestone' && list.id !== listTo.id) + .filter(list => list.issues.some(listIssue => issue.id === listIssue.id)); issue.removeMilestone(currentMilestone); issue.addMilestone(listTo.milestone); @@ -126,7 +129,7 @@ const boardsStore = { } if (listTo.type === 'closed' && listFrom.type !== 'backlog') { - issueLists.forEach((list) => { + issueLists.forEach(list => { list.removeIssue(issue); }); issue.removeLabels(listLabels); @@ -144,26 +147,28 @@ const boardsStore = { return ( (listTo.type !== 'label' && listFrom.type === 'assignee') || (listTo.type !== 'assignee' && listFrom.type === 'label') || - (listFrom.type === 'backlog') + listFrom.type === 'backlog' ); }, - moveIssueInList (list, issue, oldIndex, newIndex, idArray) { + moveIssueInList(list, issue, oldIndex, newIndex, idArray) { const beforeId = parseInt(idArray[newIndex - 1], 10) || null; const afterId = parseInt(idArray[newIndex + 1], 10) || null; list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId); }, - findList (key, val, type = 'label') { - const filteredList = this.state.lists.filter((list) => { - const byType = type ? (list.type === type) || (list.type === 'assignee') || (list.type === 'milestone') : true; + findList(key, val, type = 'label') { + const filteredList = this.state.lists.filter(list => { + const byType = type + ? list.type === type || list.type === 'assignee' || list.type === 'milestone' + : true; return list[key] === val && byType; }); return filteredList[0]; }, - updateFiltersUrl () { + updateFiltersUrl() { window.history.pushState(null, null, `?${this.filter.path}`); - } + }, }; // hacks added in order to allow milestone_select to function properly diff --git a/app/assets/javascripts/boards/stores/modal_store.js b/app/assets/javascripts/boards/stores/modal_store.js index 0d9ac367a70..b7228bf7bf5 100644 --- a/app/assets/javascripts/boards/stores/modal_store.js +++ b/app/assets/javascripts/boards/stores/modal_store.js @@ -40,7 +40,7 @@ class ModalStore { toggleAll() { const select = this.selectedCount() !== this.store.issues.length; - this.store.issues.forEach((issue) => { + this.store.issues.forEach(issue => { const issueUpdate = issue; if (issueUpdate.selected !== select) { @@ -69,13 +69,14 @@ class ModalStore { removeSelectedIssue(issue, forcePurge = false) { if (this.store.activeTab === 'all' || forcePurge) { - this.store.selectedIssues = this.store.selectedIssues - .filter(fIssue => fIssue.id !== issue.id); + this.store.selectedIssues = this.store.selectedIssues.filter( + fIssue => fIssue.id !== issue.id, + ); } } purgeUnselectedIssues() { - this.store.selectedIssues.forEach((issue) => { + this.store.selectedIssues.forEach(issue => { if (!issue.selected) { this.removeSelectedIssue(issue, true); } @@ -87,8 +88,7 @@ class ModalStore { } findSelectedIssue(issue) { - return this.store.selectedIssues - .filter(filteredIssue => filteredIssue.id === issue.id)[0]; + return this.store.selectedIssues.filter(filteredIssue => filteredIssue.id === issue.id)[0]; } } diff --git a/app/assets/javascripts/breadcrumb.js b/app/assets/javascripts/breadcrumb.js index 1474d93dde6..a37838694ec 100644 --- a/app/assets/javascripts/breadcrumb.js +++ b/app/assets/javascripts/breadcrumb.js @@ -1,6 +1,6 @@ import $ from 'jquery'; -export const addTooltipToEl = (el) => { +export const addTooltipToEl = el => { const textEl = el.querySelector('.js-breadcrumb-item-text'); if (textEl && textEl.scrollWidth > textEl.offsetWidth) { @@ -14,17 +14,18 @@ export default () => { const breadcrumbs = document.querySelector('.js-breadcrumbs-list'); if (breadcrumbs) { - const topLevelLinks = [...breadcrumbs.children].filter(el => !el.classList.contains('dropdown')) + const topLevelLinks = [...breadcrumbs.children] + .filter(el => !el.classList.contains('dropdown')) .map(el => el.querySelector('a')) .filter(el => el); const $expander = $('.js-breadcrumbs-collapsed-expander'); topLevelLinks.forEach(el => addTooltipToEl(el)); - $expander.closest('.dropdown') - .on('show.bs.dropdown hide.bs.dropdown', (e) => { - $('.js-breadcrumbs-collapsed-expander', e.currentTarget).toggleClass('open') - .tooltip('hide'); - }); + $expander.closest('.dropdown').on('show.bs.dropdown hide.bs.dropdown', e => { + $('.js-breadcrumbs-collapsed-expander', e.currentTarget) + .toggleClass('open') + .tooltip('hide'); + }); } }; diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js index e338376fcaa..97a1645aa51 100644 --- a/app/assets/javascripts/build_artifacts.js +++ b/app/assets/javascripts/build_artifacts.js @@ -12,16 +12,16 @@ export default class BuildArtifacts { } // eslint-disable-next-line class-methods-use-this disablePropagation() { - $('.top-block').on('click', '.download', function (e) { + $('.top-block').on('click', '.download', function(e) { return e.stopPropagation(); }); - return $('.tree-holder').on('click', 'tr[data-link] a', function (e) { + return $('.tree-holder').on('click', 'tr[data-link] a', function(e) { return e.stopImmediatePropagation(); }); } // eslint-disable-next-line class-methods-use-this setupEntryClick() { - return $('.tree-holder').on('click', 'tr[data-link]', function () { + return $('.tree-holder').on('click', 'tr[data-link]', function() { visitUrl(this.dataset.link, convertPermissionToBoolean(this.dataset.externalLink)); }); } @@ -37,11 +37,15 @@ export default class BuildArtifacts { // We want the tooltip to show if you hover anywhere on the row // But be placed below and in the middle of the file name $('.js-artifact-tree-row') - .on('mouseenter', (e) => { - $(e.currentTarget).find('.js-artifact-tree-tooltip').tooltip('show'); + .on('mouseenter', e => { + $(e.currentTarget) + .find('.js-artifact-tree-tooltip') + .tooltip('show'); }) - .on('mouseleave', (e) => { - $(e.currentTarget).find('.js-artifact-tree-tooltip').tooltip('hide'); + .on('mouseleave', e => { + $(e.currentTarget) + .find('.js-artifact-tree-tooltip') + .tooltip('hide'); }); } } diff --git a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js index b33adff609f..c7a917457f3 100644 --- a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js +++ b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js @@ -7,11 +7,13 @@ import statusCodes from '../lib/utils/http_status'; import VariableList from './ci_variable_list'; function generateErrorBoxContent(errors) { - const errorList = [].concat(errors).map(errorString => ` + const errorList = [].concat(errors).map( + errorString => ` <li> ${_.escape(errorString)} </li> - `); + `, + ); return ` <p> @@ -25,13 +27,7 @@ function generateErrorBoxContent(errors) { // Used for the variable list on CI/CD projects/groups settings page export default class AjaxVariableList { - constructor({ - container, - saveButton, - errorBox, - formField = 'variables', - saveEndpoint, - }) { + constructor({ container, saveButton, errorBox, formField = 'variables', saveEndpoint }) { this.container = container; this.saveButton = saveButton; this.errorBox = errorBox; @@ -51,25 +47,28 @@ export default class AjaxVariableList { } onSaveClicked() { - const loadingIcon = this.saveButton.querySelector('.js-secret-variables-save-loading-icon'); + const loadingIcon = this.saveButton.querySelector('.js-ci-variables-save-loading-icon'); loadingIcon.classList.toggle('hide', false); this.errorBox.classList.toggle('hide', true); // We use this to prevent a user from changing a key before we have a chance // to match it up in `updateRowsWithPersistedVariables` this.variableList.toggleEnableRow(false); - return axios.patch(this.saveEndpoint, { - variables_attributes: this.variableList.getAllData(), - }, { - // We want to be able to process the `res.data` from a 400 error response - // and print the validation messages such as duplicate variable keys - validateStatus: status => ( - status >= statusCodes.OK && - status < statusCodes.MULTIPLE_CHOICES - ) || - status === statusCodes.BAD_REQUEST, - }) - .then((res) => { + return axios + .patch( + this.saveEndpoint, + { + variables_attributes: this.variableList.getAllData(), + }, + { + // We want to be able to process the `res.data` from a 400 error response + // and print the validation messages such as duplicate variable keys + validateStatus: status => + (status >= statusCodes.OK && status < statusCodes.MULTIPLE_CHOICES) || + status === statusCodes.BAD_REQUEST, + }, + ) + .then(res => { loadingIcon.classList.toggle('hide', true); this.variableList.toggleEnableRow(true); @@ -90,18 +89,21 @@ export default class AjaxVariableList { } updateRowsWithPersistedVariables(persistedVariables = []) { - const persistedVariableMap = [].concat(persistedVariables).reduce((variableMap, variable) => ({ - ...variableMap, - [variable.key]: variable, - }), {}); + const persistedVariableMap = [].concat(persistedVariables).reduce( + (variableMap, variable) => ({ + ...variableMap, + [variable.key]: variable, + }), + {}, + ); - this.container.querySelectorAll('.js-row').forEach((row) => { + this.container.querySelectorAll('.js-row').forEach(row => { // If we submitted a row that was destroyed, remove it so we don't try // to destroy it again which would cause a BE error const destroyInput = row.querySelector('.js-ci-variable-input-destroy'); if (convertPermissionToBoolean(destroyInput.value)) { row.remove(); - // Update the ID input so any future edits and `_destroy` will apply on the BE + // Update the ID input so any future edits and `_destroy` will apply on the BE } else { const key = row.querySelector('.js-ci-variable-input-key').value; const persistedVariable = persistedVariableMap[key]; diff --git a/app/assets/javascripts/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci_variable_list/ci_variable_list.js index 47efb3a8cee..7bdc18ce03e 100644 --- a/app/assets/javascripts/ci_variable_list/ci_variable_list.js +++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js @@ -16,10 +16,7 @@ function createEnvironmentItem(value) { } export default class VariableList { - constructor({ - container, - formField, - }) { + constructor({ container, formField }) { this.$container = $(container); this.formField = formField; this.environmentDropdownMap = new WeakMap(); @@ -71,7 +68,7 @@ export default class VariableList { this.initRow(rowEl); }); - this.$container.on('click', '.js-row-remove-button', (e) => { + this.$container.on('click', '.js-row-remove-button', e => { e.preventDefault(); this.removeRow($(e.currentTarget).closest('.js-row')); }); @@ -81,7 +78,7 @@ export default class VariableList { .join(','); // Remove any empty rows except the last row - this.$container.on('blur', inputSelector, (e) => { + this.$container.on('blur', inputSelector, e => { const $row = $(e.currentTarget).closest('.js-row'); if ($row.is(':not(:last-child)') && !this.checkIfRowTouched($row)) { @@ -136,7 +133,7 @@ export default class VariableList { $rowClone.removeAttr('data-is-persisted'); // Reset the inputs to their defaults - Object.keys(this.inputMap).forEach((name) => { + Object.keys(this.inputMap).forEach(name => { const entry = this.inputMap[name]; $rowClone.find(entry.selector).val(entry.default); }); @@ -171,7 +168,7 @@ export default class VariableList { } checkIfRowTouched($row) { - return Object.keys(this.inputMap).some((name) => { + return Object.keys(this.inputMap).some(name => { const entry = this.inputMap[name]; const $el = $row.find(entry.selector); return $el.length && $el.val() !== entry.default; @@ -190,11 +187,14 @@ export default class VariableList { getAllData() { // Ignore the last empty row because we don't want to try persist // a blank variable and run into validation problems. - const validRows = this.$container.find('.js-row').toArray().slice(0, -1); + const validRows = this.$container + .find('.js-row') + .toArray() + .slice(0, -1); - return validRows.map((rowEl) => { + return validRows.map(rowEl => { const resultant = {}; - Object.keys(this.inputMap).forEach((name) => { + Object.keys(this.inputMap).forEach(name => { const entry = this.inputMap[name]; const $input = $(rowEl).find(entry.selector); if ($input.length) { @@ -207,11 +207,16 @@ export default class VariableList { } getEnvironmentValues() { - const valueMap = this.$container.find(this.inputMap.environment_scope.selector).toArray() - .reduce((prevValueMap, envInput) => ({ - ...prevValueMap, - [envInput.value]: envInput.value, - }), {}); + const valueMap = this.$container + .find(this.inputMap.environment_scope.selector) + .toArray() + .reduce( + (prevValueMap, envInput) => ({ + ...prevValueMap, + [envInput.value]: envInput.value, + }), + {}, + ); return Object.keys(valueMap).map(createEnvironmentItem); } diff --git a/app/assets/javascripts/ci_variable_list/native_form_variable_list.js b/app/assets/javascripts/ci_variable_list/native_form_variable_list.js index 7cd5916ac9c..e7111c666a2 100644 --- a/app/assets/javascripts/ci_variable_list/native_form_variable_list.js +++ b/app/assets/javascripts/ci_variable_list/native_form_variable_list.js @@ -2,10 +2,7 @@ import $ from 'jquery'; import VariableList from './ci_variable_list'; // Used for the variable list on scheduled pipeline edit page -export default function setupNativeFormVariableList({ - container, - formField = 'variables', -}) { +export default function setupNativeFormVariableList({ container, formField = 'variables' }) { const $container = $(container); const variableList = new VariableList({ diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index 703b0c48934..c7ffb470d4d 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -1,6 +1,6 @@ <script> import _ from 'underscore'; -import helmInstallIllustration from '@gitlab-org/gitlab-svgs/dist/illustrations/kubernetes-installation.svg'; +import helmInstallIllustration from '@gitlab/svgs/dist/illustrations/kubernetes-installation.svg'; import elasticsearchLogo from 'images/cluster_app_logos/elasticsearch.png'; import gitlabLogo from 'images/cluster_app_logos/gitlab.png'; import helmLogo from 'images/cluster_app_logos/helm.png'; diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index f8e8bf3cc40..e9c580c5dfa 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -84,12 +84,8 @@ export default class ClusterStore { this.state.status = serverState.status; this.state.statusReason = serverState.status_reason; - serverState.applications.forEach((serverAppEntry) => { - const { - name: appId, - status, - status_reason: statusReason, - } = serverAppEntry; + serverState.applications.forEach(serverAppEntry => { + const { name: appId, status, status_reason: statusReason } = serverAppEntry; this.state.applications[appId] = { ...(this.state.applications[appId] || {}), diff --git a/app/assets/javascripts/comment_type_toggle.js b/app/assets/javascripts/comment_type_toggle.js index c74184949df..a259667bb75 100644 --- a/app/assets/javascripts/comment_type_toggle.js +++ b/app/assets/javascripts/comment_type_toggle.js @@ -24,36 +24,44 @@ class CommentTypeToggle { setConfig() { const config = { - InputSetter: [{ - input: this.noteTypeInput, - valueAttribute: 'data-value', - }, - { - input: this.submitButton, - valueAttribute: 'data-submit-text', - }], + InputSetter: [ + { + input: this.noteTypeInput, + valueAttribute: 'data-value', + }, + { + input: this.submitButton, + valueAttribute: 'data-submit-text', + }, + ], }; if (this.closeButton) { - config.InputSetter.push({ - input: this.closeButton, - valueAttribute: 'data-close-text', - }, { - input: this.closeButton, - valueAttribute: 'data-close-text', - inputAttribute: 'data-alternative-text', - }); + config.InputSetter.push( + { + input: this.closeButton, + valueAttribute: 'data-close-text', + }, + { + input: this.closeButton, + valueAttribute: 'data-close-text', + inputAttribute: 'data-alternative-text', + }, + ); } if (this.reopenButton) { - config.InputSetter.push({ - input: this.reopenButton, - valueAttribute: 'data-reopen-text', - }, { - input: this.reopenButton, - valueAttribute: 'data-reopen-text', - inputAttribute: 'data-alternative-text', - }); + config.InputSetter.push( + { + input: this.reopenButton, + valueAttribute: 'data-reopen-text', + }, + { + input: this.reopenButton, + valueAttribute: 'data-reopen-text', + inputAttribute: 'data-alternative-text', + }, + ); } return config; diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js index 30d9b656fec..d4ecfa4aa93 100644 --- a/app/assets/javascripts/commit/image_file.js +++ b/app/assets/javascripts/commit/image_file.js @@ -9,44 +9,60 @@ const viewModes = ['two-up', 'swipe']; export default class ImageFile { constructor(file) { this.file = file; - this.requestImageInfo($('.two-up.view .frame.deleted img', this.file), (function(_this) { - return function(deletedWidth, deletedHeight) { - return _this.requestImageInfo($('.two-up.view .frame.added img', _this.file), function(width, height) { - _this.initViewModes(); - - // Load two-up view after images are loaded - // so that we can display the correct width and height information - const $images = $('.two-up.view img', _this.file); - - $images.waitForImages(function() { - _this.initView('two-up'); + this.requestImageInfo( + $('.two-up.view .frame.deleted img', this.file), + (function(_this) { + return function(deletedWidth, deletedHeight) { + return _this.requestImageInfo($('.two-up.view .frame.added img', _this.file), function( + width, + height, + ) { + _this.initViewModes(); + + // Load two-up view after images are loaded + // so that we can display the correct width and height information + const $images = $('.two-up.view img', _this.file); + + $images.waitForImages(function() { + _this.initView('two-up'); + }); }); - }); - }; - })(this)); + }; + })(this), + ); } initViewModes() { const viewMode = viewModes[0]; $('.view-modes', this.file).removeClass('hide'); - $('.view-modes-menu', this.file).on('click', 'li', (function(_this) { - return function(event) { - if (!$(event.currentTarget).hasClass('active')) { - return _this.activateViewMode(event.currentTarget.className); - } - }; - })(this)); + $('.view-modes-menu', this.file).on( + 'click', + 'li', + (function(_this) { + return function(event) { + if (!$(event.currentTarget).hasClass('active')) { + return _this.activateViewMode(event.currentTarget.className); + } + }; + })(this), + ); return this.activateViewMode(viewMode); } activateViewMode(viewMode) { - $('.view-modes-menu li', this.file).removeClass('active').filter("." + viewMode).addClass('active'); - return $(".view:visible:not(." + viewMode + ")", this.file).fadeOut(200, (function(_this) { - return function() { - $(".view." + viewMode, _this.file).fadeIn(200); - return _this.initView(viewMode); - }; - })(this)); + $('.view-modes-menu li', this.file) + .removeClass('active') + .filter('.' + viewMode) + .addClass('active'); + return $('.view:visible:not(.' + viewMode + ')', this.file).fadeOut( + 200, + (function(_this) { + return function() { + $('.view.' + viewMode, _this.file).fadeIn(200); + return _this.initView(viewMode); + }; + })(this), + ); } initView(viewMode) { @@ -63,135 +79,154 @@ export default class ImageFile { $body.css('user-select', 'none'); }); - $body.off('mouseup').off('mousemove').on('mouseup', function() { - dragging = false; - $body.css('user-select', ''); - }) - .on('mousemove', function(e) { - var left; - if (!dragging) return; - - left = e.pageX - ($offsetEl.offset().left + padding); - - callback(e, left); - }); + $body + .off('mouseup') + .off('mousemove') + .on('mouseup', function() { + dragging = false; + $body.css('user-select', ''); + }) + .on('mousemove', function(e) { + var left; + if (!dragging) return; + + left = e.pageX - ($offsetEl.offset().left + padding); + + callback(e, left); + }); } prepareFrames(view) { var maxHeight, maxWidth; maxWidth = 0; maxHeight = 0; - $('.frame', view).each((function(_this) { - return function(index, frame) { - var height, width; - width = $(frame).width(); - height = $(frame).height(); - maxWidth = width > maxWidth ? width : maxWidth; - return maxHeight = height > maxHeight ? height : maxHeight; - }; - })(this)).css({ - width: maxWidth, - height: maxHeight - }); + $('.frame', view) + .each( + (function(_this) { + return function(index, frame) { + var height, width; + width = $(frame).width(); + height = $(frame).height(); + maxWidth = width > maxWidth ? width : maxWidth; + return (maxHeight = height > maxHeight ? height : maxHeight); + }; + })(this), + ) + .css({ + width: maxWidth, + height: maxHeight, + }); return [maxWidth, maxHeight]; } views = { 'two-up': function() { - return $('.two-up.view .wrap', this.file).each((function(_this) { - return function(index, wrap) { - $('img', wrap).each(function() { - var currentWidth; - currentWidth = $(this).width(); - if (currentWidth > availWidth / 2) { - return $(this).width(availWidth / 2); - } - }); - return _this.requestImageInfo($('img', wrap), function(width, height) { - $('.image-info .meta-width', wrap).text(width + "px"); - $('.image-info .meta-height', wrap).text(height + "px"); - return $('.image-info', wrap).removeClass('hide'); - }); - }; - })(this)); + return $('.two-up.view .wrap', this.file).each( + (function(_this) { + return function(index, wrap) { + $('img', wrap).each(function() { + var currentWidth; + currentWidth = $(this).width(); + if (currentWidth > availWidth / 2) { + return $(this).width(availWidth / 2); + } + }); + return _this.requestImageInfo($('img', wrap), function(width, height) { + $('.image-info .meta-width', wrap).text(width + 'px'); + $('.image-info .meta-height', wrap).text(height + 'px'); + return $('.image-info', wrap).removeClass('hide'); + }); + }; + })(this), + ); }, - 'swipe': function() { + swipe() { var maxHeight, maxWidth; maxWidth = 0; maxHeight = 0; - return $('.swipe.view', this.file).each((function(_this) { - return function(index, view) { - var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref; - ref = _this.prepareFrames(view), [maxWidth, maxHeight] = ref; - $swipeFrame = $('.swipe-frame', view); - $swipeWrap = $('.swipe-wrap', view); - $swipeBar = $('.swipe-bar', view); - - $swipeFrame.css({ - width: maxWidth + 16, - height: maxHeight + 28 - }); - $swipeWrap.css({ - width: maxWidth + 1, - height: maxHeight + 2 - }); - // Set swipeBar left position to match image frame - $swipeBar.css({ - left: 1 - }); - - wrapPadding = parseInt($swipeWrap.css('right').replace('px', ''), 10); - - _this.initDraggable($swipeBar, wrapPadding, function(e, left) { - if (left > 0 && left < $swipeFrame.width() - (wrapPadding * 2)) { - $swipeWrap.width((maxWidth + 1) - left); - $swipeBar.css('left', left); - } - }); - }; - })(this)); + return $('.swipe.view', this.file).each( + (function(_this) { + return function(index, view) { + var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref; + (ref = _this.prepareFrames(view)), ([maxWidth, maxHeight] = ref); + $swipeFrame = $('.swipe-frame', view); + $swipeWrap = $('.swipe-wrap', view); + $swipeBar = $('.swipe-bar', view); + + $swipeFrame.css({ + width: maxWidth + 16, + height: maxHeight + 28, + }); + $swipeWrap.css({ + width: maxWidth + 1, + height: maxHeight + 2, + }); + // Set swipeBar left position to match image frame + $swipeBar.css({ + left: 1, + }); + + wrapPadding = parseInt($swipeWrap.css('right').replace('px', ''), 10); + + _this.initDraggable($swipeBar, wrapPadding, function(e, left) { + if (left > 0 && left < $swipeFrame.width() - wrapPadding * 2) { + $swipeWrap.width(maxWidth + 1 - left); + $swipeBar.css('left', left); + } + }); + }; + })(this), + ); }, 'onion-skin': function() { var dragTrackWidth, maxHeight, maxWidth; maxWidth = 0; maxHeight = 0; dragTrackWidth = $('.drag-track', this.file).width() - $('.dragger', this.file).width(); - 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, maxHeight] = ref; - $frame = $('.onion-skin-frame', view); - $frameAdded = $('.frame.added', view); - $track = $('.drag-track', view); - $dragger = $('.dragger', $track); - - $frame.css({ - width: maxWidth + 16, - height: maxHeight + 28 - }); - $('.swipe-wrap', view).css({ - width: maxWidth + 1, - height: maxHeight + 2 - }); - $dragger.css({ - left: dragTrackWidth - }); - - $frameAdded.css('opacity', 1); - framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10); - - _this.initDraggable($dragger, framePadding, function(e, left) { - var opacity = left / dragTrackWidth; - - if (opacity >= 0 && opacity <= 1) { - $dragger.css('left', left); - $frameAdded.css('opacity', opacity); - } - }); - }; - })(this)); - } - } + 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, maxHeight] = ref); + $frame = $('.onion-skin-frame', view); + $frameAdded = $('.frame.added', view); + $track = $('.drag-track', view); + $dragger = $('.dragger', $track); + + $frame.css({ + width: maxWidth + 16, + height: maxHeight + 28, + }); + $('.swipe-wrap', view).css({ + width: maxWidth + 1, + height: maxHeight + 2, + }); + $dragger.css({ + left: dragTrackWidth, + }); + + $frameAdded.css('opacity', 1); + framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10); + + _this.initDraggable($dragger, framePadding, function(e, left) { + var opacity = left / dragTrackWidth; + + if (opacity >= 0 && opacity <= 1) { + $dragger.css('left', left); + $frameAdded.css('opacity', opacity); + } + }); + }; + })(this), + ); + }, + }; requestImageInfo(img, callback) { const domImg = img.get(0); @@ -199,11 +234,14 @@ export default class ImageFile { if (domImg.complete) { return callback.call(this, domImg.naturalWidth, domImg.naturalHeight); } else { - return img.on('load', (function(_this) { - return function() { - return callback.call(_this, domImg.naturalWidth, domImg.naturalHeight); - }; - })(this)); + return img.on( + 'load', + (function(_this) { + return function() { + return callback.call(_this, domImg.naturalWidth, domImg.naturalHeight); + }; + })(this), + ); } } } diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js index 3d89bf1316e..340a93e4e66 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js @@ -19,11 +19,13 @@ export default () => { const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); if (pipelineTableViewEl) { - // Update MR and Commits tabs - pipelineTableViewEl.addEventListener('update-pipelines-count', (event) => { - if (event.detail.pipelines && + // Update MR and Commits tabs + pipelineTableViewEl.addEventListener('update-pipelines-count', event => { + if ( + event.detail.pipelines && event.detail.pipelines.count && - event.detail.pipelines.count.all) { + event.detail.pipelines.count.all + ) { const badge = document.querySelector('.js-pipelines-mr-count'); badge.textContent = event.detail.pipelines.count.all; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index 4849b0fa3db..a2aa3d197e3 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -1,77 +1,73 @@ <script> - import PipelinesService from '../../pipelines/services/pipelines_service'; - import PipelineStore from '../../pipelines/stores/pipelines_store'; - import pipelinesMixin from '../../pipelines/mixins/pipelines'; +import PipelinesService from '../../pipelines/services/pipelines_service'; +import PipelineStore from '../../pipelines/stores/pipelines_store'; +import pipelinesMixin from '../../pipelines/mixins/pipelines'; - export default { - mixins: [ - pipelinesMixin, - ], - props: { - endpoint: { - type: String, - required: true, - }, - helpPagePath: { - type: String, - required: true, - }, - autoDevopsHelpPath: { - type: String, - required: true, - }, - errorStateSvgPath: { - type: String, - required: true, - }, - viewType: { - type: String, - required: false, - default: 'child', - }, +export default { + mixins: [pipelinesMixin], + props: { + endpoint: { + type: String, + required: true, }, + helpPagePath: { + type: String, + required: true, + }, + autoDevopsHelpPath: { + type: String, + required: true, + }, + errorStateSvgPath: { + type: String, + required: true, + }, + viewType: { + type: String, + required: false, + default: 'child', + }, + }, - data() { - const store = new PipelineStore(); + data() { + const store = new PipelineStore(); - return { - store, - state: store.state, - }; - }, + return { + store, + state: store.state, + }; + }, - computed: { - shouldRenderTable() { - return !this.isLoading && - this.state.pipelines.length > 0 && - !this.hasError; - }, - shouldRenderErrorState() { - return this.hasError && !this.isLoading; - }, + computed: { + shouldRenderTable() { + return !this.isLoading && this.state.pipelines.length > 0 && !this.hasError; }, - created() { - this.service = new PipelinesService(this.endpoint); + shouldRenderErrorState() { + return this.hasError && !this.isLoading; }, - methods: { - successCallback(resp) { - // depending of the endpoint the response can either bring a `pipelines` key or not. - const pipelines = resp.data.pipelines || resp.data; - this.setCommonData(pipelines); + }, + created() { + this.service = new PipelinesService(this.endpoint); + }, + methods: { + successCallback(resp) { + // depending of the endpoint the response can either bring a `pipelines` key or not. + const pipelines = resp.data.pipelines || resp.data; + this.setCommonData(pipelines); - const updatePipelinesEvent = new CustomEvent('update-pipelines-count', { - detail: { - pipelines: resp.data, - }, - }); + const updatePipelinesEvent = new CustomEvent('update-pipelines-count', { + detail: { + pipelines: resp.data, + }, + }); - // notifiy to update the count in tabs - if (this.$el.parentElement) { - this.$el.parentElement.dispatchEvent(updatePipelinesEvent); - } - }, + // notifiy to update the count in tabs + if (this.$el.parentElement) { + this.$el.parentElement.dispatchEvent(updatePipelinesEvent); + } }, - }; + }, +}; </script> <template> <div class="content-list pipelines"> diff --git a/app/assets/javascripts/commit_merge_requests.js b/app/assets/javascripts/commit_merge_requests.js index 102b4ee8463..3a0ab119df6 100644 --- a/app/assets/javascripts/commit_merge_requests.js +++ b/app/assets/javascripts/commit_merge_requests.js @@ -50,7 +50,7 @@ export function createContent(mergeRequests) { if (mergeRequests.length === 0) { $content.text(s__('Commits|No related merge requests found')); } else { - mergeRequests.forEach((mergeRequest) => { + mergeRequests.forEach(mergeRequest => { const $header = createHeader($content.children().length, mergeRequests.length); const $item = createItem(mergeRequest); $content.append($header); @@ -64,8 +64,9 @@ export function createContent(mergeRequests) { export function fetchCommitMergeRequests() { const $container = $('.merge-requests'); - axios.get($container.data('projectCommitPath')) - .then((response) => { + axios + .get($container.data('projectCommitPath')) + .then(response => { const $content = createContent(response.data); $container.html($content); diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index 9a3ea7a55b6..54e2589c707 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -32,22 +32,31 @@ export default class CommitsList { if (search === this.lastSearch) return Promise.resolve(); const commitsUrl = `${form.attr('action')}?${form.serialize()}`; this.content.fadeTo('fast', 0.5); - const params = form.serializeArray().reduce((acc, obj) => Object.assign(acc, { - [obj.name]: obj.value, - }), {}); + const params = form.serializeArray().reduce( + (acc, obj) => + Object.assign(acc, { + [obj.name]: obj.value, + }), + {}, + ); - return axios.get(form.attr('action'), { - params, - }) + return axios + .get(form.attr('action'), { + params, + }) .then(({ data }) => { this.lastSearch = search; this.content.html(data.html); this.content.fadeTo('fast', 1.0); // Change url so if user reload a page - search results are saved - window.history.replaceState({ - page: commitsUrl, - }, document.title, commitsUrl); + window.history.replaceState( + { + page: commitsUrl, + }, + document.title, + commitsUrl, + ); }) .catch(() => { this.content.fadeTo('fast', 1.0); @@ -75,8 +84,15 @@ export default class CommitsList { processedData = $processedData.not(`li.js-commit-header[data-day='${loadedShownDayFirst}']`); // Update commits count in the previous commits header. - commitsCount += Number($(processedData).nextUntil('li.js-commit-header').first().find('li.commit').length); - $commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${pluralize('commit', commitsCount)}`); + commitsCount += Number( + $(processedData) + .nextUntil('li.js-commit-header') + .first() + .find('li.commit').length, + ); + $commitsHeadersLast + .find('span.commits-count') + .text(`${commitsCount} ${pluralize('commit', commitsCount)}`); } localTimeAgo($processedData.find('.js-timeago')); diff --git a/app/assets/javascripts/commons/bootstrap.js b/app/assets/javascripts/commons/bootstrap.js index 50e2949ab55..fba30aea9ae 100644 --- a/app/assets/javascripts/commons/bootstrap.js +++ b/app/assets/javascripts/commons/bootstrap.js @@ -5,6 +5,14 @@ import 'bootstrap'; // custom jQuery functions $.fn.extend({ - disable() { return $(this).prop('disabled', true).addClass('disabled'); }, - enable() { return $(this).prop('disabled', false).removeClass('disabled'); }, + disable() { + return $(this) + .prop('disabled', true) + .addClass('disabled'); + }, + enable() { + return $(this) + .prop('disabled', false) + .removeClass('disabled'); + }, }); diff --git a/app/assets/javascripts/commons/gitlab_ui.js b/app/assets/javascripts/commons/gitlab_ui.js index 1411f7ffd5e..e93e1f5ea2c 100644 --- a/app/assets/javascripts/commons/gitlab_ui.js +++ b/app/assets/javascripts/commons/gitlab_ui.js @@ -1,17 +1,7 @@ import Vue from 'vue'; -import { - Pagination, - ProgressBar, - Modal, - LoadingIcon, - ModalDirective, - TooltipDirective, -} from '@gitlab-org/gitlab-ui'; +import { GlProgressBar, GlLoadingIcon, GlTooltipDirective } from '@gitlab-org/gitlab-ui'; -Vue.component('gl-pagination', Pagination); -Vue.component('gl-progress-bar', ProgressBar); -Vue.component('gl-ui-modal', Modal); -Vue.component('gl-loading-icon', LoadingIcon); +Vue.component('gl-progress-bar', GlProgressBar); +Vue.component('gl-loading-icon', GlLoadingIcon); -Vue.directive('gl-modal', ModalDirective); -Vue.directive('gl-tooltip', TooltipDirective); +Vue.directive('gl-tooltip', GlTooltipDirective); diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js index b0c85c2572e..1000c310e35 100644 --- a/app/assets/javascripts/confirm_danger_modal.js +++ b/app/assets/javascripts/confirm_danger_modal.js @@ -13,19 +13,23 @@ function openConfirmDangerModal($form, text) { $submit.disable(); $input.focus(); - $('.js-confirm-danger-input').off('input').on('input', function handleInput() { - const confirmText = rstrip($(this).val()); - if (confirmText === confirmTextMatch) { - $submit.enable(); - } else { - $submit.disable(); - } - }); - $('.js-confirm-danger-submit').off('click').on('click', () => $form.submit()); + $('.js-confirm-danger-input') + .off('input') + .on('input', function handleInput() { + const confirmText = rstrip($(this).val()); + if (confirmText === confirmTextMatch) { + $submit.enable(); + } else { + $submit.disable(); + } + }); + $('.js-confirm-danger-submit') + .off('click') + .on('click', () => $form.submit()); } export default function initConfirmDangerModal() { - $(document).on('click', '.js-confirm-danger', (e) => { + $(document).on('click', '.js-confirm-danger', e => { e.preventDefault(); const $btn = $(e.target); const $form = $btn.closest('form'); diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js index 3a50e73ad85..dff0adba25a 100644 --- a/app/assets/javascripts/contextual_sidebar.js +++ b/app/assets/javascripts/contextual_sidebar.js @@ -20,8 +20,11 @@ export default class ContextualSidebar { } bindEvents() { - document.addEventListener('click', (e) => { - if (!e.target.closest('.nav-sidebar') && (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md')) { + document.addEventListener('click', e => { + if ( + !e.target.closest('.nav-sidebar') && + (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md') + ) { this.toggleCollapsedSidebar(true); } }); diff --git a/app/assets/javascripts/create_item_dropdown.js b/app/assets/javascripts/create_item_dropdown.js index 8ef9aa7f529..916b190f469 100644 --- a/app/assets/javascripts/create_item_dropdown.js +++ b/app/assets/javascripts/create_item_dropdown.js @@ -36,7 +36,7 @@ export default class CreateItemDropdown { }, selectable: true, toggleLabel(selected) { - return (selected && 'id' in selected) ? _.escape(selected.title) : this.defaultToggleLabel; + return selected && 'id' in selected ? _.escape(selected.title) : this.defaultToggleLabel; }, fieldName: this.fieldName, text(item) { @@ -46,7 +46,7 @@ export default class CreateItemDropdown { return _.escape(item.id); }, onFilter: this.toggleCreateNewButton.bind(this), - clicked: (options) => { + clicked: options => { options.e.preventDefault(); this.onSelect(); }, @@ -77,9 +77,8 @@ export default class CreateItemDropdown { getData(term, callback) { this.getDataOption(term, (data = []) => { // Ensure the selected item isn't already in the data to avoid duplicates - const alreadyHasSelectedItem = this.selectedItem && data.some(item => - item.id === this.selectedItem.id, - ); + const alreadyHasSelectedItem = + this.selectedItem && data.some(item => item.id === this.selectedItem.id); let uniqueData = data; if (!alreadyHasSelectedItem) { @@ -106,9 +105,7 @@ export default class CreateItemDropdown { if (newValue) { this.selectedItem = this.createNewItemFromValue(newValue); - this.$dropdownContainer - .find('.js-dropdown-create-new-item code') - .text(newValue); + this.$dropdownContainer.find('.js-dropdown-create-new-item code').text(newValue); } this.toggleFooter(!newValue); diff --git a/app/assets/javascripts/create_label.js b/app/assets/javascripts/create_label.js index a999c21b2e9..28ca7d97314 100644 --- a/app/assets/javascripts/create_label.js +++ b/app/assets/javascripts/create_label.js @@ -37,7 +37,7 @@ export default class CreateLabelDropdown { addBinding() { const self = this; - this.$colorSuggestions.on('click', function (e) { + this.$colorSuggestions.on('click', function(e) { const $this = $(this); self.addColorValue(e, $this); }); @@ -47,7 +47,7 @@ export default class CreateLabelDropdown { this.$dropdownBack.on('click', this.resetForm.bind(this)); - this.$cancelButton.on('click', function (e) { + this.$cancelButton.on('click', function(e) { e.preventDefault(); e.stopPropagation(); @@ -79,13 +79,9 @@ export default class CreateLabelDropdown { } resetForm() { - this.$newLabelField - .val('') - .trigger('change'); + this.$newLabelField.val('').trigger('change'); - this.$newColorField - .val('') - .trigger('change'); + this.$newColorField.val('').trigger('change'); this.$colorPreview .css('background-color', '') @@ -97,31 +93,34 @@ export default class CreateLabelDropdown { e.preventDefault(); e.stopPropagation(); - Api.newLabel(this.namespacePath, this.projectPath, { - title: this.$newLabelField.val(), - color: this.$newColorField.val(), - }, (label) => { - this.$newLabelCreateButton.enable(); - - if (label.message) { - let errors; - - if (typeof label.message === 'string') { - errors = label.message; + Api.newLabel( + this.namespacePath, + this.projectPath, + { + title: this.$newLabelField.val(), + color: this.$newColorField.val(), + }, + label => { + this.$newLabelCreateButton.enable(); + + if (label.message) { + let errors; + + if (typeof label.message === 'string') { + errors = label.message; + } else { + errors = Object.keys(label.message) + .map(key => `${humanize(key)} ${label.message[key].join(', ')}`) + .join('<br/>'); + } + + this.$newLabelError.html(errors).show(); } else { - errors = Object.keys(label.message).map(key => - `${humanize(key)} ${label.message[key].join(', ')}`, - ).join('<br/>'); - } + this.$dropdownBack.trigger('click'); - this.$newLabelError - .html(errors) - .show(); - } else { - this.$dropdownBack.trigger('click'); - - $(document).trigger('created.label', label); - } - }); + $(document).trigger('created.label', label); + } + }, + ); } } diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue index aa52f120fe7..3589599986d 100644 --- a/app/assets/javascripts/deploy_keys/components/app.vue +++ b/app/assets/javascripts/deploy_keys/components/app.vue @@ -95,8 +95,10 @@ export default { .catch(() => new Flash(s__('DeployKeys|Error enabling deploy key'))); }, disableKey(deployKey, callback) { - // eslint-disable-next-line no-alert - if (window.confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))) { + if ( + // eslint-disable-next-line no-alert + window.confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?')) + ) { this.service .disableKey(deployKey.id) .then(this.fetchKeys) diff --git a/app/assets/javascripts/deploy_keys/service/index.js b/app/assets/javascripts/deploy_keys/service/index.js index 9dc3b21f6f6..268a37008c5 100644 --- a/app/assets/javascripts/deploy_keys/service/index.js +++ b/app/assets/javascripts/deploy_keys/service/index.js @@ -8,17 +8,14 @@ export default class DeployKeysService { } getKeys() { - return this.axios.get() - .then(response => response.data); + return this.axios.get().then(response => response.data); } enableKey(id) { - return this.axios.put(`${id}/enable`) - .then(response => response.data); + return this.axios.put(`${id}/enable`).then(response => response.data); } disableKey(id) { - return this.axios.put(`${id}/disable`) - .then(response => response.data); + return this.axios.put(`${id}/disable`).then(response => response.data); } } diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index a044fc1ab42..245f1a7c558 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -21,9 +21,12 @@ export default class Diff { }); const tab = document.getElementById('diffs'); - if (!tab || (tab && tab.dataset && tab.dataset.isLocked !== '')) FilesCommentButton.init($diffFile); + if (!tab || (tab && tab.dataset && tab.dataset.isLocked !== '')) + FilesCommentButton.init($diffFile); - const firstFile = $('.files').first().get(0); + const firstFile = $('.files') + .first() + .get(0); const canCreateNote = firstFile && firstFile.hasAttribute('data-can-create-note'); $diffFile.each((index, file) => imageDiffHelper.initImageDiff(file, canCreateNote)); @@ -73,9 +76,10 @@ export default class Diff { const view = file.data('view'); const params = { since, to, bottom, offset, unfold, view }; - axios.get(link, { params }) - .then(({ data }) => $target.parent().replaceWith(data)) - .catch(() => flash(__('An error occurred while loading diff'))); + axios + .get(link, { params }) + .then(({ data }) => $target.parent().replaceWith(data)) + .catch(() => flash(__('An error occurred while loading diff'))); } openAnchoredDiff(cb) { diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index edca45f22f9..59680959bb1 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -41,6 +41,11 @@ export default { required: true, }, }, + data() { + return { + assignedDiscussions: false, + }; + }, computed: { ...mapState({ isLoading: state => state.diffs.isLoading, @@ -58,9 +63,9 @@ export default { plainDiffPath: state => state.diffs.plainDiffPath, emailPatchPath: state => state.diffs.emailPatchPath, }), - ...mapState('diffs', ['showTreeList']), + ...mapState('diffs', ['showTreeList', 'isLoading']), ...mapGetters('diffs', ['isParallelView']), - ...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']), + ...mapGetters(['isNotesFetched', 'getNoteableData']), targetBranch() { return { branchName: this.targetBranchName, @@ -147,13 +152,10 @@ export default { } }, setDiscussions() { - if (this.isNotesFetched) { - requestIdleCallback( - () => { - this.assignDiscussionsToDiff(this.discussionsStructuredByLineCode); - }, - { timeout: 1000 }, - ); + if (this.isNotesFetched && !this.assignedDiscussions && !this.isLoading) { + this.assignedDiscussions = true; + + requestIdleCallback(() => this.assignDiscussionsToDiff(), { timeout: 1000 }); } }, adjustView() { diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue index 9bbf62c0eb6..29b5aff0fb1 100644 --- a/app/assets/javascripts/diffs/components/compare_versions.vue +++ b/app/assets/javascripts/diffs/components/compare_versions.vue @@ -40,17 +40,14 @@ export default { comparableDiffs() { return this.mergeRequestDiffs.slice(1); }, - isWhitespaceVisible() { - return !getParameterValues('w')[0]; - }, toggleWhitespaceText() { - if (this.isWhitespaceVisible) { + if (this.isWhitespaceVisible()) { return __('Hide whitespace changes'); } return __('Show whitespace changes'); }, toggleWhitespacePath() { - if (this.isWhitespaceVisible) { + if (this.isWhitespaceVisible()) { return mergeUrlParams({ w: 1 }, window.location.href); } @@ -67,6 +64,9 @@ export default { 'expandAllFiles', 'toggleShowTreeList', ]), + isWhitespaceVisible() { + return getParameterValues('w')[0] !== '1'; + }, }, }; </script> @@ -121,7 +121,7 @@ export default { </a> <a :href="toggleWhitespacePath" - class="btn btn-default" + class="btn btn-default qa-toggle-whitespace" > {{ toggleWhitespaceText }} </a> diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index f72c7a84e5c..958e57c5652 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -29,7 +29,7 @@ export default { }, computed: { ...mapState('diffs', ['currentDiffFileId']), - ...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']), + ...mapGetters(['isNotesFetched']), isCollapsed() { return this.file.collapsed || false; }, @@ -79,7 +79,7 @@ export default { .then(() => { requestIdleCallback( () => { - this.assignDiscussionsToDiff(this.discussionsStructuredByLineCode); + this.assignDiscussionsToDiff(); }, { timeout: 1000 }, ); diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue index cfe4273742f..91052b303a6 100644 --- a/app/assets/javascripts/diffs/components/tree_list.vue +++ b/app/assets/javascripts/diffs/components/tree_list.vue @@ -1,17 +1,30 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; +import { GlTooltipDirective } from '@gitlab-org/gitlab-ui'; +import { convertPermissionToBoolean } from '~/lib/utils/common_utils'; import Icon from '~/vue_shared/components/icon.vue'; import FileRow from '~/vue_shared/components/file_row.vue'; import FileRowStats from './file_row_stats.vue'; +const treeListStorageKey = 'mr_diff_tree_list'; + export default { + directives: { + GlTooltip: GlTooltipDirective, + }, components: { Icon, FileRow, }, data() { + const treeListStored = localStorage.getItem(treeListStorageKey); + const renderTreeList = + treeListStored !== null ? convertPermissionToBoolean(treeListStored) : true; + return { search: '', + renderTreeList, + focusSearch: false, }; }, computed: { @@ -20,15 +33,35 @@ export default { filteredTreeList() { const search = this.search.toLowerCase().trim(); - if (search === '') return this.tree; + if (search === '') return this.renderTreeList ? this.tree : this.allBlobs; return this.allBlobs.filter(f => f.name.toLowerCase().indexOf(search) >= 0); }, + rowDisplayTextKey() { + if (this.renderTreeList && this.search.trim() === '') { + return 'name'; + } + + return 'path'; + }, }, methods: { ...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']), clearSearch() { this.search = ''; + this.toggleFocusSearch(false); + }, + toggleRenderTreeList(toggle) { + this.renderTreeList = toggle; + localStorage.setItem(treeListStorageKey, this.renderTreeList); + }, + toggleFocusSearch(toggle) { + this.focusSearch = toggle; + }, + blurSearch() { + if (this.search.trim() === '') { + this.toggleFocusSearch(false); + } }, }, FileRowStats, @@ -37,28 +70,67 @@ export default { <template> <div class="tree-list-holder d-flex flex-column"> - <div class="append-bottom-8 position-relative tree-list-search"> - <icon - name="search" - class="position-absolute tree-list-icon" - /> - <input - v-model="search" - :placeholder="s__('MergeRequest|Filter files')" - type="search" - class="form-control" - /> - <button - v-show="search" - :aria-label="__('Clear search')" - type="button" - class="position-absolute tree-list-icon tree-list-clear-icon border-0 p-0" - @click="clearSearch" - > + <div class="append-bottom-8 position-relative tree-list-search d-flex"> + <div class="flex-fill d-flex"> <icon - name="close" + name="search" + class="position-absolute tree-list-icon" + /> + <input + v-model="search" + :placeholder="s__('MergeRequest|Filter files')" + type="search" + class="form-control" + @focus="toggleFocusSearch(true)" + @blur="blurSearch" /> - </button> + <button + v-show="search" + :aria-label="__('Clear search')" + type="button" + class="position-absolute bg-transparent tree-list-icon tree-list-clear-icon border-0 p-0" + @click="clearSearch" + > + <icon + name="close" + /> + </button> + </div> + <div + v-show="!focusSearch" + class="btn-group prepend-left-8 tree-list-view-toggle" + > + <button + v-gl-tooltip.hover + :aria-label="__('List view')" + :title="__('List view')" + :class="{ + active: !renderTreeList + }" + class="btn btn-default pt-0 pb-0 d-flex align-items-center" + type="button" + @click="toggleRenderTreeList(false)" + > + <icon + name="hamburger" + /> + </button> + <button + v-gl-tooltip.hover + :aria-label="__('Tree view')" + :title="__('Tree view')" + :class="{ + active: renderTreeList + }" + class="btn btn-default pt-0 pb-0 d-flex align-items-center" + type="button" + @click="toggleRenderTreeList(true)" + > + <icon + name="file-tree" + /> + </button> + </div> </div> <div class="tree-list-scroll" @@ -72,6 +144,8 @@ export default { :hide-extra-on-tree="true" :extra-component="$options.FileRowStats" :show-changed-icon="true" + :display-text-key="rowDisplayTextKey" + :should-truncate-start="true" @toggleTreeOpen="toggleTreeOpen" @clickFile="scrollToFile" /> diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 1e0b27b538d..ca8ae605cb4 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -5,7 +5,6 @@ import createFlash from '~/flash'; import { s__ } from '~/locale'; import { handleLocationHash, historyPushState } from '~/lib/utils/common_utils'; import { mergeUrlParams, getLocationHash } from '~/lib/utils/url_utility'; -import { reduceDiscussionsToLineCodes } from '../../notes/stores/utils'; import { getDiffPositionByLineCode, getNoteFormData } from './utils'; import * as types from './mutation_types'; import { @@ -36,18 +35,17 @@ export const fetchDiffFiles = ({ state, commit }) => { // This is adding line discussions to the actual lines in the diff tree // once for parallel and once for inline mode -export const assignDiscussionsToDiff = ({ state, commit }, allLineDiscussions) => { +export const assignDiscussionsToDiff = ( + { commit, state, rootState }, + discussions = rootState.notes.discussions, +) => { const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles); - Object.values(allLineDiscussions).forEach(discussions => { - if (discussions.length > 0) { - const { fileHash } = discussions[0]; - commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, { - fileHash, - discussions, - diffPositionByLineCode, - }); - } + discussions.filter(discussion => discussion.diff_discussion).forEach(discussion => { + commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, { + discussion, + diffPositionByLineCode, + }); }); }; @@ -190,9 +188,7 @@ export const saveDiffDiscussion = ({ dispatch }, { note, formData }) => { return dispatch('saveNote', postData, { root: true }) .then(result => dispatch('updateDiscussion', result.discussion, { root: true })) - .then(discussion => - dispatch('assignDiscussionsToDiff', reduceDiscussionsToLineCodes([discussion])), - ) + .then(discussion => dispatch('assignDiscussionsToDiff', [discussion])) .catch(() => createFlash(s__('MergeRequests|Saving the comment failed'))); }; diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index 0b4485ecdb5..38a65f111a2 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -90,53 +90,67 @@ export default { })); }, - [types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, discussions, diffPositionByLineCode }) { - const selectedFile = state.diffFiles.find(f => f.fileHash === fileHash); - const firstDiscussion = discussions[0]; - const isDiffDiscussion = firstDiscussion.diff_discussion; - const hasLineCode = firstDiscussion.line_code; - const diffPosition = diffPositionByLineCode[firstDiscussion.line_code]; - - if ( - selectedFile && - isDiffDiscussion && - hasLineCode && - diffPosition && + [types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode }) { + const { latestDiff } = state; + + const discussionLineCode = discussion.line_code; + const fileHash = discussion.diff_file.file_hash; + const lineCheck = ({ lineCode }) => + lineCode === discussionLineCode && isDiscussionApplicableToLine({ - discussion: firstDiscussion, - diffPosition, - latestDiff: state.latestDiff, - }) - ) { - const targetLine = selectedFile.parallelDiffLines.find( - line => - (line.left && line.left.lineCode === firstDiscussion.line_code) || - (line.right && line.right.lineCode === firstDiscussion.line_code), - ); - if (targetLine) { - if (targetLine.left && targetLine.left.lineCode === firstDiscussion.line_code) { - Object.assign(targetLine.left, { - discussions, - }); - } else { - Object.assign(targetLine.right, { - discussions, + discussion, + diffPosition: diffPositionByLineCode[lineCode], + latestDiff, + }); + + state.diffFiles = state.diffFiles.map(diffFile => { + if (diffFile.fileHash === fileHash) { + const file = { ...diffFile }; + + if (file.highlightedDiffLines) { + file.highlightedDiffLines = file.highlightedDiffLines.map(line => { + if (lineCheck(line)) { + return { + ...line, + discussions: line.discussions.concat(discussion), + }; + } + + return line; }); } - } - - if (selectedFile.highlightedDiffLines) { - const targetInlineLine = selectedFile.highlightedDiffLines.find( - line => line.lineCode === firstDiscussion.line_code, - ); - if (targetInlineLine) { - Object.assign(targetInlineLine, { - discussions, + if (file.parallelDiffLines) { + file.parallelDiffLines = file.parallelDiffLines.map(line => { + const left = line.left && lineCheck(line.left); + const right = line.right && lineCheck(line.right); + + if (left || right) { + return { + left: { + ...line.left, + discussions: left ? line.left.discussions.concat(discussion) : [], + }, + right: { + ...line.right, + discussions: right && !left ? line.right.discussions.concat(discussion) : [], + }, + }; + } + + return line; }); } + + if (!file.parallelDiffLines || !file.highlightedDiffLines) { + file.discussions = file.discussions.concat(discussion); + } + + return file; } - } + + return diffFile; + }); }, [types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode }) { diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index d2778bcdf1c..9987fbcb6a7 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -136,7 +136,7 @@ export default function dropzoneInput(form) { // removeAllFiles(true) stops uploading files (if any) // and remove them from dropzone files queue. - $cancelButton.on('click', (e) => { + $cancelButton.on('click', e => { e.preventDefault(); e.stopPropagation(); Dropzone.forElement($formDropzone.get(0)).removeAllFiles(true); @@ -146,8 +146,10 @@ export default function dropzoneInput(form) { // clear dropzone files queue, change status of failed files to undefined, // and add that files to the dropzone files queue again. // addFile() adds file to dropzone files queue and upload it. - $retryLink.on('click', (e) => { - const dropzoneInstance = Dropzone.forElement(e.target.closest('.js-main-target-form').querySelector('.div-dropzone')); + $retryLink.on('click', e => { + const dropzoneInstance = Dropzone.forElement( + e.target.closest('.js-main-target-form').querySelector('.div-dropzone'), + ); const failedFiles = dropzoneInstance.files; e.preventDefault(); @@ -156,7 +158,7 @@ export default function dropzoneInput(form) { // uploading of files that are being uploaded at the moment. dropzoneInstance.removeAllFiles(true); - failedFiles.map((failedFile) => { + failedFiles.map(failedFile => { const file = failedFile; if (file.status === Dropzone.ERROR) { @@ -168,7 +170,7 @@ export default function dropzoneInput(form) { }); }); // eslint-disable-next-line consistent-return - handlePaste = (event) => { + handlePaste = event => { const pasteEvent = event.originalEvent; if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) { const image = isImage(pasteEvent); @@ -182,7 +184,7 @@ export default function dropzoneInput(form) { } }; - isImage = (data) => { + isImage = data => { let i = 0; while (i < data.clipboardData.items.length) { const item = data.clipboardData.items[i]; @@ -203,8 +205,12 @@ export default function dropzoneInput(form) { const caretStart = textarea.selectionStart; const caretEnd = textarea.selectionEnd; const textEnd = $(child).val().length; - const beforeSelection = $(child).val().substring(0, caretStart); - const afterSelection = $(child).val().substring(caretEnd, textEnd); + const beforeSelection = $(child) + .val() + .substring(0, caretStart); + const afterSelection = $(child) + .val() + .substring(caretEnd, textEnd); $(child).val(beforeSelection + formattedText + afterSelection); textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length); textarea.style.height = `${textarea.scrollHeight}px`; @@ -212,11 +218,11 @@ export default function dropzoneInput(form) { return formTextarea.trigger('input'); }; - addFileToForm = (path) => { + addFileToForm = path => { $(form).append(`<input type="hidden" name="files[]" value="${_.escape(path)}">`); }; - getFilename = (e) => { + getFilename = e => { let value; if (window.clipboardData && window.clipboardData.getData) { value = window.clipboardData.getData('Text'); @@ -231,7 +237,7 @@ export default function dropzoneInput(form) { const closeSpinner = () => $uploadingProgressContainer.addClass('hide'); - const showError = (message) => { + const showError = message => { $uploadingErrorContainer.removeClass('hide'); $uploadingErrorMessage.html(message); }; @@ -252,14 +258,15 @@ export default function dropzoneInput(form) { showSpinner(); closeAlertMessage(); - axios.post(uploadsPath, formData) + axios + .post(uploadsPath, formData) .then(({ data }) => { const md = data.link.markdown; insertToTextArea(filename, md); closeSpinner(); }) - .catch((e) => { + .catch(e => { showError(e.response.data.message); closeSpinner(); }); @@ -267,7 +274,8 @@ export default function dropzoneInput(form) { updateAttachingMessage = (files, messageContainer) => { let attachingMessage; - const filesCount = files.filter(file => file.status === 'uploading' || file.status === 'queued').length; + const filesCount = files.filter(file => file.status === 'uploading' || file.status === 'queued') + .length; // Dinamycally change uploading files text depending on files number in // dropzone files queue. @@ -282,7 +290,10 @@ export default function dropzoneInput(form) { form.find('.markdown-selector').click(function onMarkdownClick(e) { e.preventDefault(); - $(this).closest('.gfm-form').find('.div-dropzone').click(); + $(this) + .closest('.gfm-form') + .find('.div-dropzone') + .click(); formTextarea.focus(); }); diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js index c7b5a35cc14..dbfcf8cc921 100644 --- a/app/assets/javascripts/due_date_select.js +++ b/app/assets/javascripts/due_date_select.js @@ -3,8 +3,7 @@ import Pikaday from 'pikaday'; import dateFormat from 'dateformat'; import { __ } from '~/locale'; import axios from './lib/utils/axios_utils'; -import { timeFor } from './lib/utils/datetime_utility'; -import { parsePikadayDate, pikadayToString } from './lib/utils/datefix'; +import { timeFor, parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility'; import boardsStore from './boards/stores/boards_store'; class DueDateSelect { diff --git a/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js b/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js index e9defb62cf8..c5f9fcf6358 100644 --- a/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js +++ b/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js @@ -13,9 +13,11 @@ const rainbowCodePoint = 127752; // parseInt('1F308', 16) function isRainbowFlagEmoji(emojiUnicode) { const characters = Array.from(emojiUnicode); // Length 4 because flags are made of 2 characters which are surrogate pairs - return emojiUnicode.length === 4 && + return ( + emojiUnicode.length === 4 && characters[0].codePointAt(0) === baseFlagCodePoint && - characters[1].codePointAt(0) === rainbowCodePoint; + characters[1].codePointAt(0) === rainbowCodePoint + ); } // Chrome <57 renders keycaps oddly @@ -26,22 +28,28 @@ function isKeycapEmoji(emojiUnicode) { } // Check for a skin tone variation emoji which aren't always supported -const tone1 = 127995;// parseInt('1F3FB', 16) -const tone5 = 127999;// parseInt('1F3FF', 16) +const tone1 = 127995; // parseInt('1F3FB', 16) +const tone5 = 127999; // parseInt('1F3FF', 16) function isSkinToneComboEmoji(emojiUnicode) { - return emojiUnicode.length > 2 && Array.from(emojiUnicode).some((char) => { - const cp = char.codePointAt(0); - return cp >= tone1 && cp <= tone5; - }); + return ( + emojiUnicode.length > 2 && + Array.from(emojiUnicode).some(char => { + const cp = char.codePointAt(0); + return cp >= tone1 && cp <= tone5; + }) + ); } // macOS supports most skin tone emoji's but // doesn't support the skin tone versions of horse racing -const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16) +const horseRacingCodePoint = 127943; // parseInt('1F3C7', 16) function isHorceRacingSkinToneComboEmoji(emojiUnicode) { const firstCharacter = Array.from(emojiUnicode)[0]; - return firstCharacter && firstCharacter.codePointAt(0) === horseRacingCodePoint && - isSkinToneComboEmoji(emojiUnicode); + return ( + firstCharacter && + firstCharacter.codePointAt(0) === horseRacingCodePoint && + isSkinToneComboEmoji(emojiUnicode) + ); } // Check for `family_*`, `kiss_*`, `couple_*` @@ -52,7 +60,7 @@ const personEndCodePoint = 128105; // parseInt('1F469', 16) function isPersonZwjEmoji(emojiUnicode) { let hasPersonEmoji = false; let hasZwj = false; - Array.from(emojiUnicode).forEach((character) => { + Array.from(emojiUnicode).forEach(character => { const cp = character.codePointAt(0); if (cp === zwj) { hasZwj = true; @@ -80,10 +88,7 @@ function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) { // in `isEmojiUnicodeSupported` logic function checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) { const isSkinToneResult = isSkinToneComboEmoji(emojiUnicode); - return ( - (unicodeSupportMap.skinToneModifier && isSkinToneResult) || - !isSkinToneResult - ); + return (unicodeSupportMap.skinToneModifier && isSkinToneResult) || !isSkinToneResult; } // Helper func so we don't have to run `isHorceRacingSkinToneComboEmoji` twice @@ -91,8 +96,7 @@ function checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) { function checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) { const isHorseRacingSkinToneResult = isHorceRacingSkinToneComboEmoji(emojiUnicode); return ( - (unicodeSupportMap.horseRacing && isHorseRacingSkinToneResult) || - !isHorseRacingSkinToneResult + (unicodeSupportMap.horseRacing && isHorseRacingSkinToneResult) || !isHorseRacingSkinToneResult ); } @@ -100,10 +104,7 @@ function checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnico // in `isEmojiUnicodeSupported` logic function checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode) { const isPersonZwjResult = isPersonZwjEmoji(emojiUnicode); - return ( - (unicodeSupportMap.personZwj && isPersonZwjResult) || - !isPersonZwjResult - ); + return (unicodeSupportMap.personZwj && isPersonZwjResult) || !isPersonZwjResult; } // Takes in a support map and determines whether @@ -111,16 +112,20 @@ function checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode) { // // Combines all the edge case tests into a one-stop shop method function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVersion) { - const isOlderThanChrome57 = unicodeSupportMap.meta && unicodeSupportMap.meta.isChrome && + const isOlderThanChrome57 = + unicodeSupportMap.meta && + unicodeSupportMap.meta.isChrome && unicodeSupportMap.meta.chromeVersion < 57; // For comments about each scenario, see the comments above each individual respective function - return unicodeSupportMap[unicodeVersion] && + return ( + unicodeSupportMap[unicodeVersion] && !(isOlderThanChrome57 && isKeycapEmoji(emojiUnicode)) && checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) && checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) && checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) && - checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode); + checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode) + ); } export { diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index bb9c139727e..b62a5bb1940 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -15,7 +15,7 @@ import CommitComponent from '../../vue_shared/components/commit.vue'; import eventHub from '../event_hub'; /** - * Envrionment Item Component + * Environment Item Component * * Renders a table row for each environment. */ @@ -60,7 +60,7 @@ export default { computed: { /** - * Verifies if `last_deployment` key exists in the current Envrionment. + * Verifies if `last_deployment` key exists in the current Environment. * This key is required to render most of the html - this method works has * an helper. * diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue index a0797b594cb..26bec125445 100644 --- a/app/assets/javascripts/environments/components/environment_monitoring.vue +++ b/app/assets/javascripts/environments/components/environment_monitoring.vue @@ -2,14 +2,14 @@ /** * Renders the Monitoring (Metrics) link in environments table. */ -import { Button } from '@gitlab-org/gitlab-ui'; +import { GlButton } from '@gitlab-org/gitlab-ui'; import Icon from '~/vue_shared/components/icon.vue'; import tooltip from '../../vue_shared/directives/tooltip'; export default { components: { Icon, - 'gl-button': Button, + GlButton, }, directives: { tooltip, diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue index e2ecf426e64..557b2062c64 100644 --- a/app/assets/javascripts/environments/components/environments_app.vue +++ b/app/assets/javascripts/environments/components/environments_app.vue @@ -1,94 +1,92 @@ <script> - import Flash from '../../flash'; - import { s__ } from '../../locale'; - import emptyState from './empty_state.vue'; - import eventHub from '../event_hub'; - import environmentsMixin from '../mixins/environments_mixin'; - import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; - import StopEnvironmentModal from './stop_environment_modal.vue'; +import Flash from '../../flash'; +import { s__ } from '../../locale'; +import emptyState from './empty_state.vue'; +import eventHub from '../event_hub'; +import environmentsMixin from '../mixins/environments_mixin'; +import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; +import StopEnvironmentModal from './stop_environment_modal.vue'; - export default { - components: { - emptyState, - StopEnvironmentModal, - }, +export default { + components: { + emptyState, + StopEnvironmentModal, + }, - mixins: [ - CIPaginationMixin, - environmentsMixin, - ], + mixins: [CIPaginationMixin, environmentsMixin], - props: { - endpoint: { - type: String, - required: true, - }, - canCreateEnvironment: { - type: Boolean, - required: true, - }, - canCreateDeployment: { - type: Boolean, - required: true, - }, - canReadEnvironment: { - type: Boolean, - required: true, - }, - cssContainerClass: { - type: String, - required: true, - }, - newEnvironmentPath: { - type: String, - required: true, - }, - helpPagePath: { - type: String, - required: true, - }, + props: { + endpoint: { + type: String, + required: true, }, - - created() { - eventHub.$on('toggleFolder', this.toggleFolder); + canCreateEnvironment: { + type: Boolean, + required: true, }, - - beforeDestroy() { - eventHub.$off('toggleFolder'); + canCreateDeployment: { + type: Boolean, + required: true, + }, + canReadEnvironment: { + type: Boolean, + required: true, + }, + cssContainerClass: { + type: String, + required: true, + }, + newEnvironmentPath: { + type: String, + required: true, }, + helpPagePath: { + type: String, + required: true, + }, + }, + + created() { + eventHub.$on('toggleFolder', this.toggleFolder); + }, - methods: { - toggleFolder(folder) { - this.store.toggleFolder(folder); + beforeDestroy() { + eventHub.$off('toggleFolder'); + }, - if (!folder.isOpen) { - this.fetchChildEnvironments(folder, true); - } - }, + methods: { + toggleFolder(folder) { + this.store.toggleFolder(folder); - fetchChildEnvironments(folder, showLoader = false) { - this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader); + if (!folder.isOpen) { + this.fetchChildEnvironments(folder, true); + } + }, - this.service.getFolderContent(folder.folder_path) - .then(response => this.store.setfolderContent(folder, response.data.environments)) - .then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false)) - .catch(() => { - Flash(s__('Environments|An error occurred while fetching the environments.')); - this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false); - }); - }, + fetchChildEnvironments(folder, showLoader = false) { + this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader); + + this.service + .getFolderContent(folder.folder_path) + .then(response => this.store.setfolderContent(folder, response.data.environments)) + .then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false)) + .catch(() => { + Flash(s__('Environments|An error occurred while fetching the environments.')); + this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false); + }); + }, - successCallback(resp) { - this.saveData(resp); + successCallback(resp) { + this.saveData(resp); - // We need to verify if any folder is open to also update it - const openFolders = this.store.getOpenFolders(); - if (openFolders.length) { - openFolders.forEach(folder => this.fetchChildEnvironments(folder)); - } - }, + // We need to verify if any folder is open to also update it + const openFolders = this.store.getOpenFolders(); + if (openFolders.length) { + openFolders.forEach(folder => this.fetchChildEnvironments(folder)); + } }, - }; + }, +}; </script> <template> <div :class="cssContainerClass"> diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js index 5ce9225a4bb..5808a2d4afa 100644 --- a/app/assets/javascripts/environments/stores/environments_store.js +++ b/app/assets/javascripts/environments/stores/environments_store.js @@ -34,14 +34,14 @@ export default class EnvironmentsStore { * @returns {Array} */ storeEnvironments(environments = []) { - const filteredEnvironments = environments.map((env) => { - const oldEnvironmentState = this.state.environments - .find((element) => { - if (env.latest) { - return element.id === env.latest.id; - } - return element.id === env.id; - }) || {}; + const filteredEnvironments = environments.map(env => { + const oldEnvironmentState = + this.state.environments.find(element => { + if (env.latest) { + return element.id === env.latest.id; + } + return element.id === env.id; + }) || {}; let filtered = {}; @@ -101,11 +101,11 @@ export default class EnvironmentsStore { } /** - * Toggles folder open property for the given folder. - * - * @param {Object} folder - * @return {Array} - */ + * Toggles folder open property for the given folder. + * + * @param {Object} folder + * @return {Array} + */ toggleFolder(folder) { return this.updateEnvironmentProp(folder, 'isOpen', !folder.isOpen); } @@ -119,7 +119,7 @@ export default class EnvironmentsStore { * @return {Object} */ setfolderContent(folder, environments) { - const updatedEnvironments = environments.map((env) => { + const updatedEnvironments = environments.map(env => { let updated = env; if (env.latest) { @@ -148,7 +148,7 @@ export default class EnvironmentsStore { updateEnvironmentProp(environment, prop, newValue) { const { environments } = this.state; - const updatedEnvironments = environments.map((env) => { + const updatedEnvironments = environments.map(env => { const updateEnv = Object.assign({}, env); if (env.id === environment.id) { updateEnv[prop] = newValue; diff --git a/app/assets/javascripts/experimental_flags.js b/app/assets/javascripts/experimental_flags.js index 1d60847147b..42b3fb8c6da 100644 --- a/app/assets/javascripts/experimental_flags.js +++ b/app/assets/javascripts/experimental_flags.js @@ -2,7 +2,7 @@ import $ from 'jquery'; import Cookies from 'js-cookie'; export default () => { - $('.js-experiment-feature-toggle').on('change', (e) => { + $('.js-experiment-feature-toggle').on('change', e => { const el = e.target; Cookies.set(el.name, el.value, { diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js index 6a4874e1ab8..3233f5c4f71 100644 --- a/app/assets/javascripts/files_comment_button.js +++ b/app/assets/javascripts/files_comment_button.js @@ -25,13 +25,15 @@ export default { if (!this.userCanCreateNote) { // data-can-create-note is an empty string when true, otherwise undefined - this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('canCreateNote') === ''; + this.userCanCreateNote = + $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('canCreateNote') === ''; } this.isParallelView = Cookies.get('diff_view') === 'parallel'; if (this.userCanCreateNote) { - $diffFile.on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e)) + $diffFile + .on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e)) .on('mouseleave', LINE_COLUMN_CLASSES, e => this.hideButton(this.isParallelView, e)); } }, @@ -64,9 +66,11 @@ export default { }, validateButtonParent(buttonParentElement) { - return !buttonParentElement.classList.contains(EMPTY_CELL_CLASS) && + return ( + !buttonParentElement.classList.contains(EMPTY_CELL_CLASS) && !buttonParentElement.classList.contains(UNFOLDABLE_LINE_CLASS) && !buttonParentElement.classList.contains(NO_COMMENT_CLASS) && - !buttonParentElement.parentNode.classList.contains(DIFF_EXPANDED_CLASS); + !buttonParentElement.parentNode.classList.contains(DIFF_EXPANDED_CLASS) + ); }, }; diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js index b17ba3c21db..64b09c8b62c 100644 --- a/app/assets/javascripts/filterable_list.js +++ b/app/assets/javascripts/filterable_list.js @@ -65,12 +65,15 @@ export default class FilterableList { this.isBusy = true; - return axios.get(this.getFilterEndpoint(), { - params, - }).then((res) => { - this.onFilterSuccess(res, params); - this.onFilterComplete(); - }).catch(() => this.onFilterComplete()); + return axios + .get(this.getFilterEndpoint(), { + params, + }) + .then(res => { + this.onFilterSuccess(res, params); + this.onFilterComplete(); + }) + .catch(() => this.onFilterComplete()); } onFilterSuccess(response, queryData) { @@ -81,9 +84,13 @@ export default class FilterableList { // Change url so if user reload a page - search results are saved const currentPath = this.getPagePath(queryData); - return window.history.replaceState({ - page: currentPath, - }, document.title, currentPath); + return window.history.replaceState( + { + page: currentPath, + }, + document.title, + currentPath, + ); } onFilterComplete() { diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js index d36f38a70b5..d5027590bb7 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_user.js @@ -39,8 +39,9 @@ export default class DropdownUser extends FilteredSearchDropdown { } itemClicked(e) { - super.itemClicked(e, - selected => selected.querySelector('.dropdown-light-content').innerText.trim()); + super.itemClicked(e, selected => + selected.querySelector('.dropdown-light-content').innerText.trim(), + ); } renderContent(forceShowList = false) { @@ -68,7 +69,7 @@ export default class DropdownUser extends FilteredSearchDropdown { // Removes the first character if it is a quotation so that we can search // with multiple words - if (value[0] === '"' || value[0] === '\'') { + if (value[0] === '"' || value[0] === "'") { value = value.slice(1); } diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js index 4eb67ff7649..146d3ba963c 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js @@ -85,7 +85,7 @@ export default class FilteredSearchDropdown { } dispatchInputEvent() { - // Propogate input change to FilteredSearchDropdownManager + // Propagate input change to FilteredSearchDropdownManager // so that it can determine which dropdowns to open this.input.dispatchEvent( new CustomEvent('input', { 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 cd3d532c958..57ec6603d80 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -108,7 +108,7 @@ export default class FilteredSearchDropdownManager { }, }; - supportedTokens.forEach((type) => { + supportedTokens.forEach(type => { if (availableMappings[type]) { allowedMappings[type] = availableMappings[type]; } @@ -142,10 +142,7 @@ export default class FilteredSearchDropdownManager { } static addWordToInput(tokenName, tokenValue = '', clicked = false, options = {}) { - const { - uppercaseTokenName = false, - capitalizeTokenValue = false, - } = options; + const { uppercaseTokenName = false, capitalizeTokenValue = false } = options; const input = FilteredSearchContainer.container.querySelector('.filtered-search'); FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue, { uppercaseTokenName, @@ -164,13 +161,16 @@ export default class FilteredSearchDropdownManager { updateDropdownOffset(key) { // Always align dropdown with the input field - let offset = this.filteredSearchInput.getBoundingClientRect().left - this.container.querySelector('.scroll-container').getBoundingClientRect().left; + let offset = + this.filteredSearchInput.getBoundingClientRect().left - + this.container.querySelector('.scroll-container').getBoundingClientRect().left; const maxInputWidth = 240; const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth; // Make sure offset never exceeds the input container - const offsetMaxWidth = this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth; + const offsetMaxWidth = + this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth; if (offsetMaxWidth < offset) { offset = offsetMaxWidth; } @@ -196,8 +196,7 @@ export default class FilteredSearchDropdownManager { const glArguments = Object.assign({}, defaultArguments, extraArguments); // Passing glArguments to `new glClass(<arguments>)` - mappingKey.reference = - new (Function.prototype.bind.apply(glClass, [null, glArguments]))(); + mappingKey.reference = new (Function.prototype.bind.apply(glClass, [null, glArguments]))(); } if (firstLoad) { @@ -224,8 +223,8 @@ export default class FilteredSearchDropdownManager { } const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase()); - const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key - && this.mapping[match.key]; + const shouldOpenFilterDropdown = + match && this.currentDropdown !== match.key && this.mapping[match.key]; const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { @@ -236,8 +235,10 @@ export default class FilteredSearchDropdownManager { setDropdown() { const query = DropdownUtils.getSearchQuery(true); - const { lastToken, searchToken } = - this.tokenizer.processTokens(query, this.filteredSearchTokenKeys.getKeys()); + const { lastToken, searchToken } = this.tokenizer.processTokens( + query, + this.filteredSearchTokenKeys.getKeys(), + ); if (this.currentDropdown) { this.updateCurrentDropdownOffset(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 54533ebb70d..4a2af02b40a 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -1,8 +1,5 @@ import _ from 'underscore'; -import { - getParameterByName, - getUrlParamsArray, -} from '~/lib/utils/common_utils'; +import { getParameterByName, getUrlParamsArray } from '~/lib/utils/common_utils'; import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; import { visitUrl } from '../lib/utils/url_utility'; import Flash from '../flash'; @@ -48,24 +45,28 @@ export default class FilteredSearchManager { isLocalStorageAvailable: RecentSearchesService.isAvailable(), allowedKeys: this.filteredSearchTokenKeys.getKeys(), }); - this.searchHistoryDropdownElement = document.querySelector('.js-filtered-search-history-dropdown'); - const fullPath = this.searchHistoryDropdownElement ? - this.searchHistoryDropdownElement.dataset.fullPath : 'project'; + this.searchHistoryDropdownElement = document.querySelector( + '.js-filtered-search-history-dropdown', + ); + const fullPath = this.searchHistoryDropdownElement + ? this.searchHistoryDropdownElement.dataset.fullPath + : 'project'; const recentSearchesKey = `${fullPath}-${this.recentsStorageKeyNames[this.page]}`; this.recentSearchesService = new RecentSearchesService(recentSearchesKey); } setup() { // Fetch recent searches from localStorage - this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch() - .catch((error) => { + this.fetchingRecentSearchesPromise = this.recentSearchesService + .fetch() + .catch(error => { if (error.name === 'RecentSearchesServiceError') return undefined; // eslint-disable-next-line no-new new Flash('An error occurred while parsing recent searches'); // Gracefully fail to empty array return []; }) - .then((searches) => { + .then(searches => { if (!searches) { return; } @@ -120,7 +121,7 @@ export default class FilteredSearchManager { if (this.stateFilters) { this.searchStateWrapper = this.searchState.bind(this); - this.applyToStateFilters((filterEl) => { + this.applyToStateFilters(filterEl => { filterEl.addEventListener('click', this.searchStateWrapper); }); } @@ -128,14 +129,14 @@ export default class FilteredSearchManager { unbindStateEvents() { if (this.stateFilters) { - this.applyToStateFilters((filterEl) => { + this.applyToStateFilters(filterEl => { filterEl.removeEventListener('click', this.searchStateWrapper); }); } } applyToStateFilters(callback) { - this.stateFilters.querySelectorAll('a[data-state]').forEach((filterEl) => { + this.stateFilters.querySelectorAll('a[data-state]').forEach(filterEl => { if (this.states.indexOf(filterEl.dataset.state) > -1) { callback(filterEl); } @@ -207,7 +208,7 @@ export default class FilteredSearchManager { let backspaceCount = 0; // closure for keeping track of the number of backspace keystrokes - return (e) => { + return e => { // 8 = Backspace Key // 46 = Delete Key if (e.keyCode === 8 || e.keyCode === 46) { @@ -274,8 +275,12 @@ export default class FilteredSearchManager { const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null; const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null; - if (!isElementInFilteredSearch && !isElementInDynamicFilterDropdown && - !isElementInStaticFilterDropdown && inputContainer) { + if ( + !isElementInFilteredSearch && + !isElementInDynamicFilterDropdown && + !isElementInStaticFilterDropdown && + inputContainer + ) { inputContainer.classList.remove('focus'); } } @@ -368,7 +373,7 @@ export default class FilteredSearchManager { const removeElements = []; - [].forEach.call(this.tokensContainer.children, (t) => { + [].forEach.call(this.tokensContainer.children, t => { let canClearToken = t.classList.contains('js-visual-token'); if (canClearToken) { @@ -381,7 +386,7 @@ export default class FilteredSearchManager { } }); - removeElements.forEach((el) => { + removeElements.forEach(el => { el.parentElement.removeChild(el); }); @@ -397,13 +402,14 @@ export default class FilteredSearchManager { handleInputVisualToken() { const input = this.filteredSearchInput; - const { tokens, searchToken } - = this.tokenizer.processTokens(input.value, this.filteredSearchTokenKeys.getKeys()); - const { isLastVisualTokenValid } - = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + const { tokens, searchToken } = this.tokenizer.processTokens( + input.value, + this.filteredSearchTokenKeys.getKeys(), + ); + const { isLastVisualTokenValid } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); if (isLastVisualTokenValid) { - tokens.forEach((t) => { + tokens.forEach(t => { input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, ''); FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`, { uppercaseTokenName: this.filteredSearchTokenKeys.shouldUppercaseTokenName(t.key), @@ -453,15 +459,17 @@ export default class FilteredSearchManager { saveCurrentSearchQuery() { // Don't save before we have fetched the already saved searches - this.fetchingRecentSearchesPromise.then(() => { - const searchQuery = DropdownUtils.getSearchQuery(); - if (searchQuery.length > 0) { - const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery); - this.recentSearchesService.save(resultantSearches); - } - }).catch(() => { - // https://gitlab.com/gitlab-org/gitlab-ce/issues/30821 - }); + this.fetchingRecentSearchesPromise + .then(() => { + const searchQuery = DropdownUtils.getSearchQuery(); + if (searchQuery.length > 0) { + const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery); + this.recentSearchesService.save(resultantSearches); + } + }) + .catch(() => { + // https://gitlab.com/gitlab-org/gitlab-ce/issues/30821 + }); } // allows for modifying params array when a param can't be included in the URL (e.g. Service Desk) @@ -475,7 +483,7 @@ export default class FilteredSearchManager { const usernameParams = this.getUsernameParams(); let hasFilteredSearch = false; - params.forEach((p) => { + params.forEach(p => { const split = p.split('='); const keyParam = decodeURIComponent(split[0]); const value = split[1]; @@ -486,11 +494,9 @@ export default class FilteredSearchManager { if (condition) { hasFilteredSearch = true; const canEdit = this.canEdit && this.canEdit(condition.tokenKey); - FilteredSearchVisualTokens.addFilterVisualToken( - condition.tokenKey, - condition.value, - { canEdit }, - ); + FilteredSearchVisualTokens.addFilterVisualToken(condition.tokenKey, condition.value, { + canEdit, + }); } else { // Sanitize value since URL converts spaces into + // Replace before decode so that we know what was originally + versus the encoded + @@ -510,7 +516,7 @@ export default class FilteredSearchManager { if (sanitizedValue.indexOf(' ') !== -1) { // Prefer ", but use ' if required - quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\''; + quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : "'"; } hasFilteredSearch = true; @@ -531,7 +537,9 @@ export default class FilteredSearchManager { hasFilteredSearch = true; const tokenName = 'assignee'; const canEdit = this.canEdit && this.canEdit(tokenName); - FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, { canEdit }); + FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, { + canEdit, + }); } } else if (!match && keyParam === 'author_id') { const id = parseInt(value, 10); @@ -539,7 +547,9 @@ export default class FilteredSearchManager { hasFilteredSearch = true; const tokenName = 'author'; const canEdit = this.canEdit && this.canEdit(tokenName); - FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, { canEdit }); + FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, { + canEdit, + }); } } else if (!match && keyParam === 'search') { hasFilteredSearch = true; @@ -580,9 +590,11 @@ export default class FilteredSearchManager { const currentState = state || getParameterByName('state') || 'opened'; paths.push(`state=${currentState}`); - tokens.forEach((token) => { - const condition = this.filteredSearchTokenKeys - .searchByConditionKeyValue(token.key, token.value.toLowerCase()); + tokens.forEach(token => { + const condition = this.filteredSearchTokenKeys.searchByConditionKeyValue( + token.key, + token.value.toLowerCase(), + ); const tokenConfig = this.filteredSearchTokenKeys.searchByKey(token.key) || {}; const { param } = tokenConfig; @@ -601,8 +613,10 @@ export default class FilteredSearchManager { tokenValue = tokenValue.toLowerCase(); } - if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') || - (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) { + if ( + (tokenValue[0] === "'" && tokenValue[tokenValue.length - 1] === "'") || + (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"') + ) { tokenValue = tokenValue.slice(1, tokenValue.length - 1); } @@ -613,7 +627,10 @@ export default class FilteredSearchManager { }); if (searchToken) { - const sanitized = searchToken.split(' ').map(t => encodeURIComponent(t)).join('+'); + const sanitized = searchToken + .split(' ') + .map(t => encodeURIComponent(t)) + .join('+'); paths.push(`search=${sanitized}`); } @@ -630,7 +647,7 @@ export default class FilteredSearchManager { const usernamesById = {}; try { const attribute = this.filteredSearchInput.getAttribute('data-username-params'); - JSON.parse(attribute).forEach((user) => { + JSON.parse(attribute).forEach(user => { usernamesById[user.id] = user.username; }); } catch (e) { diff --git a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js index b70125c80ca..bb0ecb8efe7 100644 --- a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js +++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js @@ -58,17 +58,22 @@ export const alternativeTokenKeys = [ export const conditions = [ { - url: 'assignee_id=0', + url: 'assignee_id=None', tokenKey: 'assignee', value: 'none', }, { - url: 'milestone_title=No+Milestone', + url: 'assignee_id=Any', + tokenKey: 'assignee', + value: 'any', + }, + { + url: 'milestone_title=None', tokenKey: 'milestone', value: 'none', }, { - url: 'milestone_title=Any+Milestone', + url: 'milestone_title=Any', tokenKey: 'milestone', value: 'any', }, @@ -87,6 +92,16 @@ export const conditions = [ tokenKey: 'label', value: 'none', }, + { + url: 'my_reaction_emoji=None', + tokenKey: 'my-reaction', + value: 'none', + }, + { + url: 'my_reaction_emoji=Any', + tokenKey: 'my-reaction', + value: 'any', + }, ]; const IssuableFilteredSearchTokenKeys = new FilteredSearchTokenKeys( diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js index a29de9ae899..c2397842125 100644 --- a/app/assets/javascripts/flash.js +++ b/app/assets/javascripts/flash.js @@ -8,14 +8,19 @@ const hideFlash = (flashEl, fadeTransition = true) => { }); } - flashEl.addEventListener('transitionend', () => { - flashEl.remove(); - window.dispatchEvent(new Event('resize')); - if (document.body.classList.contains('flash-shown')) document.body.classList.remove('flash-shown'); - }, { - once: true, - passive: true, - }); + flashEl.addEventListener( + 'transitionend', + () => { + flashEl.remove(); + window.dispatchEvent(new Event('resize')); + if (document.body.classList.contains('flash-shown')) + document.body.classList.remove('flash-shown'); + }, + { + once: true, + passive: true, + }, + ); if (!fadeTransition) flashEl.dispatchEvent(new Event('transitionend')); }; @@ -30,12 +35,14 @@ const createAction = config => ` </a> `; -const createFlashEl = (message, type, isInContentWrapper = false) => ` +const createFlashEl = (message, type, isFixedLayout = false) => ` <div class="flash-${type}" > <div - class="flash-text ${isInContentWrapper ? 'container-fluid container-limited' : ''}" + class="flash-text ${ + isFixedLayout ? 'container-fluid container-limited limit-container-width' : '' + }" > ${_.escape(message)} </div> @@ -69,12 +76,15 @@ const createFlash = function createFlash( addBodyClass = false, ) { const flashContainer = parent.querySelector('.flash-container'); + const navigation = parent.querySelector('.content'); if (!flashContainer) return null; - const isInContentWrapper = flashContainer.parentNode.classList.contains('content-wrapper'); + const isFixedLayout = navigation + ? navigation.parentNode.classList.contains('container-limited') + : true; - flashContainer.innerHTML = createFlashEl(message, type, isInContentWrapper); + flashContainer.innerHTML = createFlashEl(message, type, isFixedLayout); const flashEl = flashContainer.querySelector(`.flash-${type}`); removeFlashClickListener(flashEl, fadeTransition); @@ -83,7 +93,9 @@ const createFlash = function createFlash( flashEl.innerHTML += createAction(actionConfig); if (actionConfig.clickHandler) { - flashEl.querySelector('.flash-action').addEventListener('click', e => actionConfig.clickHandler(e)); + flashEl + .querySelector('.flash-action') + .addEventListener('click', e => actionConfig.clickHandler(e)); } } @@ -94,11 +106,5 @@ const createFlash = function createFlash( return flashContainer; }; -export { - createFlash as default, - createFlashEl, - createAction, - hideFlash, - removeFlashClickListener, -}; +export { createFlash as default, createFlashEl, createAction, hideFlash, removeFlashClickListener }; window.Flash = createFlash; diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js index f820f0dc3f0..3ac00c51df4 100644 --- a/app/assets/javascripts/fly_out_nav.js +++ b/app/assets/javascripts/fly_out_nav.js @@ -11,9 +11,13 @@ let sidebar; export const mousePos = []; -export const setSidebar = (el) => { sidebar = el; }; +export const setSidebar = el => { + sidebar = el; +}; export const getOpenMenu = () => currentOpenMenu; -export const setOpenMenu = (menu = null) => { currentOpenMenu = menu; }; +export const setOpenMenu = (menu = null) => { + currentOpenMenu = menu; +}; export const slope = (a, b) => (b.y - a.y) / (b.x - a.x); @@ -21,9 +25,10 @@ let headerHeight = 50; export const getHeaderHeight = () => headerHeight; -export const isSidebarCollapsed = () => sidebar && sidebar.classList.contains('sidebar-collapsed-desktop'); +export const isSidebarCollapsed = () => + sidebar && sidebar.classList.contains('sidebar-collapsed-desktop'); -export const canShowActiveSubItems = (el) => { +export const canShowActiveSubItems = el => { if (el.classList.contains('active') && !isSidebarCollapsed()) { return false; } @@ -31,7 +36,10 @@ export const canShowActiveSubItems = (el) => { return true; }; -export const canShowSubItems = () => bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md' || bp.getBreakpointSize() === 'lg'; +export const canShowSubItems = () => + bp.getBreakpointSize() === 'sm' || + bp.getBreakpointSize() === 'md' || + bp.getBreakpointSize() === 'lg'; export const getHideSubItemsInterval = () => { if (!currentOpenMenu || !mousePos.length) return 0; @@ -41,11 +49,12 @@ export const getHideSubItemsInterval = () => { const currentMousePosY = currentMousePos.y; const [menuTop, menuBottom] = menuCornerLocs; - if (currentMousePosY < menuTop.y || - currentMousePosY > menuBottom.y) return 0; + if (currentMousePosY < menuTop.y || currentMousePosY > menuBottom.y) return 0; - if (slope(prevMousePos, menuBottom) < slope(currentMousePos, menuBottom) && - slope(prevMousePos, menuTop) > slope(currentMousePos, menuTop)) { + if ( + slope(prevMousePos, menuBottom) < slope(currentMousePos, menuBottom) && + slope(prevMousePos, menuTop) > slope(currentMousePos, menuTop) + ) { return HIDE_INTERVAL_TIMEOUT; } @@ -56,11 +65,12 @@ export const calculateTop = (boundingRect, outerHeight) => { const windowHeight = window.innerHeight; const bottomOverflow = windowHeight - (boundingRect.top + outerHeight); - return bottomOverflow < 0 ? (boundingRect.top - outerHeight) + boundingRect.height : - boundingRect.top; + return bottomOverflow < 0 + ? boundingRect.top - outerHeight + boundingRect.height + : boundingRect.top; }; -export const hideMenu = (el) => { +export const hideMenu = el => { if (!el) return; const parentEl = el.parentNode; @@ -101,7 +111,7 @@ export const moveSubItemsToPosition = (el, subItems) => { } }; -export const showSubLevelItems = (el) => { +export const showSubLevelItems = el => { const subItems = el.querySelector('.sidebar-sub-level-items'); const isIconOnly = subItems && subItems.classList.contains('is-fly-out-only'); @@ -128,16 +138,20 @@ export const mouseEnterTopItems = (el, timeout = getHideSubItemsInterval()) => { }, timeout); }; -export const mouseLeaveTopItem = (el) => { +export const mouseLeaveTopItem = el => { const subItems = el.querySelector('.sidebar-sub-level-items'); - if (!canShowSubItems() || !canShowActiveSubItems(el) || - (subItems && subItems === currentOpenMenu)) return; + if ( + !canShowSubItems() || + !canShowActiveSubItems(el) || + (subItems && subItems === currentOpenMenu) + ) + return; el.classList.remove(IS_OVER_CLASS); }; -export const documentMouseMove = (e) => { +export const documentMouseMove = e => { mousePos.push({ x: e.clientX, y: e.clientY, @@ -146,7 +160,7 @@ export const documentMouseMove = (e) => { if (mousePos.length > 6) mousePos.shift(); }; -export const subItemsMouseLeave = (relatedTarget) => { +export const subItemsMouseLeave = relatedTarget => { clearTimeout(timeoutId); if (relatedTarget && !relatedTarget.closest(`.${IS_OVER_CLASS}`)) { @@ -174,7 +188,7 @@ export default () => { headerHeight = document.querySelector('.nav-sidebar').offsetTop; - items.forEach((el) => { + items.forEach(el => { const subItems = el.querySelector('.sidebar-sub-level-items'); if (subItems) { diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 95636a9ccdd..00b3d283570 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -94,7 +94,7 @@ class GfmAutoComplete { ...this.getDefaultCallbacks(), beforeSave(commands) { if (GfmAutoComplete.isLoading(commands)) return commands; - return $.map(commands, (c) => { + return $.map(commands, c => { let search = c.name; if (c.aliases.length > 0) { search = `${search} ${c.aliases.join(' ')}`; @@ -167,7 +167,7 @@ class GfmAutoComplete { callbacks: { ...this.getDefaultCallbacks(), beforeSave(members) { - return $.map(members, (m) => { + return $.map(members, m => { let title = ''; if (m.username == null) { return m; @@ -178,7 +178,9 @@ class GfmAutoComplete { } const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase(); - const imgAvatar = `<img src="${m.avatar_url}" alt="${m.username}" class="avatar avatar-inline center s26"/>`; + const imgAvatar = `<img src="${m.avatar_url}" alt="${ + m.username + }" class="avatar avatar-inline center s26"/>`; const txtAvatar = `<div class="avatar center avatar-inline s26">${autoCompleteAvatar}</div>`; return { @@ -201,7 +203,7 @@ class GfmAutoComplete { displayTpl(value) { let tmpl = GfmAutoComplete.Loading.template; if (value.title != null) { - tmpl = GfmAutoComplete.Issues.template; + tmpl = GfmAutoComplete.Issues.templateFunction(value.id, value.title); } return tmpl; }, @@ -211,7 +213,7 @@ class GfmAutoComplete { callbacks: { ...this.getDefaultCallbacks(), beforeSave(issues) { - return $.map(issues, (i) => { + return $.map(issues, i => { if (i.title == null) { return i; } @@ -244,7 +246,7 @@ class GfmAutoComplete { callbacks: { ...this.getDefaultCallbacks(), beforeSave(milestones) { - return $.map(milestones, (m) => { + return $.map(milestones, m => { if (m.title == null) { return m; } @@ -267,7 +269,7 @@ class GfmAutoComplete { displayTpl(value) { let tmpl = GfmAutoComplete.Loading.template; if (value.title != null) { - tmpl = GfmAutoComplete.Issues.template; + tmpl = GfmAutoComplete.Issues.templateFunction(value.id, value.title); } return tmpl; }, @@ -277,7 +279,7 @@ class GfmAutoComplete { callbacks: { ...this.getDefaultCallbacks(), beforeSave(merges) { - return $.map(merges, (m) => { + return $.map(merges, m => { if (m.title == null) { return m; } @@ -324,13 +326,20 @@ class GfmAutoComplete { }, matcher(flag, subtext) { const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers); - const subtextNodes = subtext.split(/\n+/g).pop().split(GfmAutoComplete.regexSubtext); + const subtextNodes = subtext + .split(/\n+/g) + .pop() + .split(GfmAutoComplete.regexSubtext); // Check if ~ is followed by '/label', '/relabel' or '/unlabel' commands. - command = subtextNodes.find((node) => { - if (node === LABEL_COMMAND.LABEL || - node === LABEL_COMMAND.RELABEL || - node === LABEL_COMMAND.UNLABEL) { return node; } + command = subtextNodes.find(node => { + if ( + node === LABEL_COMMAND.LABEL || + node === LABEL_COMMAND.RELABEL || + node === LABEL_COMMAND.UNLABEL + ) { + return node; + } return null; }); @@ -370,7 +379,7 @@ class GfmAutoComplete { displayTpl(value) { let tmpl = GfmAutoComplete.Loading.template; if (value.title != null) { - tmpl = GfmAutoComplete.Issues.template; + tmpl = GfmAutoComplete.Issues.templateFunction(value.id, value.title); } return tmpl; }, @@ -380,7 +389,7 @@ class GfmAutoComplete { callbacks: { ...this.getDefaultCallbacks(), beforeSave(snippets) { - return $.map(snippets, (m) => { + return $.map(snippets, m => { if (m.title == null) { return m; } @@ -458,13 +467,17 @@ class GfmAutoComplete { this.loadData($input, at, validEmojiNames); GfmAutoComplete.glEmojiTag = glEmojiTag; }) - .catch(() => { this.isLoadingData[at] = false; }); + .catch(() => { + this.isLoadingData[at] = false; + }); } else if (dataSource) { AjaxCache.retrieve(dataSource, true) - .then((data) => { + .then(data => { this.loadData($input, at, data); }) - .catch(() => { this.isLoadingData[at] = false; }); + .catch(() => { + this.isLoadingData[at] = false; + }); } else { this.isLoadingData[at] = false; } @@ -497,15 +510,16 @@ class GfmAutoComplete { } const loadingState = GfmAutoComplete.defaultLoadingData[0]; - return dataToInspect && - (dataToInspect === loadingState || dataToInspect.name === loadingState); + return dataToInspect && (dataToInspect === loadingState || dataToInspect.name === loadingState); } static defaultMatcher(flag, subtext, controllers) { // The below is taken from At.js source // Tweaked to commands to start without a space only if char before is a non-word character // https://github.com/ichord/At.js - const atSymbolsWithBar = Object.keys(controllers).join('|').replace(/[$]/, '\\$&'); + const atSymbolsWithBar = Object.keys(controllers) + .join('|') + .replace(/[$]/, '\\$&'); const atSymbolsWithoutBar = Object.keys(controllers).join(''); const targetSubtext = subtext.split(GfmAutoComplete.regexSubtext).pop(); const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'); @@ -513,7 +527,10 @@ class GfmAutoComplete { const accentAChar = decodeURI('%C3%80'); const accentYChar = decodeURI('%C3%BF'); - const regexp = new RegExp(`^(?:\\B|[^a-zA-Z0-9_\`${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, 'gi'); + const regexp = new RegExp( + `^(?:\\B|[^a-zA-Z0-9_\`${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, + 'gi', + ); return regexp.exec(targetSubtext); } @@ -552,13 +569,15 @@ GfmAutoComplete.Members = { template: '<li>${avatarTag} ${username} <small>${title}</small></li>', }; GfmAutoComplete.Labels = { - // eslint-disable-next-line no-template-curly-in-string - template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>', + template: + // eslint-disable-next-line no-template-curly-in-string + '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>', }; // Issues, MergeRequests and Snippets GfmAutoComplete.Issues = { - // eslint-disable-next-line no-template-curly-in-string - template: '<li><small>${id}</small> ${title}</li>', + templateFunction(id, title) { + return `<li><small>${id}</small> ${_.escape(title)}</li>`; + }, }; // Milestones GfmAutoComplete.Milestones = { @@ -566,7 +585,8 @@ GfmAutoComplete.Milestones = { template: '<li>${title}</li>', }; GfmAutoComplete.Loading = { - template: '<li style="pointer-events: none;"><i class="fa fa-spinner fa-spin"></i> Loading...</li>', + template: + '<li style="pointer-events: none;"><i class="fa fa-spinner fa-spin"></i> Loading...</li>', }; export default GfmAutoComplete; diff --git a/app/assets/javascripts/gl_field_error.js b/app/assets/javascripts/gl_field_error.js index 87c6e37b9fb..a5b8c357e8a 100644 --- a/app/assets/javascripts/gl_field_error.js +++ b/app/assets/javascripts/gl_field_error.js @@ -116,7 +116,8 @@ export default class GlFieldError { this.form.focusOnFirstInvalid.apply(this.form); // For UX, wait til after first invalid submission to check each keyup - this.inputElement.off('keyup.fieldValidator') + this.inputElement + .off('keyup.fieldValidator') .on('keyup.fieldValidator', this.updateValidity.bind(this)); } diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js index b9c51045b1d..d5d5954ce6a 100644 --- a/app/assets/javascripts/gl_field_errors.js +++ b/app/assets/javascripts/gl_field_errors.js @@ -16,16 +16,19 @@ export default class GlFieldErrors { initValidators() { // register selectors here as needed const validateSelectors = [':text', ':password', '[type=email]'] - .map(selector => `input${selector}`).join(','); + .map(selector => `input${selector}`) + .join(','); - this.state.inputs = this.form.find(validateSelectors).toArray() + this.state.inputs = this.form + .find(validateSelectors) + .toArray() .filter(input => !input.classList.contains(customValidationFlag)) .map(input => new GlFieldError({ input, formErrors: this })); this.form.on('submit', GlFieldErrors.catchInvalidFormSubmit); } - /* Neccessary to prevent intercept and override invalid form submit + /* Necessary to prevent intercept and override invalid form submit * because Safari & iOS quietly allow form submission when form is invalid * and prevents disabling of invalid submit button by application.js */ @@ -42,7 +45,7 @@ export default class GlFieldErrors { /* Public method for triggering validity updates manually */ updateFormValidityState() { - this.state.inputs.forEach((field) => { + this.state.inputs.forEach(field => { if (field.state.submitted) { field.updateValidity(); } @@ -50,8 +53,9 @@ export default class GlFieldErrors { } focusOnFirstInvalid() { - const firstInvalid = this.state.inputs - .filter(input => !input.inputDomElement.validity.valid)[0]; + const firstInvalid = this.state.inputs.filter( + input => !input.inputDomElement.validity.valid, + )[0]; firstInvalid.inputElement.focus(); } } diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index e672284a2d0..f842d2d74db 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -39,7 +39,10 @@ export default class GLForm { this.form.find('.div-dropzone').remove(); this.form.addClass('gfm-form'); // 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')); + 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'), this.enableGFM); dropzoneInput(this.form); @@ -55,11 +58,9 @@ export default class GLForm { } setupAutosize() { - this.textarea.off('autosize:resized') - .on('autosize:resized', this.setHeightData.bind(this)); + this.textarea.off('autosize:resized').on('autosize:resized', this.setHeightData.bind(this)); - this.textarea.off('mouseup.autosize') - .on('mouseup.autosize', this.destroyAutosize.bind(this)); + this.textarea.off('mouseup.autosize').on('mouseup.autosize', this.destroyAutosize.bind(this)); setTimeout(() => { autosize(this.textarea); @@ -91,10 +92,14 @@ export default class GLForm { addEventListeners() { this.textarea.on('focus', function focusTextArea() { - $(this).closest('.md-area').addClass('is-focused'); + $(this) + .closest('.md-area') + .addClass('is-focused'); }); this.textarea.on('blur', function blurTextArea() { - $(this).closest('.md-area').removeClass('is-focused'); + $(this) + .closest('.md-area') + .removeClass('is-focused'); }); } } diff --git a/app/assets/javascripts/group.js b/app/assets/javascripts/group.js index 4365305c168..903c838e266 100644 --- a/app/assets/javascripts/group.js +++ b/app/assets/javascripts/group.js @@ -1,4 +1,5 @@ import $ from 'jquery'; +import { slugifyWithHyphens } from './lib/utils/text_utility'; export default class Group { constructor() { @@ -7,17 +8,18 @@ export default class Group { this.updateHandler = this.update.bind(this); this.resetHandler = this.reset.bind(this); if (this.groupName.val() === '') { - this.groupPath.on('keyup', this.updateHandler); - this.groupName.on('keydown', this.resetHandler); + this.groupName.on('keyup', this.updateHandler); + this.groupPath.on('keydown', this.resetHandler); } } update() { - this.groupName.val(this.groupPath.val()); + const slug = slugifyWithHyphens(this.groupName.val()); + this.groupPath.val(slug); } reset() { - this.groupPath.off('keyup', this.updateHandler); - this.groupName.off('keydown', this.resetHandler); + this.groupName.off('keyup', this.updateHandler); + this.groupPath.off('keydown', this.resetHandler); } } diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js index beaac61e887..dcda625f587 100644 --- a/app/assets/javascripts/group_avatar.js +++ b/app/assets/javascripts/group_avatar.js @@ -7,8 +7,9 @@ export default function groupAvatar() { }); $('.js-group-avatar-input').on('change', function onChangeAvatarInput() { const form = $(this).closest('form'); - // eslint-disable-next-line no-useless-escape - const filename = $(this).val().replace(/^.*[\\\/]/, ''); + const filename = $(this) + .val() + .replace(/^.*[\\\/]/, ''); // eslint-disable-line no-useless-escape return form.find('.js-avatar-filename').text(filename); }); } diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js index d33e3a37580..9b74560f914 100644 --- a/app/assets/javascripts/group_label_subscription.js +++ b/app/assets/javascripts/group_label_subscription.js @@ -23,7 +23,8 @@ export default class GroupLabelSubscription { event.preventDefault(); const url = this.$unsubscribeButtons.attr('data-url'); - axios.post(url) + axios + .post(url) .then(() => { this.toggleSubscriptionButtons(); this.$unsubscribeButtons.removeAttr('data-url'); @@ -39,7 +40,8 @@ export default class GroupLabelSubscription { this.$unsubscribeButtons.attr('data-url', url); - axios.post(url) + axios + .post(url) .then(() => GroupLabelSubscription.setNewTooltip($btn)) .then(() => this.toggleSubscriptionButtons()) .catch(() => flash(__('There was an error when subscribing to this label.'))); @@ -58,6 +60,8 @@ export default class GroupLabelSubscription { const newTitle = tooltipTitles[type]; $('.js-unsubscribe-button', $button.closest('.label-actions-list')) - .tooltip('hide').attr('title', newTitle).tooltip('_fixTitle'); + .tooltip('hide') + .attr('title', newTitle) + .tooltip('_fixTitle'); } } diff --git a/app/assets/javascripts/groups/components/item_stats.vue b/app/assets/javascripts/groups/components/item_stats.vue index 87ab5480c15..829924ba63c 100644 --- a/app/assets/javascripts/groups/components/item_stats.vue +++ b/app/assets/javascripts/groups/components/item_stats.vue @@ -1,44 +1,44 @@ <script> - import icon from '~/vue_shared/components/icon.vue'; - import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; - import { - ITEM_TYPE, - VISIBILITY_TYPE_ICON, - GROUP_VISIBILITY_TYPE, - PROJECT_VISIBILITY_TYPE, - } from '../constants'; - import itemStatsValue from './item_stats_value.vue'; +import icon from '~/vue_shared/components/icon.vue'; +import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import { + ITEM_TYPE, + VISIBILITY_TYPE_ICON, + GROUP_VISIBILITY_TYPE, + PROJECT_VISIBILITY_TYPE, +} from '../constants'; +import itemStatsValue from './item_stats_value.vue'; - export default { - components: { - icon, - timeAgoTooltip, - itemStatsValue, +export default { + components: { + icon, + timeAgoTooltip, + itemStatsValue, + }, + props: { + item: { + type: Object, + required: true, }, - props: { - item: { - type: Object, - required: true, - }, + }, + computed: { + visibilityIcon() { + return VISIBILITY_TYPE_ICON[this.item.visibility]; }, - computed: { - visibilityIcon() { - return VISIBILITY_TYPE_ICON[this.item.visibility]; - }, - visibilityTooltip() { - if (this.item.type === ITEM_TYPE.GROUP) { - return GROUP_VISIBILITY_TYPE[this.item.visibility]; - } - return PROJECT_VISIBILITY_TYPE[this.item.visibility]; - }, - isProject() { - return this.item.type === ITEM_TYPE.PROJECT; - }, - isGroup() { - return this.item.type === ITEM_TYPE.GROUP; - }, + visibilityTooltip() { + if (this.item.type === ITEM_TYPE.GROUP) { + return GROUP_VISIBILITY_TYPE[this.item.visibility]; + } + return PROJECT_VISIBILITY_TYPE[this.item.visibility]; }, - }; + isProject() { + return this.item.type === ITEM_TYPE.PROJECT; + }, + isGroup() { + return this.item.type === ITEM_TYPE.GROUP; + }, + }, +}; </script> <template> diff --git a/app/assets/javascripts/groups/components/item_stats_value.vue b/app/assets/javascripts/groups/components/item_stats_value.vue index ef9f2bca76c..c542ca946d3 100644 --- a/app/assets/javascripts/groups/components/item_stats_value.vue +++ b/app/assets/javascripts/groups/components/item_stats_value.vue @@ -1,52 +1,52 @@ <script> - import tooltip from '~/vue_shared/directives/tooltip'; - import icon from '~/vue_shared/components/icon.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; +import icon from '~/vue_shared/components/icon.vue'; - export default { - components: { - icon, +export default { + components: { + icon, + }, + directives: { + tooltip, + }, + props: { + title: { + type: String, + required: false, + default: '', }, - directives: { - tooltip, + cssClass: { + type: String, + required: false, + default: '', }, - props: { - title: { - type: String, - required: false, - default: '', - }, - cssClass: { - type: String, - required: false, - default: '', - }, - iconName: { - type: String, - required: true, - }, - tooltipPlacement: { - type: String, - required: false, - default: 'bottom', - }, - /** - * value could either be number or string - * as `memberCount` is always passed as string - * while `subgroupCount` & `projectCount` - * are always number - */ - value: { - type: [Number, String], - required: false, - default: '', - }, + iconName: { + type: String, + required: true, }, - computed: { - isValuePresent() { - return this.value !== ''; - }, + tooltipPlacement: { + type: String, + required: false, + default: 'bottom', }, - }; + /** + * value could either be number or string + * as `memberCount` is always passed as string + * while `subgroupCount` & `projectCount` + * are always number + */ + value: { + type: [Number, String], + required: false, + default: '', + }, + }, + computed: { + isValuePresent() { + return this.value !== ''; + }, + }, +}; </script> <template> diff --git a/app/assets/javascripts/groups/new_group_child.js b/app/assets/javascripts/groups/new_group_child.js index a120d501e35..012177479c6 100644 --- a/app/assets/javascripts/groups/new_group_child.js +++ b/app/assets/javascripts/groups/new_group_child.js @@ -37,20 +37,22 @@ export default class NewGroupChild { getDroplabConfig() { return { - InputSetter: [{ - input: this.newGroupChildButton, - valueAttribute: 'data-value', - inputAttribute: 'data-action', - }, { - input: this.newGroupChildButton, - valueAttribute: 'data-text', - }], + InputSetter: [ + { + input: this.newGroupChildButton, + valueAttribute: 'data-value', + inputAttribute: 'data-action', + }, + { + input: this.newGroupChildButton, + valueAttribute: 'data-text', + }, + ], }; } bindEvents() { - this.newGroupChildButton - .addEventListener('click', this.onClickNewGroupChildButton.bind(this)); + this.newGroupChildButton.addEventListener('click', this.onClickNewGroupChildButton.bind(this)); } onClickNewGroupChildButton(e) { diff --git a/app/assets/javascripts/groups/store/groups_store.js b/app/assets/javascripts/groups/store/groups_store.js index 4a7569078a1..16f95d5a0cc 100644 --- a/app/assets/javascripts/groups/store/groups_store.js +++ b/app/assets/javascripts/groups/store/groups_store.js @@ -17,13 +17,14 @@ export default class GroupsStore { } setSearchedGroups(rawGroups) { - const formatGroups = groups => groups.map((group) => { - const formattedGroup = this.formatGroupItem(group); - if (formattedGroup.children && formattedGroup.children.length) { - formattedGroup.children = formatGroups(formattedGroup.children); - } - return formattedGroup; - }); + const formatGroups = groups => + groups.map(group => { + const formattedGroup = this.formatGroupItem(group); + if (formattedGroup.children && formattedGroup.children.length) { + formattedGroup.children = formatGroups(formattedGroup.children); + } + return formattedGroup; + }); if (rawGroups && rawGroups.length) { this.state.groups = formatGroups(rawGroups); @@ -62,10 +63,10 @@ export default class GroupsStore { formatGroupItem(rawGroupItem) { const groupChildren = rawGroupItem.children || []; - const groupIsOpen = (groupChildren.length > 0) || false; - const childrenCount = this.hideProjects ? - rawGroupItem.subgroup_count : - rawGroupItem.children_count; + const groupIsOpen = groupChildren.length > 0 || false; + const childrenCount = this.hideProjects + ? rawGroupItem.subgroup_count + : rawGroupItem.children_count; return { id: rawGroupItem.id, diff --git a/app/assets/javascripts/groups/transfer_dropdown.js b/app/assets/javascripts/groups/transfer_dropdown.js index e0eb118ddf7..26510fcdb2a 100644 --- a/app/assets/javascripts/groups/transfer_dropdown.js +++ b/app/assets/javascripts/groups/transfer_dropdown.js @@ -22,7 +22,7 @@ export default class TransferDropdown { search: { fields: ['text'] }, data: extraOptions.concat(this.data), text: item => item.text, - clicked: (options) => { + clicked: options => { const { e } = options; e.preventDefault(); this.assignSelected(options.selectedObj); diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index e37fc5c4be6..b4a3037c1b7 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -23,7 +23,7 @@ export default function groupsSelect() { axios[params.type.toLowerCase()](params.url, { params: params.data, }) - .then((res) => { + .then(res => { const results = res.data || []; const headers = normalizeHeaders(res.headers); const currentPage = parseInt(headers['X-PAGE'], 10) || 0; @@ -36,7 +36,8 @@ export default function groupsSelect() { more, }, }); - }).catch(params.error); + }) + .catch(params.error); }, data(search, page) { return { @@ -68,7 +69,9 @@ export default function groupsSelect() { } }, formatResult(object) { - return `<div class='group-result'> <div class='group-name'>${object.full_name}</div> <div class='group-path'>${object.full_path}</div> </div>`; + return `<div class='group-result'> <div class='group-name'>${ + object.full_name + }</div> <div class='group-path'>${object.full_path}</div> </div>`; }, formatSelection(object) { return object.full_name; diff --git a/app/assets/javascripts/helpers/avatar_helper.js b/app/assets/javascripts/helpers/avatar_helper.js index d3b1d0f11fd..35ac7b2629c 100644 --- a/app/assets/javascripts/helpers/avatar_helper.js +++ b/app/assets/javascripts/helpers/avatar_helper.js @@ -19,7 +19,9 @@ export function renderIdenticon(entity, options = {}) { const bgClass = getIdenticonBackgroundClass(entity.id); const title = getIdenticonTitle(entity.name); - return `<div class="avatar identicon ${_.escape(sizeClass)} ${_.escape(bgClass)}">${_.escape(title)}</div>`; + return `<div class="avatar identicon ${_.escape(sizeClass)} ${_.escape(bgClass)}">${_.escape( + title, + )}</div>`; } export function renderAvatar(entity, options = {}) { diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index ad6151e3bf6..0a368f6558c 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -43,7 +43,7 @@ export default { 'currentProjectId', 'errorMessage', ]), - ...mapGetters(['activeFile', 'hasChanges', 'someUncommitedChanges', 'isCommitModeActive']), + ...mapGetters(['activeFile', 'hasChanges', 'someUncommittedChanges', 'isCommitModeActive']), }, mounted() { window.onbeforeunload = e => this.onBeforeUnload(e); @@ -63,7 +63,7 @@ export default { onBeforeUnload(e = {}) { const returnValue = __('Are you sure you want to lose unsaved changes?'); - if (!this.someUncommitedChanges) return undefined; + if (!this.someUncommittedChanges) return undefined; Object.assign(e, { returnValue, diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue index dc84ee12f1e..364ab9426e0 100644 --- a/app/assets/javascripts/ide/components/ide_side_bar.vue +++ b/app/assets/javascripts/ide/components/ide_side_bar.vue @@ -1,6 +1,6 @@ <script> import { mapState, mapGetters } from 'vuex'; -import { SkeletonLoading } from '@gitlab-org/gitlab-ui'; +import { GlSkeletonLoading } from '@gitlab-org/gitlab-ui'; import IdeTree from './ide_tree.vue'; import ResizablePanel from './resizable_panel.vue'; import ActivityBar from './activity_bar.vue'; @@ -13,7 +13,7 @@ import { activityBarViews } from '../constants'; export default { components: { - SkeletonLoading, + GlSkeletonLoading, ResizablePanel, ActivityBar, CommitSection, @@ -25,11 +25,11 @@ export default { }, computed: { ...mapState(['loading', 'currentActivityView', 'changedFiles', 'stagedFiles', 'lastCommitMsg']), - ...mapGetters(['currentProject', 'someUncommitedChanges']), + ...mapGetters(['currentProject', 'someUncommittedChanges']), showSuccessMessage() { return ( this.currentActivityView === activityBarViews.edit && - (this.lastCommitMsg && !this.someUncommitedChanges) + (this.lastCommitMsg && !this.someUncommittedChanges) ); }, }, @@ -50,7 +50,7 @@ export default { :key="n" class="multi-file-loading-container" > - <skeleton-loading /> + <gl-skeleton-loading /> </div> </div> </template> diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue index e88f01fb4f4..d2ff55a4ee3 100644 --- a/app/assets/javascripts/ide/components/ide_tree_list.vue +++ b/app/assets/javascripts/ide/components/ide_tree_list.vue @@ -1,7 +1,7 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; import Icon from '~/vue_shared/components/icon.vue'; -import { SkeletonLoading } from '@gitlab-org/gitlab-ui'; +import { GlSkeletonLoading } from '@gitlab-org/gitlab-ui'; import FileRow from '~/vue_shared/components/file_row.vue'; import NavDropdown from './nav_dropdown.vue'; import FileRowExtra from './file_row_extra.vue'; @@ -9,7 +9,7 @@ import FileRowExtra from './file_row_extra.vue'; export default { components: { Icon, - SkeletonLoading, + GlSkeletonLoading, NavDropdown, FileRow, }, @@ -51,7 +51,7 @@ export default { :key="n" class="multi-file-loading-container" > - <skeleton-loading /> + <gl-skeleton-loading /> </div> </template> <template v-else> diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue index 0a2681b7a1e..b670b0355b7 100644 --- a/app/assets/javascripts/ide/components/pipelines/list.vue +++ b/app/assets/javascripts/ide/components/pipelines/list.vue @@ -25,7 +25,7 @@ export default { ...mapState('pipelines', ['isLoadingPipeline', 'latestPipeline', 'stages', 'isLoadingJobs']), ciLintText() { return sprintf( - __('You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}'), + __('You can test your .gitlab-ci.yml in %{linkStart}CI Lint%{linkEnd}.'), { linkStart: `<a href="${_.escape(this.currentProject.web_url)}/-/ci/lint">`, linkEnd: '</a>', diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue index d3b24c5b793..5e86876c1c1 100644 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -27,10 +27,10 @@ export default { 'unusedSeal', ]), ...mapState('commit', ['commitMessage', 'submitCommitLoading']), - ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges', 'activeFile']), + ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommittedChanges', 'activeFile']), ...mapGetters('commit', ['discardDraftButtonDisabled']), showStageUnstageArea() { - return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal); + return !!(this.someUncommittedChanges || this.lastCommitMsg || !this.unusedSeal); }, activeFileKey() { return this.activeFile ? this.activeFile.key : null; diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js index 709748fb530..8ad85074d6b 100644 --- a/app/assets/javascripts/ide/stores/getters.js +++ b/app/assets/javascripts/ide/stores/getters.js @@ -63,7 +63,7 @@ export const isEditModeActive = state => state.currentActivityView === activityB export const isCommitModeActive = state => state.currentActivityView === activityBarViews.commit; export const isReviewModeActive = state => state.currentActivityView === activityBarViews.review; -export const someUncommitedChanges = state => +export const someUncommittedChanges = state => !!(state.changedFiles.length || state.stagedFiles.length); export const getChangesInFolder = state => path => { diff --git a/app/assets/javascripts/image_diff/image_diff.js b/app/assets/javascripts/image_diff/image_diff.js index fab0255c378..3587f073a00 100644 --- a/app/assets/javascripts/image_diff/image_diff.js +++ b/app/assets/javascripts/image_diff/image_diff.js @@ -60,8 +60,10 @@ export default class ImageDiff { } renderBadge(discussionEl, index) { - const imageBadge = imageDiffHelper - .generateBadgeFromDiscussionDOM(this.imageFrameEl, discussionEl); + const imageBadge = imageDiffHelper.generateBadgeFromDiscussionDOM( + this.imageFrameEl, + discussionEl, + ); this.imageBadges.push(imageBadge); diff --git a/app/assets/javascripts/image_diff/init_discussion_tab.js b/app/assets/javascripts/image_diff/init_discussion_tab.js index 2f16c6ef115..dbe4c06a4e9 100644 --- a/app/assets/javascripts/image_diff/init_discussion_tab.js +++ b/app/assets/javascripts/image_diff/init_discussion_tab.js @@ -8,5 +8,6 @@ export default () => { const diffFileEls = document.querySelectorAll('.timeline-content .diff-file.js-image-file'); [...diffFileEls].forEach(diffFileEl => - imageDiffHelper.initImageDiff(diffFileEl, canCreateNote, renderCommentBadge)); + imageDiffHelper.initImageDiff(diffFileEl, canCreateNote, renderCommentBadge), + ); }; diff --git a/app/assets/javascripts/image_diff/replaced_image_diff.js b/app/assets/javascripts/image_diff/replaced_image_diff.js index 4abd13fb472..8d9e65155d8 100644 --- a/app/assets/javascripts/image_diff/replaced_image_diff.js +++ b/app/assets/javascripts/image_diff/replaced_image_diff.js @@ -26,7 +26,7 @@ export default class ReplacedImageDiff extends ImageDiff { this.imageEls = {}; const viewTypeNames = Object.getOwnPropertyNames(viewTypes); - viewTypeNames.forEach((viewType) => { + viewTypeNames.forEach(viewType => { this.imageEls[viewType] = this.imageFrameEls[viewType].querySelector('img'); }); } @@ -79,13 +79,12 @@ export default class ReplacedImageDiff extends ImageDiff { // Re-render indicator in new view if (indicator.removed) { - const normalizedIndicator = imageDiffHelper - .resizeCoordinatesToImageElement(this.imageEl, { - x: indicator.x, - y: indicator.y, - width: indicator.image.width, - height: indicator.image.height, - }); + const normalizedIndicator = imageDiffHelper.resizeCoordinatesToImageElement(this.imageEl, { + x: indicator.x, + y: indicator.y, + width: indicator.image.width, + height: indicator.image.height, + }); imageDiffHelper.showCommentIndicator(this.imageFrameEl, normalizedIndicator); } } diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js index eda8cdad908..f1beb1a8ea5 100644 --- a/app/assets/javascripts/importer_status.js +++ b/app/assets/javascripts/importer_status.js @@ -60,66 +60,71 @@ class ImporterStatus { attributes = Object.assign(repoData, attributes); } - return axios.post(this.importUrl, attributes) - .then(({ data }) => { - const job = $(`tr#repo_${id}`); - job.attr('id', `project_${data.id}`); - - job.find('.import-target').html(`<a href="${data.full_path}">${data.full_path}</a>`); - $('table.import-jobs tbody').prepend(job); - - job.addClass('table-active'); - const connectingVerb = this.ciCdOnly ? __('connecting') : __('importing'); - job.find('.import-actions').html(sprintf( - _.escape(__('%{loadingIcon} Started')), { - loadingIcon: `<i class="fa fa-spinner fa-spin" aria-label="${_.escape(connectingVerb)}"></i>`, - }, - false, - )); - }) - .catch((error) => { - let details = error; - - const $statusField = $(`#repo_${this.id} .job-status`); - $statusField.text(__('Failed')); - - if (error.response && error.response.data && error.response.data.errors) { - details = error.response.data.errors; - } - - flash(sprintf(__('An error occurred while importing project: %{details}'), { details })); - }); + return axios + .post(this.importUrl, attributes) + .then(({ data }) => { + const job = $(`tr#repo_${id}`); + job.attr('id', `project_${data.id}`); + + job.find('.import-target').html(`<a href="${data.full_path}">${data.full_path}</a>`); + $('table.import-jobs tbody').prepend(job); + + job.addClass('table-active'); + const connectingVerb = this.ciCdOnly ? __('connecting') : __('importing'); + job.find('.import-actions').html( + sprintf( + _.escape(__('%{loadingIcon} Started')), + { + loadingIcon: `<i class="fa fa-spinner fa-spin" aria-label="${_.escape( + connectingVerb, + )}"></i>`, + }, + false, + ), + ); + }) + .catch(error => { + let details = error; + + const $statusField = $(`#repo_${this.id} .job-status`); + $statusField.text(__('Failed')); + + if (error.response && error.response.data && error.response.data.errors) { + details = error.response.data.errors; + } + + flash(sprintf(__('An error occurred while importing project: %{details}'), { details })); + }); } autoUpdate() { - return axios.get(this.jobsUrl) - .then(({ data = [] }) => { - data.forEach((job) => { - const jobItem = $(`#project_${job.id}`); - const statusField = jobItem.find('.job-status'); - - const spinner = '<i class="fa fa-spinner fa-spin"></i>'; - - switch (job.import_status) { - case 'finished': - jobItem.removeClass('table-active').addClass('table-success'); - statusField.html(`<span><i class="fa fa-check"></i> ${__('Done')}</span>`); - break; - case 'scheduled': - statusField.html(`${spinner} ${__('Scheduled')}`); - break; - case 'started': - statusField.html(`${spinner} ${__('Started')}`); - break; - case 'failed': - statusField.html(__('Failed')); - break; - default: - statusField.html(job.import_status); - break; - } - }); + return axios.get(this.jobsUrl).then(({ data = [] }) => { + data.forEach(job => { + const jobItem = $(`#project_${job.id}`); + const statusField = jobItem.find('.job-status'); + + const spinner = '<i class="fa fa-spinner fa-spin"></i>'; + + switch (job.import_status) { + case 'finished': + jobItem.removeClass('table-active').addClass('table-success'); + statusField.html(`<span><i class="fa fa-check"></i> ${__('Done')}</span>`); + break; + case 'scheduled': + statusField.html(`${spinner} ${__('Scheduled')}`); + break; + case 'started': + statusField.html(`${spinner} ${__('Started')}`); + break; + case 'failed': + statusField.html(__('Failed')); + break; + default: + statusField.html(job.import_status); + break; + } }); + }); } setAutoUpdate() { @@ -141,7 +146,4 @@ function initImporterStatus() { } } -export { - initImporterStatus as default, - ImporterStatus, -}; +export { initImporterStatus as default, ImporterStatus }; diff --git a/app/assets/javascripts/init_changes_dropdown.js b/app/assets/javascripts/init_changes_dropdown.js index 5c5a6e01848..e708e5d0978 100644 --- a/app/assets/javascripts/init_changes_dropdown.js +++ b/app/assets/javascripts/init_changes_dropdown.js @@ -1,7 +1,7 @@ import $ from 'jquery'; import { stickyMonitor } from './lib/utils/sticky'; -export default (stickyTop) => { +export default stickyTop => { stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop); $('.js-diff-stats-dropdown').glDropdown({ diff --git a/app/assets/javascripts/init_notes.js b/app/assets/javascripts/init_notes.js index 3c71258e53b..a77828e8cf2 100644 --- a/app/assets/javascripts/init_notes.js +++ b/app/assets/javascripts/init_notes.js @@ -2,13 +2,7 @@ import Notes from './notes'; export default () => { const dataEl = document.querySelector('.js-notes-data'); - const { - notesUrl, - notesIds, - now, - diffView, - enableGFM, - } = JSON.parse(dataEl.innerHTML); + const { notesUrl, notesIds, now, diffView, 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 diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js index bd90d0eaa32..08b858305ab 100644 --- a/app/assets/javascripts/integrations/integration_settings_form.js +++ b/app/assets/javascripts/integrations/integration_settings_form.js @@ -97,7 +97,8 @@ export default class IntegrationSettingsForm { testSettings(formData) { this.toggleSubmitBtnState(true); - return axios.put(this.testEndPoint, formData) + return axios + .put(this.testEndPoint, formData) .then(({ data }) => { if (data.error) { let flashActions; @@ -105,7 +106,7 @@ export default class IntegrationSettingsForm { if (data.test_failed) { flashActions = { title: 'Save anyway', - clickHandler: (e) => { + clickHandler: e => { e.preventDefault(); this.$form.submit(); }, diff --git a/app/assets/javascripts/issuable/auto_width_dropdown_select.js b/app/assets/javascripts/issuable/auto_width_dropdown_select.js index 07cf1eff279..612c524ca1c 100644 --- a/app/assets/javascripts/issuable/auto_width_dropdown_select.js +++ b/app/assets/javascripts/issuable/auto_width_dropdown_select.js @@ -27,7 +27,10 @@ class AutoWidthDropdownSelect { // We have to look at the parent because // `offsetParent` on a `display: none;` is `null` - const offsetParentWidth = $(this).parent().offsetParent().width(); + const offsetParentWidth = $(this) + .parent() + .offsetParent() + .width(); // Reset any width to let it naturally flow $dropdown.css('width', 'auto'); if ($dropdown.outerWidth(false) > offsetParentWidth) { diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js index 9848bcc2e64..b844e4c5e5b 100644 --- a/app/assets/javascripts/issuable_bulk_update_actions.js +++ b/app/assets/javascripts/issuable_bulk_update_actions.js @@ -32,7 +32,7 @@ export default { onFormSubmitFailure() { this.form.find('[type="submit"]').enable(); - return new Flash("Issue update failed"); + return new Flash('Issue update failed'); }, getSelectedIssues() { @@ -63,7 +63,7 @@ export default { const result = []; const labelsToKeep = this.$labelDropdown.data('indeterminate'); - this.getLabelsFromSelection().forEach((id) => { + this.getLabelsFromSelection().forEach(id => { if (labelsToKeep.indexOf(id) === -1) { result.push(id); } @@ -89,8 +89,8 @@ export default { issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(), subscription_event: this.form.find('input[name="update[subscription_event]"]').val(), add_label_ids: [], - remove_label_ids: [] - } + remove_label_ids: [], + }, }; if (this.willUpdateLabels) { formData.update.add_label_ids = this.$labelDropdown.data('marked'); @@ -134,7 +134,7 @@ export default { // Collect unique label IDs for all checked issues this.getElement('.selected-issuable:checked').each((i, el) => { issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'); - issuableLabels.forEach((labelId) => { + issuableLabels.forEach(labelId => { // Store unique IDs if (uniqueIds.indexOf(labelId) === -1) { uniqueIds.push(labelId); diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 0140960b367..c81a2230310 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -1,6 +1,3 @@ -/* eslint-disable no-new, no-unused-vars, consistent-return, no-else-return */ -/* global GitLab */ - import $ from 'jquery'; import Pikaday from 'pikaday'; import Autosave from './autosave'; @@ -8,7 +5,7 @@ import UsersSelect from './users_select'; import GfmAutoComplete from './gfm_auto_complete'; import ZenMode from './zen_mode'; import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select'; -import { parsePikadayDate, pikadayToString } from './lib/utils/datefix'; +import { parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility'; export default class IssuableForm { constructor(form) { @@ -19,9 +16,11 @@ export default class IssuableForm { this.handleSubmit = this.handleSubmit.bind(this); this.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i; - new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup(); - new UsersSelect(); - new ZenMode(); + this.gfmAutoComplete = new GfmAutoComplete( + gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources, + ).setup(); + this.usersSelect = new UsersSelect(); + this.zenMode = new ZenMode(); this.titleField = this.form.find('input[name*="[title]"]'); this.descriptionField = this.form.find('textarea[name*="[description]"]'); @@ -57,8 +56,16 @@ export default class IssuableForm { } initAutosave() { - new Autosave(this.titleField, [document.location.pathname, document.location.search, 'title']); - return new Autosave(this.descriptionField, [document.location.pathname, document.location.search, 'description']); + this.autosave = new Autosave(this.titleField, [ + document.location.pathname, + document.location.search, + 'title', + ]); + return new Autosave(this.descriptionField, [ + document.location.pathname, + document.location.search, + 'description', + ]); } handleSubmit() { @@ -74,7 +81,7 @@ export default class IssuableForm { this.$wipExplanation = this.form.find('.js-wip-explanation'); this.$noWipExplanation = this.form.find('.js-no-wip-explanation'); if (!(this.$wipExplanation.length && this.$noWipExplanation.length)) { - return; + return undefined; } this.form.on('click', '.js-toggle-wip', this.toggleWip); this.titleField.on('keyup blur', this.renderWipExplanation); @@ -89,10 +96,9 @@ export default class IssuableForm { if (this.workInProgress()) { this.$wipExplanation.show(); return this.$noWipExplanation.hide(); - } else { - this.$wipExplanation.hide(); - return this.$noWipExplanation.show(); } + this.$wipExplanation.hide(); + return this.$noWipExplanation.show(); } toggleWip(event) { @@ -110,7 +116,7 @@ export default class IssuableForm { } addWip() { - this.titleField.val(`WIP: ${(this.titleField.val())}`); + this.titleField.val(`WIP: ${this.titleField.val()}`); } initTargetBranchDropdown() { diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index ba14aaeed2c..3cabbfc6e27 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -1,164 +1,164 @@ <script> - import _ from 'underscore'; - import { mapGetters, mapState, mapActions } from 'vuex'; - import { isScrolledToBottom } from '~/lib/utils/scroll_utils'; - import bp from '~/breakpoints'; - import CiHeader from '~/vue_shared/components/header_ci_component.vue'; - import Callout from '~/vue_shared/components/callout.vue'; - import createStore from '../store'; - import EmptyState from './empty_state.vue'; - import EnvironmentsBlock from './environments_block.vue'; - import ErasedBlock from './erased_block.vue'; - import Log from './job_log.vue'; - import LogTopBar from './job_log_controllers.vue'; - import StuckBlock from './stuck_block.vue'; - import Sidebar from './sidebar.vue'; +import _ from 'underscore'; +import { mapGetters, mapState, mapActions } from 'vuex'; +import { isScrolledToBottom } from '~/lib/utils/scroll_utils'; +import bp from '~/breakpoints'; +import CiHeader from '~/vue_shared/components/header_ci_component.vue'; +import Callout from '~/vue_shared/components/callout.vue'; +import createStore from '../store'; +import EmptyState from './empty_state.vue'; +import EnvironmentsBlock from './environments_block.vue'; +import ErasedBlock from './erased_block.vue'; +import Log from './job_log.vue'; +import LogTopBar from './job_log_controllers.vue'; +import StuckBlock from './stuck_block.vue'; +import Sidebar from './sidebar.vue'; - export default { - name: 'JobPageApp', - store: createStore(), - components: { - CiHeader, - Callout, - EmptyState, - EnvironmentsBlock, - ErasedBlock, - Log, - LogTopBar, - StuckBlock, - Sidebar, +export default { + name: 'JobPageApp', + store: createStore(), + components: { + CiHeader, + Callout, + EmptyState, + EnvironmentsBlock, + ErasedBlock, + Log, + LogTopBar, + StuckBlock, + Sidebar, + }, + props: { + runnerSettingsUrl: { + type: String, + required: false, + default: null, }, - props: { - runnerSettingsUrl: { - type: String, - required: false, - default: null, - }, - runnerHelpUrl: { - type: String, - required: false, - default: null, - }, - endpoint: { - type: String, - required: true, - }, - terminalPath: { - type: String, - required: false, - default: null, - }, - pagePath: { - type: String, - required: true, - }, - logState: { - type: String, - required: true, - }, + runnerHelpUrl: { + type: String, + required: false, + default: null, }, - computed: { - ...mapState([ - 'isLoading', - 'job', - 'isSidebarOpen', - 'trace', - 'isTraceComplete', - 'traceSize', - 'isTraceSizeVisible', - 'isScrollBottomDisabled', - 'isScrollTopDisabled', - 'isScrolledToBottomBeforeReceivingTrace', - 'hasError', - ]), - ...mapGetters([ - 'headerActions', - 'headerTime', - 'shouldRenderCalloutMessage', - 'shouldRenderTriggeredLabel', - 'hasEnvironment', - 'isJobStuck', - 'hasTrace', - 'emptyStateIllustration', - 'isScrollingDown', - 'emptyStateAction', - ]), + endpoint: { + type: String, + required: true, + }, + terminalPath: { + type: String, + required: false, + default: null, + }, + pagePath: { + type: String, + required: true, + }, + logState: { + type: String, + required: true, + }, + }, + computed: { + ...mapState([ + 'isLoading', + 'job', + 'isSidebarOpen', + 'trace', + 'isTraceComplete', + 'traceSize', + 'isTraceSizeVisible', + 'isScrollBottomDisabled', + 'isScrollTopDisabled', + 'isScrolledToBottomBeforeReceivingTrace', + 'hasError', + ]), + ...mapGetters([ + 'headerActions', + 'headerTime', + 'shouldRenderCalloutMessage', + 'shouldRenderTriggeredLabel', + 'hasEnvironment', + 'hasTrace', + 'emptyStateIllustration', + 'isScrollingDown', + 'emptyStateAction', + 'hasRunnersForProject', + ]), - shouldRenderContent() { - return !this.isLoading && !this.hasError; - } + shouldRenderContent() { + return !this.isLoading && !this.hasError; }, - watch: { - // Once the job log is loaded, - // fetch the stages for the dropdown on the sidebar - job(newVal, oldVal) { - if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) { - this.fetchStages(); - } - }, + }, + watch: { + // Once the job log is loaded, + // fetch the stages for the dropdown on the sidebar + job(newVal, oldVal) { + if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) { + this.fetchStages(); + } }, - created() { - this.throttled = _.throttle(this.toggleScrollButtons, 100); + }, + created() { + this.throttled = _.throttle(this.toggleScrollButtons, 100); - this.setJobEndpoint(this.endpoint); - this.setTraceOptions({ - logState: this.logState, - pagePath: this.pagePath, - }); + this.setJobEndpoint(this.endpoint); + this.setTraceOptions({ + logState: this.logState, + pagePath: this.pagePath, + }); - this.fetchJob(); - this.fetchTrace(); + this.fetchJob(); + this.fetchTrace(); - window.addEventListener('resize', this.onResize); - window.addEventListener('scroll', this.updateScroll); - }, + window.addEventListener('resize', this.onResize); + window.addEventListener('scroll', this.updateScroll); + }, - mounted() { + mounted() { + this.updateSidebar(); + }, + + destroyed() { + window.removeEventListener('resize', this.onResize); + window.removeEventListener('scroll', this.updateScroll); + }, + + methods: { + ...mapActions([ + 'setJobEndpoint', + 'setTraceOptions', + 'fetchJob', + 'fetchStages', + 'hideSidebar', + 'showSidebar', + 'toggleSidebar', + 'fetchTrace', + 'scrollBottom', + 'scrollTop', + 'toggleScrollButtons', + 'toggleScrollAnimation', + ]), + onResize() { this.updateSidebar(); + this.updateScroll(); }, - - destroyed() { - window.removeEventListener('resize', this.onResize); - window.removeEventListener('scroll', this.updateScroll); + updateSidebar() { + if (bp.getBreakpointSize() === 'xs') { + this.hideSidebar(); + } else if (!this.isSidebarOpen) { + this.showSidebar(); + } }, + updateScroll() { + if (!isScrolledToBottom()) { + this.toggleScrollAnimation(false); + } else if (this.isScrollingDown) { + this.toggleScrollAnimation(true); + } - methods: { - ...mapActions([ - 'setJobEndpoint', - 'setTraceOptions', - 'fetchJob', - 'fetchStages', - 'hideSidebar', - 'showSidebar', - 'toggleSidebar', - 'fetchTrace', - 'scrollBottom', - 'scrollTop', - 'toggleScrollButtons', - 'toggleScrollAnimation', - ]), - onResize() { - this.updateSidebar(); - this.updateScroll(); - }, - updateSidebar() { - if (bp.getBreakpointSize() === 'xs') { - this.hideSidebar(); - } else if (!this.isSidebarOpen) { - this.showSidebar(); - } - }, - updateScroll() { - if (!isScrolledToBottom()) { - this.toggleScrollAnimation(false); - } else if (this.isScrollingDown) { - this.toggleScrollAnimation(true); - } - - this.throttled(); - }, + this.throttled(); }, - }; + }, +}; </script> <template> <div> @@ -195,9 +195,9 @@ <!-- Body Section --> <stuck-block - v-if="isJobStuck" + v-if="job.stuck" class="js-job-stuck" - :has-no-runners-for-project="job.runners.available" + :has-no-runners-for-project="hasRunnersForProject" :tags="job.tags" :runners-path="runnerSettingsUrl" /> diff --git a/app/assets/javascripts/jobs/components/job_log.vue b/app/assets/javascripts/jobs/components/job_log.vue index ffa6ada3e28..92e20e92d66 100644 --- a/app/assets/javascripts/jobs/components/job_log.vue +++ b/app/assets/javascripts/jobs/components/job_log.vue @@ -1,45 +1,45 @@ <script> - import { mapState, mapActions } from 'vuex'; +import { mapState, mapActions } from 'vuex'; - export default { - name: 'JobLog', - props: { - trace: { - type: String, - required: true, - }, - isComplete: { - type: Boolean, - required: true, - }, +export default { + name: 'JobLog', + props: { + trace: { + type: String, + required: true, }, - computed: { - ...mapState(['isScrolledToBottomBeforeReceivingTrace']), + isComplete: { + type: Boolean, + required: true, }, - updated() { - this.$nextTick(() => this.handleScrollDown()); + }, + computed: { + ...mapState(['isScrolledToBottomBeforeReceivingTrace']), + }, + updated() { + this.$nextTick(() => this.handleScrollDown()); + }, + mounted() { + this.$nextTick(() => this.handleScrollDown()); + }, + methods: { + ...mapActions(['scrollBottom']), + /** + * The job log is sent in HTML, which means we need to use `v-html` to render it + * Using the updated hook with $nextTick is not enough to wait for the DOM to be updated + * in this case because it runs before `v-html` has finished running, since there's no + * Vue binding. + * In order to scroll the page down after `v-html` has finished, we need to use setTimeout + */ + handleScrollDown() { + if (this.isScrolledToBottomBeforeReceivingTrace) { + setTimeout(() => { + this.scrollBottom(); + }, 0); + } }, - mounted() { - this.$nextTick(() => this.handleScrollDown()); - }, - methods: { - ...mapActions(['scrollBottom']), - /** - * The job log is sent in HTML, which means we need to use `v-html` to render it - * Using the updated hook with $nextTick is not enough to wait for the DOM to be updated - * in this case because it runs before `v-html` has finished running, since there's no - * Vue binding. - * In order to scroll the page down after `v-html` has finished, we need to use setTimeout - */ - handleScrollDown() { - if (this.isScrolledToBottomBeforeReceivingTrace) { - setTimeout(() => { - this.scrollBottom(); - }, 0); - } - }, - }, - }; + }, +}; </script> <template> <pre class="js-build-trace build-trace qa-build-trace"> diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue index 906769ee6a2..28a02230d89 100644 --- a/app/assets/javascripts/jobs/components/sidebar.vue +++ b/app/assets/javascripts/jobs/components/sidebar.vue @@ -31,7 +31,7 @@ export default { }, }, computed: { - ...mapState(['job', 'stages', 'jobs', 'selectedStage']), + ...mapState(['job', 'stages', 'jobs', 'selectedStage', 'isLoadingStages']), coverage() { return `${this.job.coverage}%`; }, @@ -59,10 +59,10 @@ export default { return ''; } - let t = this.job.metadata.timeout_human_readable; - if (this.job.metadata.timeout_source !== '') { - t += ` (from ${this.job.metadata.timeout_source})`; - } + let t = this.job.metadata.timeout_human_readable; + if (this.job.metadata.timeout_source !== '') { + t += ` (from ${this.job.metadata.timeout_source})`; + } return t; }, @@ -270,6 +270,7 @@ export default { /> <stages-dropdown + v-if="!isLoadingStages" :stages="stages" :pipeline="job.pipeline" :selected-stage="selectedStage" diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue index e5e1d56e287..dc26b246d71 100644 --- a/app/assets/javascripts/jobs/components/stages_dropdown.vue +++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue @@ -22,7 +22,6 @@ export default { required: true, }, }, - computed: { hasRef() { return !_.isEmpty(this.pipeline.ref); diff --git a/app/assets/javascripts/jobs/components/stuck_block.vue b/app/assets/javascripts/jobs/components/stuck_block.vue index a60643b2c65..1d5789b175a 100644 --- a/app/assets/javascripts/jobs/components/stuck_block.vue +++ b/app/assets/javascripts/jobs/components/stuck_block.vue @@ -23,14 +23,7 @@ export default { <template> <div class="bs-callout bs-callout-warning"> <p - v-if="hasNoRunnersForProject" - class="js-stuck-no-runners append-bottom-0" - > - {{ s__(`Job|This job is stuck, because the project - doesn't have any runners online assigned to it.`) }} - </p> - <p - v-else-if="tags.length" + v-if="tags.length" class="js-stuck-with-tags append-bottom-0" > {{ s__(`This job is stuck, because you don't have @@ -44,6 +37,13 @@ export default { </span> </p> <p + v-else-if="hasNoRunnersForProject" + class="js-stuck-no-runners append-bottom-0" + > + {{ s__(`Job|This job is stuck, because the project + doesn't have any runners online assigned to it.`) }} + </p> + <p v-else class="js-stuck-no-active-runner append-bottom-0" > diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js index ccd096a1da5..a32e945627c 100644 --- a/app/assets/javascripts/jobs/index.js +++ b/app/assets/javascripts/jobs/index.js @@ -23,4 +23,3 @@ export default () => { }, }); }; - diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js index 4ce395a9106..d440b2c9ef1 100644 --- a/app/assets/javascripts/jobs/store/getters.js +++ b/app/assets/javascripts/jobs/store/getters.js @@ -35,23 +35,19 @@ export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status); * Used to check if it should render the job log or the empty state * @returns {Boolean} */ -export const hasTrace = state => state.job.has_trace || (!_.isEmpty(state.job.status) && state.job.status.group === 'running'); +export const hasTrace = state => + state.job.has_trace || (!_.isEmpty(state.job.status) && state.job.status.group === 'running'); export const emptyStateIllustration = state => (state.job && state.job.status && state.job.status.illustration) || {}; -export const emptyStateAction = state => (state.job && state.job.status && state.job.status.action) || {}; -/** - * When the job is pending and there are no available runners - * we need to render the stuck block; - * - * @returns {Boolean} - */ -export const isJobStuck = state => - (!_.isEmpty(state.job.status) && state.job.status.group === 'pending') && - (!_.isEmpty(state.job.runners) && state.job.runners.available === false); +export const emptyStateAction = state => + (state.job && state.job.status && state.job.status.action) || {}; export const isScrollingDown = state => isScrolledToBottom() && !state.isTraceComplete; +export const hasRunnersForProject = state => + state.job.runners.available && !state.job.runners.online; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/jobs/store/mutations.js index 4195d787f12..cd440d21c1f 100644 --- a/app/assets/javascripts/jobs/store/mutations.js +++ b/app/assets/javascripts/jobs/store/mutations.js @@ -71,7 +71,7 @@ export default { * after the first request, * and we do not want to hijack that */ - if (state.selectedStage === 'More' && job.stage) { + if (state.selectedStage === '' && job.stage) { state.selectedStage = job.stage; } }, diff --git a/app/assets/javascripts/jobs/store/state.js b/app/assets/javascripts/jobs/store/state.js index 0eb269ca38f..04825187c99 100644 --- a/app/assets/javascripts/jobs/store/state.js +++ b/app/assets/javascripts/jobs/store/state.js @@ -1,5 +1,3 @@ -import { __ } from '~/locale'; - export default () => ({ jobEndpoint: null, traceEndpoint: null, @@ -29,7 +27,7 @@ export default () => ({ // sidebar dropdown & list of jobs isLoadingStages: false, isLoadingJobs: false, - selectedStage: __('More'), + selectedStage: '', stages: [], jobs: [], }); diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 3c38d998b6c..5457604b3b9 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -25,7 +25,35 @@ export default class LabelsSelect { } $els.each(function(i, dropdown) { - var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer; + var $block, + $colorPreview, + $dropdown, + $form, + $loading, + $selectbox, + $sidebarCollapsedValue, + $value, + abilityName, + defaultLabel, + enableLabelCreateButton, + issueURLSplit, + issueUpdateURL, + labelUrl, + namespacePath, + projectPath, + saveLabelData, + selectedLabel, + showAny, + showNo, + $sidebarLabelTooltip, + initialSelected, + $toggleText, + fieldName, + useId, + propertyName, + showMenuAbove, + $container, + $dropdownContainer; $dropdown = $(dropdown); $dropdownContainer = $dropdown.closest('.labels-filter'); $toggleText = $dropdown.find('.dropdown-toggle-text'); @@ -34,7 +62,7 @@ export default class LabelsSelect { labelUrl = $dropdown.data('labels'); issueUpdateURL = $dropdown.data('issueUpdate'); selectedLabel = $dropdown.data('selected'); - if ((selectedLabel != null) && !$dropdown.hasClass('js-multiselect')) { + if (selectedLabel != null && !$dropdown.hasClass('js-multiselect')) { selectedLabel = selectedLabel.split(','); } showNo = $dropdown.data('showNo'); @@ -50,26 +78,37 @@ export default class LabelsSelect { $value = $block.find('.value'); $loading = $block.find('.block-loading').fadeOut(); fieldName = $dropdown.data('fieldName'); - useId = $dropdown.is('.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown'); + useId = $dropdown.is( + '.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown', + ); propertyName = useId ? 'id' : 'title'; initialSelected = $selectbox .find('input[name="' + $dropdown.data('fieldName') + '"]') - .map(function () { + .map(function() { return this.value; - }).get(); + }) + .get(); const { handleClick } = options; $sidebarLabelTooltip.tooltip(); if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) { - new CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath); + new CreateLabelDropdown( + $dropdown.closest('.dropdown').find('.dropdown-new-label'), + namespacePath, + projectPath, + ); } saveLabelData = function() { var data, selected; - selected = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "']").map(function() { - return this.value; - }).get(); + selected = $dropdown + .closest('.selectbox') + .find("input[name='" + fieldName + "']") + .map(function() { + return this.value; + }) + .get(); if (_.isEqual(initialSelected, selected)) return; initialSelected = selected; @@ -82,7 +121,8 @@ export default class LabelsSelect { } $loading.removeClass('hidden').fadeIn(); $dropdown.trigger('loading.gl.dropdown'); - axios.put(issueUpdateURL, data) + axios + .put(issueUpdateURL, data) .then(({ data }) => { var labelCount, template, labelTooltipTitle, labelTitles, formattedLabels; $loading.fadeOut(); @@ -96,8 +136,7 @@ export default class LabelsSelect { issueUpdateURL, }); labelCount = data.labels.length; - } - else { + } else { template = '<span class="no-value">None</span>'; } $value.removeAttr('style').html(template); @@ -114,17 +153,14 @@ export default class LabelsSelect { } labelTooltipTitle = labelTitles.join(', '); - } - else { + } else { labelTooltipTitle = __('Labels'); } - $sidebarLabelTooltip - .attr('title', labelTooltipTitle) - .tooltip('_fixTitle'); + $sidebarLabelTooltip.attr('title', labelTooltipTitle).tooltip('_fixTitle'); $('.has-tooltip', $value).tooltip({ - container: 'body' + container: 'body', }); }) .catch(() => flash(__('Error saving label update.'))); @@ -132,34 +168,38 @@ export default class LabelsSelect { $dropdown.glDropdown({ showMenuAbove: showMenuAbove, data: function(term, callback) { - axios.get(labelUrl) - .then((res) => { - let data = _.chain(res.data).groupBy(function(label) { - return label.title; - }).map(function(label) { - var color; - color = _.map(label, function(dup) { - return dup.color; - }); - return { - id: label[0].id, - title: label[0].title, - color: color, - duplicate: color.length > 1 - }; - }).value(); + axios + .get(labelUrl) + .then(res => { + let data = _.chain(res.data) + .groupBy(function(label) { + return label.title; + }) + .map(function(label) { + var color; + color = _.map(label, function(dup) { + return dup.color; + }); + return { + id: label[0].id, + title: label[0].title, + color: color, + duplicate: color.length > 1, + }; + }) + .value(); if ($dropdown.hasClass('js-extra-options')) { var extraData = []; if (showNo) { extraData.unshift({ id: 0, - title: 'No Label' + title: 'No Label', }); } if (showAny) { extraData.unshift({ isAny: true, - title: 'Any Label' + title: 'Any Label', }); } if (extraData.length) { @@ -176,11 +216,22 @@ export default class LabelsSelect { .catch(() => flash(__('Error fetching labels.'))); }, renderRow: function(label, instance) { - var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue; + var $a, + $li, + color, + colorEl, + indeterminate, + removesAll, + selectedClass, + spacing, + i, + marked, + dropdownName, + dropdownValue; $li = $('<li>'); $a = $('<a href="#">'); selectedClass = []; - removesAll = label.id <= 0 || (label.id == null); + removesAll = label.id <= 0 || label.id == null; if ($dropdown.hasClass('js-filter-bulk-update')) { indeterminate = $dropdown.data('indeterminate') || []; marked = $dropdown.data('marked') || []; @@ -200,9 +251,19 @@ export default class LabelsSelect { } else { if (this.id(label)) { dropdownName = $dropdown.data('fieldName'); - dropdownValue = this.id(label).toString().replace(/'/g, '\\\''); - - if ($form.find("input[type='hidden'][name='" + dropdownName + "'][value='" + dropdownValue + "']").length) { + dropdownValue = this.id(label) + .toString() + .replace(/'/g, "\\'"); + + if ( + $form.find( + "input[type='hidden'][name='" + + dropdownName + + "'][value='" + + dropdownValue + + "']", + ).length + ) { selectedClass.push('is-active'); } } @@ -213,16 +274,14 @@ export default class LabelsSelect { } if (label.duplicate) { color = DropdownUtils.duplicateLabelColor(label.color); - } - else { + } else { if (label.color != null) { [color] = label.color; } } if (color) { colorEl = "<span class='dropdown-label-box' style='background: " + color + "'></span>"; - } - else { + } else { colorEl = ''; } // We need to identify which items are actually labels @@ -235,7 +294,7 @@ export default class LabelsSelect { return $li.html($a).prop('outerHTML'); }, search: { - fields: ['title'] + fields: ['title'], }, selectable: true, filterable: true, @@ -255,25 +314,21 @@ export default class LabelsSelect { if (selected && selected.id === 0) { this.selected = []; return 'No Label'; - } - else if (isSelected) { + } else if (isSelected) { this.selected.push(title); - } - else if (!isSelected && title) { + } else if (!isSelected && title) { var index = this.selected.indexOf(title); this.selected.splice(index, 1); } if (selectedLabels.length === 1) { return selectedLabels; - } - else if (selectedLabels.length) { + } else if (selectedLabels.length) { return sprintf(__('%{firstLabel} +%{labelCount} more'), { firstLabel: selectedLabels[0], - labelCount: selectedLabels.length - 1 + labelCount: selectedLabels.length - 1, }); - } - else { + } else { return defaultLabel; } }, @@ -285,10 +340,9 @@ export default class LabelsSelect { return label.id; } - if ($dropdown.hasClass("js-filter-submit") && (label.isAny == null)) { + if ($dropdown.hasClass('js-filter-submit') && label.isAny == null) { return label.title; - } - else { + } else { return label.id; } }, @@ -310,13 +364,13 @@ export default class LabelsSelect { } if ($dropdown.hasClass('js-multiselect')) { if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { - selectedLabels = $dropdown.closest('form').find("input:hidden[name='" + ($dropdown.data('fieldName')) + "']"); + selectedLabels = $dropdown + .closest('form') + .find("input:hidden[name='" + $dropdown.data('fieldName') + "']"); Issuable.filterResults($dropdown.closest('form')); - } - else if ($dropdown.hasClass('js-filter-submit')) { + } else if ($dropdown.hasClass('js-filter-submit')) { $dropdown.closest('form').submit(); - } - else { + } else { if (!$dropdown.hasClass('js-filter-bulk-update')) { saveLabelData(); } @@ -325,7 +379,7 @@ export default class LabelsSelect { }, multiSelect: $dropdown.hasClass('js-multiselect'), vue: $dropdown.hasClass('js-issue-board-sidebar'), - clicked: function (clickEvent) { + clicked: function(clickEvent) { const { $el, e, isMarking } = clickEvent; const label = clickEvent.selectedObj; @@ -339,7 +393,8 @@ export default class LabelsSelect { isMRIndex = page === 'projects:merge_requests:index'; if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) { - $dropdown.parent() + $dropdown + .parent() .find('.dropdown-clear-active') .removeClass('is-active'); } @@ -367,28 +422,26 @@ export default class LabelsSelect { e.preventDefault(); return; - } - else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { + } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { if (!$dropdown.hasClass('js-multiselect')) { selectedLabel = label.title; return Issuable.filterResults($dropdown.closest('form')); } - } - else if ($dropdown.hasClass('js-filter-submit')) { + } else if ($dropdown.hasClass('js-filter-submit')) { return $dropdown.closest('form').submit(); - } - else if ($dropdown.hasClass('js-issue-board-sidebar')) { + } else if ($dropdown.hasClass('js-issue-board-sidebar')) { if ($el.hasClass('is-active')) { - boardsStore.detail.issue.labels.push(new ListLabel({ - id: label.id, - title: label.title, - color: label.color[0], - textColor: '#fff' - })); - } - else { + boardsStore.detail.issue.labels.push( + new ListLabel({ + id: label.id, + title: label.title, + color: label.color[0], + textColor: '#fff', + }), + ); + } else { var { labels } = boardsStore.detail.issue; - labels = labels.filter(function (selectedLabel) { + labels = labels.filter(function(selectedLabel) { return selectedLabel.id !== label.id; }); boardsStore.detail.issue.labels = labels; @@ -396,19 +449,16 @@ export default class LabelsSelect { $loading.fadeIn(); - boardsStore.detail.issue.update($dropdown.attr('data-issue-update')) + boardsStore.detail.issue + .update($dropdown.attr('data-issue-update')) .then(fadeOutLoader) .catch(fadeOutLoader); - } - else if (handleClick) { + } else if (handleClick) { e.preventDefault(); handleClick(label); - } - else { + } else { if ($dropdown.hasClass('js-multiselect')) { - - } - else { + } else { return saveLabelData(); } } @@ -436,15 +486,17 @@ export default class LabelsSelect { // so best approach is to use traditional way of // concatenation // see: http://2ality.com/2016/05/template-literal-whitespace.html#joining-arrays - const tpl = _.template([ - '<% _.each(labels, function(label){ %>', - '<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">', - '<span class="badge label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">', - '<%- label.title %>', - '</span>', - '</a>', - '<% }); %>', - ].join('')); + const tpl = _.template( + [ + '<% _.each(labels, function(label){ %>', + '<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">', + '<span class="badge label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">', + '<%- label.title %>', + '</span>', + '</a>', + '<% }); %>', + ].join(''), + ); return tpl(tplData); } diff --git a/app/assets/javascripts/lib/utils/ace_utils.js b/app/assets/javascripts/lib/utils/ace_utils.js index efc4b2a8d94..ee71ae0e61a 100644 --- a/app/assets/javascripts/lib/utils/ace_utils.js +++ b/app/assets/javascripts/lib/utils/ace_utils.js @@ -1,6 +1,6 @@ /* global ace */ export default function getModeByFileExtension(path) { - const modelist = ace.require("ace/ext/modelist"); + const modelist = ace.require('ace/ext/modelist'); return modelist.getModeForPath(path).mode; -}; +} diff --git a/app/assets/javascripts/lib/utils/datefix.js b/app/assets/javascripts/lib/utils/datefix.js deleted file mode 100644 index 19e4085dbbb..00000000000 --- a/app/assets/javascripts/lib/utils/datefix.js +++ /dev/null @@ -1,28 +0,0 @@ -export const pad = (val, len = 2) => `0${val}`.slice(-len); - -/** - * Formats dates in Pickaday - * @param {String} dateString Date in yyyy-mm-dd format - * @return {Date} UTC format - */ -export const parsePikadayDate = dateString => { - const parts = dateString.split('-'); - const year = parseInt(parts[0], 10); - const month = parseInt(parts[1] - 1, 10); - const day = parseInt(parts[2], 10); - - return new Date(year, month, day); -}; - -/** - * Used `onSelect` method in pickaday - * @param {Date} date UTC format - * @return {String} Date formated in yyyy-mm-dd - */ -export const pikadayToString = date => { - const day = pad(date.getDate()); - const month = pad(date.getMonth() + 1); - const year = date.getFullYear(); - - return `${year}-${month}-${day}`; -}; diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 833dbefd3dc..46740308f17 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -1,4 +1,5 @@ import $ from 'jquery'; +import _ from 'underscore'; import timeago from 'timeago.js'; import dateFormat from 'dateformat'; import { pluralize } from './text_utility'; @@ -46,6 +47,8 @@ const getMonthNames = abbreviated => { ]; }; +export const pad = (val, len = 2) => `0${val}`.slice(-len); + /** * Given a date object returns the day of the week in English * @param {date} date @@ -74,10 +77,10 @@ let timeagoInstance; /** * Sets a timeago Instance */ -export function getTimeago() { +export const getTimeago = () => { if (!timeagoInstance) { - const localeRemaining = function getLocaleRemaining(number, index) { - return [ + const localeRemaining = (number, index) => + [ [s__('Timeago|just now'), s__('Timeago|right now')], [s__('Timeago|%s seconds ago'), s__('Timeago|%s seconds remaining')], [s__('Timeago|1 minute ago'), s__('Timeago|1 minute remaining')], @@ -93,9 +96,9 @@ export function getTimeago() { [s__('Timeago|1 year ago'), s__('Timeago|1 year remaining')], [s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')], ][index]; - }; - const locale = function getLocale(number, index) { - return [ + + const locale = (number, index) => + [ [s__('Timeago|just now'), s__('Timeago|right now')], [s__('Timeago|%s seconds ago'), s__('Timeago|in %s seconds')], [s__('Timeago|1 minute ago'), s__('Timeago|in 1 minute')], @@ -111,7 +114,6 @@ export function getTimeago() { [s__('Timeago|1 year ago'), s__('Timeago|in 1 year')], [s__('Timeago|%s years ago'), s__('Timeago|in %s years')], ][index]; - }; timeago.register(timeagoLanguageCode, locale); timeago.register(`${timeagoLanguageCode}-remaining`, localeRemaining); @@ -119,7 +121,7 @@ export function getTimeago() { } return timeagoInstance; -} +}; /** * For the given element, renders a timeago instance. @@ -184,7 +186,7 @@ export const getDayDifference = (a, b) => { * @param {Number} seconds * @return {String} */ -export function timeIntervalInWords(intervalInSeconds) { +export const timeIntervalInWords = intervalInSeconds => { const secondsInteger = parseInt(intervalInSeconds, 10); const minutes = Math.floor(secondsInteger / 60); const seconds = secondsInteger - minutes * 60; @@ -196,9 +198,9 @@ export function timeIntervalInWords(intervalInSeconds) { text = `${seconds} ${pluralize('second', seconds)}`; } return text; -} +}; -export function dateInWords(date, abbreviated = false, hideYear = false) { +export const dateInWords = (date, abbreviated = false, hideYear = false) => { if (!date) return date; const month = date.getMonth(); @@ -240,7 +242,7 @@ export function dateInWords(date, abbreviated = false, hideYear = false) { } return `${monthName} ${date.getDate()}, ${year}`; -} +}; /** * Returns month name based on provided date. @@ -391,3 +393,95 @@ export const formatTime = milliseconds => { formattedTime += remainingSeconds; return formattedTime; }; + +/** + * Formats dates in Pickaday + * @param {String} dateString Date in yyyy-mm-dd format + * @return {Date} UTC format + */ +export const parsePikadayDate = dateString => { + const parts = dateString.split('-'); + const year = parseInt(parts[0], 10); + const month = parseInt(parts[1] - 1, 10); + const day = parseInt(parts[2], 10); + + return new Date(year, month, day); +}; + +/** + * Used `onSelect` method in pickaday + * @param {Date} date UTC format + * @return {String} Date formated in yyyy-mm-dd + */ +export const pikadayToString = date => { + const day = pad(date.getDate()); + const month = pad(date.getMonth() + 1); + const year = date.getFullYear(); + + return `${year}-${month}-${day}`; +}; + +/** + * Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # } + * Seconds can be negative or positive, zero or non-zero. Can be configured for any day + * or week length. + */ +export const parseSeconds = (seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) => { + const DAYS_PER_WEEK = daysPerWeek; + const HOURS_PER_DAY = hoursPerDay; + const MINUTES_PER_HOUR = 60; + const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR; + const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR; + + const timePeriodConstraints = { + weeks: MINUTES_PER_WEEK, + days: MINUTES_PER_DAY, + hours: MINUTES_PER_HOUR, + minutes: 1, + }; + + let unorderedMinutes = Math.abs(seconds / MINUTES_PER_HOUR); + + return _.mapObject(timePeriodConstraints, minutesPerPeriod => { + const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod); + + unorderedMinutes -= periodCount * minutesPerPeriod; + + return periodCount; + }); +}; + +/** + * Accepts a timeObject (see parseSeconds) and returns a condensed string representation of it + * (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included. + */ +export const stringifyTime = timeObject => { + const reducedTime = _.reduce( + timeObject, + (memo, unitValue, unitName) => { + const isNonZero = !!unitValue; + return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo; + }, + '', + ).trim(); + return reducedTime.length ? reducedTime : '0m'; +}; + +/** + * Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns + * the first non-zero unit/value pair. + */ +export const abbreviateTime = timeStr => + timeStr.split(' ').filter(unitStr => unitStr.charAt(0) !== '0')[0]; + +/** + * Calculates the milliseconds between now and a given date string. + * The result cannot become negative. + * + * @param endDate date string that the time difference is calculated for + * @return {number} number of milliseconds remaining until the given date + */ +export const calculateRemainingMilliseconds = endDate => { + const remainingMilliseconds = new Date(endDate).getTime() - Date.now(); + return Math.max(remainingMilliseconds, 0); +}; diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js index afbab59055b..2ccc51c35f7 100644 --- a/app/assets/javascripts/lib/utils/number_utils.js +++ b/app/assets/javascripts/lib/utils/number_utils.js @@ -7,7 +7,7 @@ import { BYTES_IN_KIB } from './constants'; * * * Show 3 digits to the right * * For 2 digits to the left of the decimal point and X digits to the right of it * * * Show 2 digits to the right -*/ + */ export function formatRelevantDigits(number) { let digitsLeft = ''; let relevantDigits = 0; diff --git a/app/assets/javascripts/lib/utils/pretty_time.js b/app/assets/javascripts/lib/utils/pretty_time.js deleted file mode 100644 index d92b8a7179f..00000000000 --- a/app/assets/javascripts/lib/utils/pretty_time.js +++ /dev/null @@ -1,63 +0,0 @@ -import _ from 'underscore'; - -/* - * TODO: Make these methods more configurable (e.g. stringifyTime condensed or - * non-condensed, abbreviateTimelengths) - * */ - -/* - * Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # } - * Seconds can be negative or positive, zero or non-zero. Can be configured for any day - * or week length. -*/ - -export function parseSeconds(seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) { - const DAYS_PER_WEEK = daysPerWeek; - const HOURS_PER_DAY = hoursPerDay; - const MINUTES_PER_HOUR = 60; - const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR; - const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR; - - const timePeriodConstraints = { - weeks: MINUTES_PER_WEEK, - days: MINUTES_PER_DAY, - hours: MINUTES_PER_HOUR, - minutes: 1, - }; - - let unorderedMinutes = Math.abs(seconds / MINUTES_PER_HOUR); - - return _.mapObject(timePeriodConstraints, minutesPerPeriod => { - const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod); - - unorderedMinutes -= periodCount * minutesPerPeriod; - - return periodCount; - }); -} - -/* -* Accepts a timeObject (see parseSeconds) and returns a condensed string representation of it -* (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included. -*/ - -export function stringifyTime(timeObject) { - const reducedTime = _.reduce( - timeObject, - (memo, unitValue, unitName) => { - const isNonZero = !!unitValue; - return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo; - }, - '', - ).trim(); - return reducedTime.length ? reducedTime : '0m'; -} - -/* -* Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns -* the first non-zero unit/value pair. -*/ - -export function abbreviateTime(timeStr) { - return timeStr.split(' ').filter(unitStr => unitStr.charAt(0) !== '0')[0]; -} diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index e26a6b986be..c52cfb806a2 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -2,6 +2,8 @@ import $ from 'jquery'; import { insertText } from '~/lib/utils/common_utils'; +const LINK_TAG_PATTERN = '[{text}](url)'; + function selectedText(text, textarea) { return text.substring(textarea.selectionStart, textarea.selectionEnd); } @@ -76,6 +78,21 @@ export function insertMarkdownText({ textArea, text, tag, blockTag, selected, wr removedFirstNewLine = false; currentLineEmpty = false; + // check for link pattern and selected text is an URL + // if so fill in the url part instead of the text part of the pattern. + if (tag === LINK_TAG_PATTERN) { + if (URL) { + try { + const ignoredUrl = new URL(selected); + // valid url + tag = '[text]({text})'; + select = 'text'; + } catch (e) { + // ignore - no valid url + } + } + } + // Remove the first newline if (selected.indexOf('\n') === 0) { removedFirstNewLine = true; diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js index df5cd1b8c51..0beedcacf33 100644 --- a/app/assets/javascripts/member_expiration_date.js +++ b/app/assets/javascripts/member_expiration_date.js @@ -1,6 +1,6 @@ import $ from 'jquery'; import Pikaday from 'pikaday'; -import { parsePikadayDate, pikadayToString } from './lib/utils/datefix'; +import { parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility'; // Add datepickers to all `js-access-expiration-date` elements. If those elements are // children of an element with the `clearable-input` class, and have a sibling diff --git a/app/assets/javascripts/members.js b/app/assets/javascripts/members.js index 7d0c701fd70..bd263c75a3d 100644 --- a/app/assets/javascripts/members.js +++ b/app/assets/javascripts/members.js @@ -7,8 +7,12 @@ export default class Members { } addListeners() { - $('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this)); - $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this)); + $('.js-member-update-control') + .off('change') + .on('change', this.formSubmit.bind(this)); + $('.js-edit-member-form') + .off('ajax:success') + .on('ajax:success', this.formSuccess.bind(this)); gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change'); } @@ -28,7 +32,7 @@ export default class Members { toggleLabel(selected, $el) { return $el.text(); }, - clicked: (options) => { + clicked: options => { this.formSubmit(null, options.$el); }, }); diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 42fb5c7177a..d32f39881dd 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -9,7 +9,10 @@ import '~/gl_dropdown'; import axios from './lib/utils/axios_utils'; import { timeFor } from './lib/utils/datetime_utility'; import ModalStore from './boards/stores/modal_store'; -import boardsStore, { boardStoreIssueSet, boardStoreIssueDelete } from './boards/stores/boards_store'; +import boardsStore, { + boardStoreIssueSet, + boardStoreIssueDelete, +} from './boards/stores/boards_store'; export default class MilestoneSelect { constructor(currentProject, els, options = {}) { diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 67338aa96c3..98182d92c2f 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -149,7 +149,7 @@ export default { .catch(() => Flash(s__('Metrics|There was an error getting deployment information.'))), this.service .getEnvironmentsData() - .then((data) => this.store.storeEnvironmentsData(data)) + .then(data => this.store.storeEnvironmentsData(data)) .catch(() => Flash(s__('Metrics|There was an error getting environments information.'))), ]) .then(() => { @@ -157,6 +157,7 @@ export default { this.state = 'noData'; return; } + this.showEmptyState = false; }) .then(this.resize) @@ -195,7 +196,10 @@ export default { name="chevron-down" /> </button> - <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"> + <div + v-if="store.environmentsData.length > 0" + class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up" + > <ul> <li v-for="environment in store.environmentsData" diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index ed5c8b15945..5c6e2e09e46 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -121,6 +121,7 @@ export default { draw() { const breakpointSize = bp.getBreakpointSize(); const query = this.graphData.queries[0]; + const svgWidth = this.$refs.baseSvg.getBoundingClientRect().width; this.margin = measurements.large.margin; if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') { this.graphHeight = 300; @@ -130,13 +131,13 @@ export default { this.unitOfDisplay = query.unit || ''; this.yAxisLabel = this.graphData.y_label || 'Values'; this.legendTitle = query.label || 'Average'; - this.graphWidth = this.$refs.baseSvg.clientWidth - this.margin.left - this.margin.right; + this.graphWidth = svgWidth - this.margin.left - this.margin.right; this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom; this.baseGraphHeight = this.graphHeight - 50; this.baseGraphWidth = this.graphWidth; // pixel offsets inside the svg and outside are not 1:1 - this.realPixelRatio = this.$refs.baseSvg.clientWidth / this.baseGraphWidth; + this.realPixelRatio = svgWidth / this.baseGraphWidth; this.renderAxesPaths(); this.formatDeployments(); diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 1369b5820d5..90fe339e3de 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -16,7 +16,7 @@ import 'vendor/jquery.atwho'; import AjaxCache from '~/lib/utils/ajax_cache'; import Vue from 'vue'; import syntaxHighlight from '~/syntax_highlight'; -import { SkeletonLoading } from '@gitlab-org/gitlab-ui'; +import { GlSkeletonLoading } from '@gitlab-org/gitlab-ui'; import axios from './lib/utils/axios_utils'; import { getLocationHash } from './lib/utils/url_utility'; import Flash from './flash'; @@ -1293,10 +1293,10 @@ export default class Notes { new Vue({ el, components: { - SkeletonLoading, + GlSkeletonLoading, }, render(createElement) { - return createElement('skeleton-loading'); + return createElement('gl-skeleton-loading'); }, }); } diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index d9e99603238..eaa0cded224 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -3,13 +3,13 @@ import { mapState, mapActions } from 'vuex'; import imageDiffHelper from '~/image_diff/helpers/index'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import DiffFileHeader from '~/diffs/components/diff_file_header.vue'; -import { SkeletonLoading } from '@gitlab-org/gitlab-ui'; +import { GlSkeletonLoading } from '@gitlab-org/gitlab-ui'; import { trimFirstCharOfLineContent } from '~/diffs/store/utils'; export default { components: { DiffFileHeader, - SkeletonLoading, + GlSkeletonLoading, }, props: { discussion: { @@ -143,7 +143,7 @@ export default { class="line_content js-success-lazy-load" > <span></span> - <skeleton-loading /> + <gl-skeleton-loading /> <span></span> </td> </tr> diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue index 27972682ca1..6e8f43048d1 100644 --- a/app/assets/javascripts/notes/components/discussion_filter.vue +++ b/app/assets/javascripts/notes/components/discussion_filter.vue @@ -22,9 +22,7 @@ export default { return { currentValue: this.defaultValue }; }, computed: { - ...mapGetters([ - 'getNotesDataByProp', - ]), + ...mapGetters(['getNotesDataByProp']), currentFilter() { if (!this.currentValue) return this.filters[0]; return this.filters.find(filter => filter.value === this.currentValue); @@ -51,7 +49,7 @@ export default { <button id="discussion-filter-dropdown" ref="dropdownToggle" - class="btn btn-default" + class="btn btn-default qa-discussion-filter" data-toggle="dropdown" aria-expanded="false" > @@ -69,6 +67,7 @@ export default { > <button :class="{ 'is-active': filter.value === currentValue }" + class="qa-filter-options" type="button" @click="selectFilter(filter.value)" > diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index c68860d98ae..df7ab4502a6 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -95,31 +95,22 @@ export default { return awardList.filter(award => award.user.id === this.getUserData.id).length; }, awardTitle(awardsList) { - const hasReactionByCurrentUser = this.hasReactionByCurrentUser( - awardsList, - ); + const hasReactionByCurrentUser = this.hasReactionByCurrentUser(awardsList); const TOOLTIP_NAME_COUNT = hasReactionByCurrentUser ? 9 : 10; let awardList = awardsList; // Filter myself from list if I am awarded. if (hasReactionByCurrentUser) { - awardList = awardList.filter( - award => award.user.id !== this.getUserData.id, - ); + awardList = awardList.filter(award => award.user.id !== this.getUserData.id); } // Get only 9-10 usernames to show in tooltip text. - const namesToShow = awardList - .slice(0, TOOLTIP_NAME_COUNT) - .map(award => award.user.name); + const namesToShow = awardList.slice(0, TOOLTIP_NAME_COUNT).map(award => award.user.name); // Get the remaining list to use in `and x more` text. - const remainingAwardList = awardList.slice( - TOOLTIP_NAME_COUNT, - awardList.length, - ); + const remainingAwardList = awardList.slice(TOOLTIP_NAME_COUNT, awardList.length); - // Add myself to the begining of the list so title will start with You. + // Add myself to the beginning of the list so title will start with You. if (hasReactionByCurrentUser) { namesToShow.unshift('You'); } @@ -128,9 +119,7 @@ export default { // We have 10+ awarded user, join them with comma and add `and x more`. if (remainingAwardList.length) { - title = `${namesToShow.join(', ')}, and ${ - remainingAwardList.length - } more.`; + title = `${namesToShow.join(', ')}, and ${remainingAwardList.length} more.`; } else if (namesToShow.length > 1) { // Join all names with comma but not the last one, it will be added with and text. title = namesToShow.slice(0, namesToShow.length - 1).join(', '); @@ -170,9 +159,7 @@ export default { awardName: parsedName, }; - this.toggleAwardRequest(data).catch(() => - Flash('Something went wrong on our end.'), - ); + this.toggleAwardRequest(data).catch(() => Flash('Something went wrong on our end.')); }, }, }; diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index b0faa443a18..7514ce8a1eb 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -54,7 +54,13 @@ export default { }; }, computed: { - ...mapGetters(['isNotesFetched', 'discussions', 'getNotesDataByProp', 'discussionCount', 'isLoading']), + ...mapGetters([ + 'isNotesFetched', + 'discussions', + 'getNotesDataByProp', + 'discussionCount', + 'isLoading', + ]), noteableType() { return this.noteableData.noteableType; }, diff --git a/app/assets/javascripts/notes/discussion_filters.js b/app/assets/javascripts/notes/discussion_filters.js index 012ffc4093e..06eadaeea0e 100644 --- a/app/assets/javascripts/notes/discussion_filters.js +++ b/app/assets/javascripts/notes/discussion_filters.js @@ -1,15 +1,17 @@ import Vue from 'vue'; import DiscussionFilter from './components/discussion_filter.vue'; -export default (store) => { +export default store => { const discussionFilterEl = document.getElementById('js-vue-discussion-filter'); if (discussionFilterEl) { const { defaultFilter, notesFilters } = discussionFilterEl.dataset; const defaultValue = defaultFilter ? parseInt(defaultFilter, 10) : null; const filterValues = notesFilters ? JSON.parse(notesFilters) : {}; - const filters = Object.keys(filterValues).map(entry => - ({ title: entry, value: filterValues[entry] })); + const filters = Object.keys(filterValues).map(entry => ({ + title: entry, + value: filterValues[entry], + })); return new Vue({ el: discussionFilterEl, diff --git a/app/assets/javascripts/notes/stores/collapse_utils.js b/app/assets/javascripts/notes/stores/collapse_utils.js index fa4a1c56b20..bee6d4f0329 100644 --- a/app/assets/javascripts/notes/stores/collapse_utils.js +++ b/app/assets/javascripts/notes/stores/collapse_utils.js @@ -68,12 +68,9 @@ export const collapseSystemNotes = notes => { lastDescriptionSystemNote = note; lastDescriptionSystemNoteIndex = acc.length; } else if (lastDescriptionSystemNote) { - const timeDifferenceMinutes = getTimeDifferenceMinutes( - lastDescriptionSystemNote, - note, - ); + const timeDifferenceMinutes = getTimeDifferenceMinutes(lastDescriptionSystemNote, note); - // are they less than 10 minutes appart? + // are they less than 10 minutes apart? if (timeDifferenceMinutes > 10) { // reset counter descriptionChangedTimes = 1; diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index 21c334a9d33..e4f36154fcd 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -1,6 +1,5 @@ import _ from 'underscore'; import * as constants from '../constants'; -import { reduceDiscussionsToLineCodes } from './utils'; import { collapseSystemNotes } from './collapse_utils'; export const discussions = state => collapseSystemNotes(state.discussions); @@ -31,9 +30,6 @@ export const notesById = state => return acc; }, {}); -export const discussionsStructuredByLineCode = state => - reduceDiscussionsToLineCodes(state.discussions); - export const noteableType = state => { const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants; diff --git a/app/assets/javascripts/notes/stores/index.js b/app/assets/javascripts/notes/stores/index.js index f105b7d0d11..d41b02b4a4b 100644 --- a/app/assets/javascripts/notes/stores/index.js +++ b/app/assets/javascripts/notes/stores/index.js @@ -4,5 +4,4 @@ import notesModule from './modules'; Vue.use(Vuex); -export default () => - new Vuex.Store(notesModule()); +export default () => new Vuex.Store(notesModule()); diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js index 0e41ff03d67..dd57539e4d8 100644 --- a/app/assets/javascripts/notes/stores/utils.js +++ b/app/assets/javascripts/notes/stores/utils.js @@ -25,18 +25,6 @@ export const getQuickActionText = note => { return text; }; -export const reduceDiscussionsToLineCodes = selectedDiscussions => - selectedDiscussions.reduce((acc, note) => { - if (note.diff_discussion && note.line_code) { - // For context about line notes: there might be multiple notes with the same line code - const items = acc[note.line_code] || []; - items.push(note); - - Object.assign(acc, { [note.line_code]: items }); - } - return acc; - }, {}); - export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note); export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim(); diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js index 3b58c54b3f4..386a9b2c740 100644 --- a/app/assets/javascripts/pager.js +++ b/app/assets/javascripts/pager.js @@ -7,14 +7,21 @@ const ENDLESS_SCROLL_BOTTOM_PX = 400; const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000; export default { - init(limit = 0, preload = false, disable = false, prepareData = $.noop, callback = $.noop) { + init( + limit = 0, + preload = false, + disable = false, + prepareData = $.noop, + callback = $.noop, + container = '', + ) { this.url = $('.content_list').data('href') || removeParams(['limit', 'offset']); this.limit = limit; this.offset = parseInt(getParameterByName('offset'), 10) || this.limit; this.disable = disable; this.prepareData = prepareData; this.callback = callback; - this.loading = $('.loading').first(); + this.loading = $(`${container} .loading`).first(); if (preload) { this.offset = 0; this.getOld(); diff --git a/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js index 15e737fff05..d9cf62db3f7 100644 --- a/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js +++ b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js @@ -31,7 +31,7 @@ export default class AbuseReports { $messageCellElement.text(originalMessage); } else { $messageCellElement.data('messageTruncated', 'true'); - $messageCellElement.text(`${originalMessage.substr(0, (MAX_MESSAGE_LENGTH - 3))}...`); + $messageCellElement.text(`${originalMessage.substr(0, MAX_MESSAGE_LENGTH - 3)}...`); } } } diff --git a/app/assets/javascripts/pages/admin/admin.js b/app/assets/javascripts/pages/admin/admin.js index ff4d6ab15f9..4616a075729 100644 --- a/app/assets/javascripts/pages/admin/admin.js +++ b/app/assets/javascripts/pages/admin/admin.js @@ -23,7 +23,7 @@ export default function adminInit() { } }); - $('body').on('click', '.js-toggle-colors-link', (e) => { + $('body').on('click', '.js-toggle-colors-link', e => { e.preventDefault(); $('.js-toggle-colors-container').toggleClass('hide'); }); @@ -33,12 +33,15 @@ export default function adminInit() { $(this).tab('show'); }); - $('.log-bottom').on('click', (e) => { + $('.log-bottom').on('click', e => { e.preventDefault(); const $visibleLog = $('.file-content:visible'); - $visibleLog.animate({ - scrollTop: $visibleLog.find('ol').height(), - }, 'fast'); + $visibleLog.animate( + { + scrollTop: $visibleLog.find('ol').height(), + }, + 'fast', + ); }); $('.change-owner-link').on('click', function changeOwnerLinkClick(e) { @@ -47,7 +50,7 @@ export default function adminInit() { modal.show(); }); - $('.change-owner-cancel-link').on('click', (e) => { + $('.change-owner-cancel-link').on('click', e => { e.preventDefault(); modal.hide(); $('.change-owner-link').show(); diff --git a/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js b/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js index 7281f907ec7..455c637a6b3 100644 --- a/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js +++ b/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js @@ -1,10 +1,14 @@ import { __ } from '~/locale'; export const PLACEHOLDER_USER_EXTERNAL_DEFAULT_TRUE = __('Regex pattern'); -export const PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE = __('To define internal users, first enable new users set to external'); +export const PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE = __( + 'To define internal users, first enable new users set to external', +); function setUserInternalRegexPlaceholder(checkbox) { - const userInternalRegex = document.getElementById('application_setting_user_default_internal_regex'); + const userInternalRegex = document.getElementById( + 'application_setting_user_default_internal_regex', + ); if (checkbox && userInternalRegex) { if (checkbox.checked) { userInternalRegex.readOnly = false; diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js index e7ceccb6f47..d5ded3f9a79 100644 --- a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js +++ b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js @@ -17,20 +17,24 @@ export default () => { const previewPath = $('textarea#broadcast_message_message').data('previewPath'); - $('textarea#broadcast_message_message').on('input', _.debounce(function onMessageInput() { - const message = $(this).val(); - if (message === '') { - $('.js-broadcast-message-preview').text('Your message here'); - } else { - axios.post(previewPath, { - broadcast_message: { - message, - }, - }) - .then(({ data }) => { - $('.js-broadcast-message-preview').html(data.message); - }) - .catch(() => flash(__('An error occurred while rendering preview broadcast message'))); - } - }, 250)); + $('textarea#broadcast_message_message').on( + 'input', + _.debounce(function onMessageInput() { + const message = $(this).val(); + if (message === '') { + $('.js-broadcast-message-preview').text('Your message here'); + } else { + axios + .post(previewPath, { + broadcast_message: { + message, + }, + }) + .then(({ data }) => { + $('.js-broadcast-message-preview').html(data.message); + }) + .catch(() => flash(__('An error occurred while rendering preview broadcast message'))); + } + }, 250), + ); }; diff --git a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue index bc84666779e..e2fec3c7172 100644 --- a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue +++ b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue @@ -1,39 +1,42 @@ <script> - import axios from '~/lib/utils/axios_utils'; - import createFlash from '~/flash'; - import GlModal from '~/vue_shared/components/gl_modal.vue'; - import { redirectTo } from '~/lib/utils/url_utility'; - import { s__ } from '~/locale'; +import axios from '~/lib/utils/axios_utils'; +import createFlash from '~/flash'; +import GlModal from '~/vue_shared/components/gl_modal.vue'; +import { redirectTo } from '~/lib/utils/url_utility'; +import { s__ } from '~/locale'; - export default { - components: { - GlModal, +export default { + components: { + GlModal, + }, + props: { + url: { + type: String, + required: true, }, - props: { - url: { - type: String, - required: true, - }, + }, + computed: { + text() { + return s__( + 'AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running.', + ); }, - computed: { - text() { - return s__('AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running.'); - }, + }, + methods: { + onSubmit() { + return axios + .post(this.url) + .then(response => { + // follow the rediect to refresh the page + redirectTo(response.request.responseURL); + }) + .catch(error => { + createFlash(s__('AdminArea|Stopping jobs failed')); + throw error; + }); }, - methods: { - onSubmit() { - return axios.post(this.url) - .then((response) => { - // follow the rediect to refresh the page - redirectTo(response.request.responseURL); - }) - .catch((error) => { - createFlash(s__('AdminArea|Stopping jobs failed')); - throw error; - }); - }, - }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/pages/admin/projects/index.js b/app/assets/javascripts/pages/admin/projects/index.js index 31c96eb87af..d6b1e747aec 100644 --- a/app/assets/javascripts/pages/admin/projects/index.js +++ b/app/assets/javascripts/pages/admin/projects/index.js @@ -4,6 +4,7 @@ import NamespaceSelect from '../../../namespace_select'; document.addEventListener('DOMContentLoaded', () => { new ProjectsList(); // eslint-disable-line no-new - document.querySelectorAll('.js-namespace-select') + document + .querySelectorAll('.js-namespace-select') .forEach(dropdown => new NamespaceSelect({ dropdown })); }); diff --git a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue index ff66d3a8ac4..3c383735f4a 100644 --- a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue +++ b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue @@ -1,81 +1,84 @@ <script> - import _ from 'underscore'; - import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; - import { s__, sprintf } from '~/locale'; +import _ from 'underscore'; +import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; +import { s__, sprintf } from '~/locale'; - export default { - components: { - DeprecatedModal, +export default { + components: { + DeprecatedModal, + }, + props: { + deleteProjectUrl: { + type: String, + required: false, + default: '', }, - props: { - deleteProjectUrl: { - type: String, - required: false, - default: '', - }, - projectName: { - type: String, - required: false, - default: '', - }, - csrfToken: { - type: String, - required: false, - default: '', - }, + projectName: { + type: String, + required: false, + default: '', }, - data() { - return { - enteredProjectName: '', - }; + csrfToken: { + type: String, + required: false, + default: '', }, - computed: { - title() { - return sprintf(s__('AdminProjects|Delete Project %{projectName}?'), - { - projectName: `'${_.escape(this.projectName)}'`, - }, - false, - ); - }, - text() { - return sprintf(s__(`AdminProjects| + }, + data() { + return { + enteredProjectName: '', + }; + }, + computed: { + title() { + return sprintf( + s__('AdminProjects|Delete Project %{projectName}?'), + { + projectName: `'${_.escape(this.projectName)}'`, + }, + false, + ); + }, + text() { + return sprintf( + s__(`AdminProjects| You’re about to permanently delete the project %{projectName}, its repository, and all related resources including issues, merge requests, etc.. Once you confirm and press %{strong_start}Delete project%{strong_end}, it cannot be undone or recovered.`), - { - projectName: `<strong>${_.escape(this.projectName)}</strong>`, - strong_start: '<strong>', - strong_end: '</strong>', - }, - false, - ); - }, - confirmationTextLabel() { - return sprintf(s__('AdminUsers|To confirm, type %{projectName}'), - { - projectName: `<code>${_.escape(this.projectName)}</code>`, - }, - false, - ); - }, - primaryButtonLabel() { - return s__('AdminProjects|Delete project'); - }, - canSubmit() { - return this.enteredProjectName === this.projectName; - }, + { + projectName: `<strong>${_.escape(this.projectName)}</strong>`, + strong_start: '<strong>', + strong_end: '</strong>', + }, + false, + ); + }, + confirmationTextLabel() { + return sprintf( + s__('AdminUsers|To confirm, type %{projectName}'), + { + projectName: `<code>${_.escape(this.projectName)}</code>`, + }, + false, + ); + }, + primaryButtonLabel() { + return s__('AdminProjects|Delete project'); + }, + canSubmit() { + return this.enteredProjectName === this.projectName; + }, + }, + methods: { + onCancel() { + this.enteredProjectName = ''; }, - methods: { - onCancel() { - this.enteredProjectName = ''; - }, - onSubmit() { - this.$refs.form.submit(); - this.enteredProjectName = ''; - }, + onSubmit() { + this.$refs.form.submit(); + this.enteredProjectName = ''; }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/pages/admin/projects/index/index.js b/app/assets/javascripts/pages/admin/projects/index/index.js index ddbefec87b6..6fa8760545d 100644 --- a/app/assets/javascripts/pages/admin/projects/index/index.js +++ b/app/assets/javascripts/pages/admin/projects/index/index.js @@ -28,7 +28,7 @@ document.addEventListener('DOMContentLoaded', () => { }, }); - $(document).on('shown.bs.modal', (event) => { + $(document).on('shown.bs.modal', event => { if (event.relatedTarget.classList.contains('delete-project-button')) { const buttonProps = event.relatedTarget.dataset; deleteModal.deleteProjectUrl = buttonProps.deleteProjectUrl; 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 8d5efcdcd96..4b33fcc759a 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 @@ -1,114 +1,119 @@ <script> - import _ from 'underscore'; - import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; - import { s__, sprintf } from '~/locale'; +import _ from 'underscore'; +import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; +import { s__, sprintf } from '~/locale'; - export default { - components: { - DeprecatedModal, +export default { + components: { + DeprecatedModal, + }, + props: { + deleteUserUrl: { + type: String, + required: false, + default: '', }, - props: { - deleteUserUrl: { - type: String, - required: false, - default: '', - }, - blockUserUrl: { - type: String, - required: false, - default: '', - }, - deleteContributions: { - type: Boolean, - required: false, - default: false, - }, - username: { - type: String, - required: false, - default: '', - }, - csrfToken: { - type: String, - required: false, - default: '', - }, + blockUserUrl: { + type: String, + required: false, + default: '', }, - data() { - return { - enteredUsername: '', - }; + deleteContributions: { + type: Boolean, + required: false, + default: false, }, - computed: { - title() { - const keepContributionsTitle = s__('AdminUsers|Delete User %{username}?'); - const deleteContributionsTitle = s__('AdminUsers|Delete User %{username} and contributions?'); + username: { + type: String, + required: false, + default: '', + }, + csrfToken: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + enteredUsername: '', + }; + }, + computed: { + title() { + const keepContributionsTitle = s__('AdminUsers|Delete User %{username}?'); + const deleteContributionsTitle = s__('AdminUsers|Delete User %{username} and contributions?'); - return sprintf( - this.deleteContributions ? deleteContributionsTitle : keepContributionsTitle, { - username: `'${_.escape(this.username)}'`, - }, false); - }, - text() { - const keepContributionsText = s__(`AdminArea| + return sprintf( + this.deleteContributions ? deleteContributionsTitle : keepContributionsTitle, + { + username: `'${_.escape(this.username)}'`, + }, + false, + ); + }, + text() { + const keepContributionsText = s__(`AdminArea| You are about to permanently delete the user %{username}. Issues, merge requests, and groups linked to them will be transferred to a system-wide "Ghost-user". To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`); - const deleteContributionsText = s__(`AdminArea| + const deleteContributionsText = s__(`AdminArea| You are about to permanently delete the user %{username}. This will delete all of the issues, merge requests, and groups linked to them. To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`); - return sprintf(this.deleteContributions ? deleteContributionsText : keepContributionsText, - { - username: `<strong>${_.escape(this.username)}</strong>`, - strong_start: '<strong>', - strong_end: '</strong>', - }, - false, - ); - }, - confirmationTextLabel() { - return sprintf(s__('AdminUsers|To confirm, type %{username}'), - { - username: `<code>${_.escape(this.username)}</code>`, - }, - false, - ); - }, - primaryButtonLabel() { - const keepContributionsLabel = s__('AdminUsers|Delete user'); - const deleteContributionsLabel = s__('AdminUsers|Delete user and contributions'); + return sprintf( + this.deleteContributions ? deleteContributionsText : keepContributionsText, + { + username: `<strong>${_.escape(this.username)}</strong>`, + strong_start: '<strong>', + strong_end: '</strong>', + }, + false, + ); + }, + confirmationTextLabel() { + return sprintf( + s__('AdminUsers|To confirm, type %{username}'), + { + username: `<code>${_.escape(this.username)}</code>`, + }, + false, + ); + }, + primaryButtonLabel() { + const keepContributionsLabel = s__('AdminUsers|Delete user'); + const deleteContributionsLabel = s__('AdminUsers|Delete user and contributions'); - return this.deleteContributions ? deleteContributionsLabel : keepContributionsLabel; - }, - secondaryButtonLabel() { - return s__('AdminUsers|Block user'); - }, - canSubmit() { - return this.enteredUsername === this.username; - }, + return this.deleteContributions ? deleteContributionsLabel : keepContributionsLabel; }, - methods: { - onCancel() { - this.enteredUsername = ''; - }, - onSecondaryAction() { - const { form } = this.$refs; + secondaryButtonLabel() { + return s__('AdminUsers|Block user'); + }, + canSubmit() { + return this.enteredUsername === this.username; + }, + }, + methods: { + onCancel() { + this.enteredUsername = ''; + }, + onSecondaryAction() { + const { form } = this.$refs; - form.action = this.blockUserUrl; - this.$refs.method.value = 'put'; + form.action = this.blockUserUrl; + this.$refs.method.value = 'put'; - form.submit(); - }, - onSubmit() { - this.$refs.form.submit(); - this.enteredUsername = ''; - }, + form.submit(); + }, + onSubmit() { + this.$refs.form.submit(); + this.enteredUsername = ''; }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/pages/admin/users/index.js b/app/assets/javascripts/pages/admin/users/index.js index 06599c3fd5f..45046688b57 100644 --- a/app/assets/javascripts/pages/admin/users/index.js +++ b/app/assets/javascripts/pages/admin/users/index.js @@ -32,12 +32,14 @@ document.addEventListener('DOMContentLoaded', () => { }, }); - $(document).on('shown.bs.modal', (event) => { + $(document).on('shown.bs.modal', event => { if (event.relatedTarget.classList.contains('delete-user-button')) { const buttonProps = event.relatedTarget.dataset; deleteModal.deleteUserUrl = buttonProps.deleteUserUrl; deleteModal.blockUserUrl = buttonProps.blockUserUrl; - deleteModal.deleteContributions = event.relatedTarget.hasAttribute('data-delete-contributions'); + deleteModal.deleteContributions = event.relatedTarget.hasAttribute( + 'data-delete-contributions', + ); deleteModal.username = buttonProps.username; } }); diff --git a/app/assets/javascripts/pages/admin/users/new/index.js b/app/assets/javascripts/pages/admin/users/new/index.js index 58bfa8d64e7..3e6a090cb0e 100644 --- a/app/assets/javascripts/pages/admin/users/new/index.js +++ b/app/assets/javascripts/pages/admin/users/new/index.js @@ -4,7 +4,9 @@ export default class UserInternalRegexHandler { constructor() { this.regexPattern = $('[data-user-internal-regex-pattern]').data('user-internal-regex-pattern'); if (this.regexPattern && this.regexPattern !== '') { - this.regexOptions = $('[data-user-internal-regex-options]').data('user-internal-regex-options'); + this.regexOptions = $('[data-user-internal-regex-options]').data( + 'user-internal-regex-options', + ); this.external = $('#user_external'); this.warningMessage = $('#warning_external_automatically_set'); this.addListenerToEmailField(); @@ -13,7 +15,7 @@ export default class UserInternalRegexHandler { } addListenerToEmailField() { - $('#user_email').on('input', (event) => { + $('#user_email').on('input', event => { this.setExternalCheckbox(event.currentTarget.value); }); } diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js index 72f3f70b98f..1b56b97f751 100644 --- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js +++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js @@ -79,7 +79,8 @@ export default class Todos { .then(({ data }) => { this.updateRowState(target); this.updateBadges(data); - }).catch(() => { + }) + .catch(() => { this.updateRowState(target, true); return flash(__('Error updating todo status.')); }); @@ -118,10 +119,12 @@ export default class Todos { axios[target.dataset.method](target.dataset.href, { ids: this.todo_ids, - }).then(({ data }) => { - this.updateAllState(target, data); - this.updateBadges(data); - }).catch(() => flash(__('Error updating status for all todos.'))); + }) + .then(({ data }) => { + this.updateAllState(target, data); + this.updateBadges(data); + }) + .catch(() => flash(__('Error updating status for all todos.'))); } updateAllState(target, data) { @@ -133,7 +136,7 @@ export default class Todos { target.removeAttribute('disabled'); target.classList.remove('disabled'); - this.todo_ids = (target === markAllDoneBtn) ? data.updated_ids : []; + this.todo_ids = target === markAllDoneBtn ? data.updated_ids : []; undoAllBtn.classList.toggle('hidden'); markAllDoneBtn.classList.toggle('hidden'); todoListContainer.classList.toggle('hidden'); diff --git a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js index d3b2656743d..ae0a8c74964 100644 --- a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js +++ b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js @@ -9,7 +9,7 @@ document.addEventListener('DOMContentLoaded', () => { // eslint-disable-next-line no-new new AjaxVariableList({ container: variableListEl, - saveButton: variableListEl.querySelector('.js-secret-variables-save-button'), + saveButton: variableListEl.querySelector('.js-ci-variables-save-button'), errorBox: variableListEl.querySelector('.js-ci-variable-error-box'), saveEndpoint: variableListEl.dataset.saveEndpoint, }); diff --git a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue index 48668562f09..a4778077bc4 100644 --- a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue +++ b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue @@ -1,94 +1,117 @@ <script> - import axios from '~/lib/utils/axios_utils'; +import axios from '~/lib/utils/axios_utils'; - import Flash from '~/flash'; - import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; - import { n__, s__, sprintf } from '~/locale'; - import { redirectTo } from '~/lib/utils/url_utility'; - import eventHub from '../event_hub'; +import Flash from '~/flash'; +import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; +import { n__, s__, sprintf } from '~/locale'; +import { redirectTo } from '~/lib/utils/url_utility'; +import eventHub from '../event_hub'; - export default { - components: { - DeprecatedModal, +export default { + components: { + DeprecatedModal, + }, + props: { + issueCount: { + type: Number, + required: true, }, - props: { - issueCount: { - type: Number, - required: true, - }, - mergeRequestCount: { - type: Number, - required: true, - }, - milestoneId: { - type: Number, - required: true, - }, - milestoneTitle: { - type: String, - required: true, - }, - milestoneUrl: { - type: String, - required: true, - }, + mergeRequestCount: { + type: Number, + required: true, }, - computed: { - text() { - const milestoneTitle = sprintf('<strong>%{milestoneTitle}</strong>', { milestoneTitle: this.milestoneTitle }); - - if (this.issueCount === 0 && this.mergeRequestCount === 0) { - return sprintf( - s__(`Milestones| -You’re about to permanently delete the milestone %{milestoneTitle}. -This milestone is not currently used in any issues or merge requests.`), - { - milestoneTitle, - }, - false, - ); - } + milestoneId: { + type: Number, + required: true, + }, + milestoneTitle: { + type: String, + required: true, + }, + milestoneUrl: { + type: String, + required: true, + }, + }, + computed: { + text() { + const milestoneTitle = sprintf('<strong>%{milestoneTitle}</strong>', { + milestoneTitle: this.milestoneTitle, + }); + if (this.issueCount === 0 && this.mergeRequestCount === 0) { return sprintf( s__(`Milestones| -You’re about to permanently delete the milestone %{milestoneTitle} and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}. -Once deleted, it cannot be undone or recovered.`), +You’re about to permanently delete the milestone %{milestoneTitle}. +This milestone is not currently used in any issues or merge requests.`), { milestoneTitle, - issuesWithCount: n__('%d issue', '%d issues', this.issueCount), - mergeRequestsWithCount: n__('%d merge request', '%d merge requests', this.mergeRequestCount), }, false, ); - }, - title() { - return sprintf(s__('Milestones|Delete milestone %{milestoneTitle}?'), { milestoneTitle: this.milestoneTitle }); - }, - }, - methods: { - onSubmit() { - eventHub.$emit('deleteMilestoneModal.requestStarted', this.milestoneUrl); + } - return axios.delete(this.milestoneUrl) - .then((response) => { - eventHub.$emit('deleteMilestoneModal.requestFinished', { milestoneUrl: this.milestoneUrl, successful: true }); + return sprintf( + s__(`Milestones| +You’re about to permanently delete the milestone %{milestoneTitle} and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}. +Once deleted, it cannot be undone or recovered.`), + { + milestoneTitle, + issuesWithCount: n__('%d issue', '%d issues', this.issueCount), + mergeRequestsWithCount: n__( + '%d merge request', + '%d merge requests', + this.mergeRequestCount, + ), + }, + false, + ); + }, + title() { + return sprintf(s__('Milestones|Delete milestone %{milestoneTitle}?'), { + milestoneTitle: this.milestoneTitle, + }); + }, + }, + methods: { + onSubmit() { + eventHub.$emit('deleteMilestoneModal.requestStarted', this.milestoneUrl); - // follow the rediect to milestones overview page - redirectTo(response.request.responseURL); - }) - .catch((error) => { - eventHub.$emit('deleteMilestoneModal.requestFinished', { milestoneUrl: this.milestoneUrl, successful: false }); + return axios + .delete(this.milestoneUrl) + .then(response => { + eventHub.$emit('deleteMilestoneModal.requestFinished', { + milestoneUrl: this.milestoneUrl, + successful: true, + }); - if (error.response && error.response.status === 404) { - Flash(sprintf(s__('Milestones|Milestone %{milestoneTitle} was not found'), { milestoneTitle: this.milestoneTitle })); - } else { - Flash(sprintf(s__('Milestones|Failed to delete milestone %{milestoneTitle}'), { milestoneTitle: this.milestoneTitle })); - } - throw error; + // follow the rediect to milestones overview page + redirectTo(response.request.responseURL); + }) + .catch(error => { + eventHub.$emit('deleteMilestoneModal.requestFinished', { + milestoneUrl: this.milestoneUrl, + successful: false, }); - }, + + if (error.response && error.response.status === 404) { + Flash( + sprintf(s__('Milestones|Milestone %{milestoneTitle} was not found'), { + milestoneTitle: this.milestoneTitle, + }), + ); + } else { + Flash( + sprintf(s__('Milestones|Failed to delete milestone %{milestoneTitle}'), { + milestoneTitle: this.milestoneTitle, + }), + ); + } + throw error; + }); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue index 2c683a39f42..9d19e4a095d 100644 --- a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue +++ b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue @@ -1,54 +1,66 @@ <script> - import axios from '~/lib/utils/axios_utils'; - import createFlash from '~/flash'; - import GlModal from '~/vue_shared/components/gl_modal.vue'; - import { s__, sprintf } from '~/locale'; - import { visitUrl } from '~/lib/utils/url_utility'; - import eventHub from '../event_hub'; +import axios from '~/lib/utils/axios_utils'; +import createFlash from '~/flash'; +import GlModal from '~/vue_shared/components/gl_modal.vue'; +import { s__, sprintf } from '~/locale'; +import { visitUrl } from '~/lib/utils/url_utility'; +import eventHub from '../event_hub'; - export default { - components: { - GlModal, +export default { + components: { + GlModal, + }, + props: { + milestoneTitle: { + type: String, + required: true, }, - props: { - milestoneTitle: { - type: String, - required: true, - }, - url: { - type: String, - required: true, - }, - groupName: { - type: String, - required: true, - }, + url: { + type: String, + required: true, }, - computed: { - title() { - return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), { milestoneTitle: this.milestoneTitle }); - }, - text() { - return sprintf(s__(`Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}. + groupName: { + type: String, + required: true, + }, + }, + computed: { + title() { + return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), { + milestoneTitle: this.milestoneTitle, + }); + }, + text() { + return sprintf( + s__(`Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}. Existing project milestones with the same title will be merged. - This action cannot be reversed.`), { milestoneTitle: this.milestoneTitle, groupName: this.groupName }); - }, + This action cannot be reversed.`), + { milestoneTitle: this.milestoneTitle, groupName: this.groupName }, + ); }, - methods: { - onSubmit() { - eventHub.$emit('promoteMilestoneModal.requestStarted', this.url); - return axios.post(this.url, { params: { format: 'json' } }) - .then((response) => { - eventHub.$emit('promoteMilestoneModal.requestFinished', { milestoneUrl: this.url, successful: true }); - visitUrl(response.data.url); - }) - .catch((error) => { - eventHub.$emit('promoteMilestoneModal.requestFinished', { milestoneUrl: this.url, successful: false }); - createFlash(error); + }, + methods: { + onSubmit() { + eventHub.$emit('promoteMilestoneModal.requestStarted', this.url); + return axios + .post(this.url, { params: { format: 'json' } }) + .then(response => { + eventHub.$emit('promoteMilestoneModal.requestFinished', { + milestoneUrl: this.url, + successful: true, }); - }, + visitUrl(response.data.url); + }) + .catch(error => { + eventHub.$emit('promoteMilestoneModal.requestFinished', { + milestoneUrl: this.url, + successful: false, + }); + createFlash(error); + }); }, - }; + }, +}; </script> <template> <gl-modal @@ -65,4 +77,3 @@ {{ text }} </gl-modal> </template> - diff --git a/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js b/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js index d51b5c221e3..1d559dc6e41 100644 --- a/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js +++ b/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js @@ -7,7 +7,9 @@ export default () => { Vue.use(Translate); const onRequestFinished = ({ milestoneUrl, successful }) => { - const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`); + const button = document.querySelector( + `.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`, + ); if (!successful) { button.removeAttribute('disabled'); @@ -16,14 +18,16 @@ export default () => { button.querySelector('.js-loading-icon').classList.add('hidden'); }; - const onRequestStarted = (milestoneUrl) => { - const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`); + const onRequestStarted = milestoneUrl => { + const button = document.querySelector( + `.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`, + ); button.setAttribute('disabled', ''); button.querySelector('.js-loading-icon').classList.remove('hidden'); eventHub.$once('deleteMilestoneModal.requestFinished', onRequestFinished); }; - const onDeleteButtonClick = (event) => { + const onDeleteButtonClick = event => { const button = event.currentTarget; const modalProps = { milestoneId: parseInt(button.dataset.milestoneId, 10), @@ -37,12 +41,12 @@ export default () => { }; const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button'); - deleteMilestoneButtons.forEach((button) => { + deleteMilestoneButtons.forEach(button => { button.addEventListener('click', onDeleteButtonClick); }); eventHub.$once('deleteMilestoneModal.mounted', () => { - deleteMilestoneButtons.forEach((button) => { + deleteMilestoneButtons.forEach(button => { button.removeAttribute('disabled'); }); }); diff --git a/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js b/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js index 8e79341e96a..fcc62a2b2af 100644 --- a/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js +++ b/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js @@ -7,20 +7,24 @@ Vue.use(Translate); export default () => { const onRequestFinished = ({ milestoneUrl, successful }) => { - const button = document.querySelector(`.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`); + const button = document.querySelector( + `.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`, + ); if (!successful) { button.removeAttribute('disabled'); } }; - const onRequestStarted = (milestoneUrl) => { - const button = document.querySelector(`.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`); + const onRequestStarted = milestoneUrl => { + const button = document.querySelector( + `.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`, + ); button.setAttribute('disabled', ''); eventHub.$once('promoteMilestoneModal.requestFinished', onRequestFinished); }; - const onDeleteButtonClick = (event) => { + const onDeleteButtonClick = event => { const button = event.currentTarget; const modalProps = { milestoneTitle: button.dataset.milestoneTitle, @@ -32,12 +36,12 @@ export default () => { }; const promoteMilestoneButtons = document.querySelectorAll('.js-promote-project-milestone-button'); - promoteMilestoneButtons.forEach((button) => { + promoteMilestoneButtons.forEach(button => { button.addEventListener('click', onDeleteButtonClick); }); eventHub.$once('promoteMilestoneModal.mounted', () => { - promoteMilestoneButtons.forEach((button) => { + promoteMilestoneButtons.forEach(button => { button.removeAttribute('disabled'); }); }); diff --git a/app/assets/javascripts/pages/profiles/index.js b/app/assets/javascripts/pages/profiles/index.js index 04e50963699..883be18b336 100644 --- a/app/assets/javascripts/pages/profiles/index.js +++ b/app/assets/javascripts/pages/profiles/index.js @@ -3,9 +3,12 @@ import '~/profile/gl_crop'; import Profile from '~/profile/profile'; document.addEventListener('DOMContentLoaded', () => { - $(document).on('input.ssh_key', '#key_key', function () { // eslint-disable-line func-names + // eslint-disable-next-line func-names + $(document).on('input.ssh_key', '#key_key', function() { const $title = $('#key_title'); - const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/); + const comment = $(this) + .val() + .match(/^\S+ \S+ (.+)\n?$/); // Extract the SSH Key title from its comment if (comment && comment.length > 1) { diff --git a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js index 8e8f47c21d8..417935e2ad0 100644 --- a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js +++ b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js @@ -5,7 +5,9 @@ document.addEventListener('DOMContentLoaded', () => { const twoFactorNode = document.querySelector('.js-two-factor-auth'); const skippable = twoFactorNode.dataset.twoFactorSkippable === 'true'; if (skippable) { - const button = `<a class="btn btn-sm btn-warning float-right" data-method="patch" href="${twoFactorNode.dataset.two_factor_skip_url}">Configure it later</a>`; + const button = `<a class="btn btn-sm btn-warning float-right" data-method="patch" href="${ + twoFactorNode.dataset.two_factor_skip_url + }">Configure it later</a>`; const flashAlert = document.querySelector('.flash-alert .container-fluid'); if (flashAlert) flashAlert.insertAdjacentHTML('beforeend', button); } diff --git a/app/assets/javascripts/pages/projects/branches/new/index.js b/app/assets/javascripts/pages/projects/branches/new/index.js index a9658fd1eb4..13ff47d53c2 100644 --- a/app/assets/javascripts/pages/projects/branches/new/index.js +++ b/app/assets/javascripts/pages/projects/branches/new/index.js @@ -1,6 +1,11 @@ import $ from 'jquery'; import NewBranchForm from '~/new_branch_form'; -document.addEventListener('DOMContentLoaded', () => ( - new NewBranchForm($('.js-create-branch-form'), JSON.parse(document.getElementById('availableRefs').innerHTML)) -)); +document.addEventListener( + 'DOMContentLoaded', + () => + new NewBranchForm( + $('.js-create-branch-form'), + JSON.parse(document.getElementById('availableRefs').innerHTML), + ), +); diff --git a/app/assets/javascripts/pages/projects/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js index 80159a82bd4..3ccad513c05 100644 --- a/app/assets/javascripts/pages/projects/graphs/charts/index.js +++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js @@ -31,14 +31,16 @@ document.addEventListener('DOMContentLoaded', () => { const chartData = data => ({ labels: Object.keys(data), - datasets: [{ - fillColor: 'rgba(220,220,220,0.5)', - strokeColor: 'rgba(220,220,220,1)', - barStrokeWidth: 1, - barValueSpacing: 1, - barDatasetSpacing: 1, - data: _.values(data), - }], + datasets: [ + { + fillColor: 'rgba(220,220,220,0.5)', + strokeColor: 'rgba(220,220,220,1)', + barStrokeWidth: 1, + barValueSpacing: 1, + barDatasetSpacing: 1, + data: _.values(data), + }, + ], }); const hourData = chartData(projectChartData.hour); @@ -51,7 +53,9 @@ document.addEventListener('DOMContentLoaded', () => { responsiveChart($('#month-chart'), monthData); const data = projectChartData.languages; - const ctx = $('#languages-chart').get(0).getContext('2d'); + const ctx = $('#languages-chart') + .get(0) + .getContext('2d'); const options = { scaleOverlay: true, responsive: true, diff --git a/app/assets/javascripts/pages/projects/graphs/show/index.js b/app/assets/javascripts/pages/projects/graphs/show/index.js index 71f629fbc13..f79c386b59e 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/index.js +++ b/app/assets/javascripts/pages/projects/graphs/show/index.js @@ -7,7 +7,8 @@ import ContributorsStatGraph from './stat_graph_contributors'; document.addEventListener('DOMContentLoaded', () => { const url = document.querySelector('.js-graphs-show').dataset.projectGraphPath; - axios.get(url) + axios + .get(url) .then(({ data }) => { const graph = new ContributorsStatGraph(); graph.init(data); 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 58bb8c5b0c8..76613394af6 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 @@ -3,7 +3,11 @@ import $ from 'jquery'; import _ from 'underscore'; import { n__, s__, createDateTimeFormat, sprintf } from '~/locale'; -import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph'; +import { + ContributorsGraph, + ContributorsAuthorGraph, + ContributorsMasterGraph, +} from './stat_graph_contributors_graph'; import ContributorsStatGraphUtil from './stat_graph_contributors_util'; export default (function() { @@ -14,7 +18,7 @@ export default (function() { ContributorsStatGraph.prototype.init = function(log) { var author_commits, total_commits; this.parsed_log = ContributorsStatGraphUtil.parse_log(log); - this.set_current_field("commits"); + this.set_current_field('commits'); total_commits = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field); author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field); this.add_master_graph(total_commits); @@ -31,23 +35,26 @@ export default (function() { var limited_author_data; this.authors = []; limited_author_data = author_data.slice(0, 100); - return _.each(limited_author_data, (function(_this) { - return function(d) { - var author_graph, author_header; - author_header = _this.create_author_header(d); - $(".contributors-list").append(author_header); - - author_graph = new ContributorsAuthorGraph(d.dates); - _this.authors[d.author_name] = author_graph; - return author_graph.draw(); - }; - })(this)); + return _.each( + limited_author_data, + (function(_this) { + return function(d) { + var author_graph, author_header; + author_header = _this.create_author_header(d); + $('.contributors-list').append(author_header); + + author_graph = new ContributorsAuthorGraph(d.dates); + _this.authors[d.author_name] = author_graph; + return author_graph.draw(); + }; + })(this), + ); }; ContributorsStatGraph.prototype.format_author_commit_info = function(author) { var commits; commits = $('<span/>', { - "class": 'graph-author-commits-count' + class: 'graph-author-commits-count', }); commits.text(n__('%d commit', '%d commits', author.commits)); return $('<span/>').append(commits); @@ -56,13 +63,13 @@ export default (function() { ContributorsStatGraph.prototype.create_author_header = function(author) { var author_commit_info, author_commit_info_span, author_email, author_name, list_item; list_item = $('<li/>', { - "class": 'person', - style: 'display: block;' + class: 'person', + style: 'display: block;', }); author_name = $('<h4>' + author.author_name + '</h4>'); author_email = $('<p class="graph-author-email">' + author.author_email + '</p>'); author_commit_info_span = $('<span/>', { - "class": 'commits' + class: 'commits', }); author_commit_info = this.format_author_commit_info(author); author_commit_info_span.html(author_commit_info); @@ -80,37 +87,41 @@ export default (function() { }; ContributorsStatGraph.prototype.redraw_authors = function() { - $("ol").html(""); + $('ol').html(''); 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); - if (_this.authors[d.author_name] != null) { - $(_this.authors[d.author_name].list_item).appendTo("ol"); - _this.authors[d.author_name].set_data(d.dates); - return _this.authors[d.author_name].redraw(); - } - return ''; - }; - })(this)); + 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); + if (_this.authors[d.author_name] != null) { + $(_this.authors[d.author_name].list_item).appendTo('ol'); + _this.authors[d.author_name].set_data(d.dates); + return _this.authors[d.author_name].redraw(); + } + return ''; + }; + })(this), + ); }; ContributorsStatGraph.prototype.set_current_field = function(field) { - return this.field = field; + return (this.field = field); }; ContributorsStatGraph.prototype.change_date_header = function() { const { x_domain } = ContributorsGraph.prototype; - const formattedDateRange = sprintf( - s__('ContributorsPage|%{startDate} – %{endDate}'), - { - startDate: this.dateFormat.format(new Date(x_domain[0])), - endDate: this.dateFormat.format(new Date(x_domain[1])), - }, - ); + const formattedDateRange = sprintf(s__('ContributorsPage|%{startDate} – %{endDate}'), { + startDate: this.dateFormat.format(new Date(x_domain[0])), + endDate: this.dateFormat.format(new Date(x_domain[1])), + }); return $('#date_header').text(formattedDateRange); }; @@ -120,7 +131,7 @@ export default (function() { if ($author != null) { author_list_item = $(this.authors[author.author_name].list_item); author_commit_info = this.format_author_commit_info(author); - return author_list_item.find("span").html(author_commit_info); + return author_list_item.find('span').html(author_commit_info); } return ''; }; diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js index 5f91686347a..377dce6c746 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js @@ -11,10 +11,32 @@ import { brushX } from 'd3-brush'; import { timeParse } from 'd3-time-format'; import { dateTickFormat } from '~/lib/utils/tick_formats'; -const d3 = { extent, max, select, scaleTime, scaleLinear, axisLeft, axisBottom, area, brushX, timeParse }; +const d3 = { + extent, + max, + select, + scaleTime, + scaleLinear, + axisLeft, + axisBottom, + area, + brushX, + timeParse, +}; const hasProp = {}.hasOwnProperty; -const extend = function(child, parent) { for (const key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; +const extend = function(child, parent) { + for (const key in parent) { + if (hasProp.call(parent, key)) child[key] = parent[key]; + } + function ctor() { + this.constructor = child; + } + ctor.prototype = parent.prototype; + child.prototype = new ctor(); + child.__super__ = parent.prototype; + return child; +}; export const ContributorsGraph = (function() { function ContributorsGraph() {} @@ -23,7 +45,7 @@ export const ContributorsGraph = (function() { top: 20, right: 10, bottom: 30, - left: 40 + left: 40, }; ContributorsGraph.prototype.x_domain = null; @@ -33,35 +55,39 @@ export const ContributorsGraph = (function() { ContributorsGraph.prototype.dates = []; ContributorsGraph.prototype.determine_width = function(baseWidth, $parentElement) { - const parentPaddingWidth = parseFloat($parentElement.css('padding-left')) + parseFloat($parentElement.css('padding-right')); + const parentPaddingWidth = + parseFloat($parentElement.css('padding-left')) + + parseFloat($parentElement.css('padding-right')); const marginWidth = this.MARGIN.left + this.MARGIN.right; return baseWidth - parentPaddingWidth - marginWidth; }; ContributorsGraph.set_x_domain = function(data) { - return ContributorsGraph.prototype.x_domain = data; + return (ContributorsGraph.prototype.x_domain = data); }; ContributorsGraph.set_y_domain = function(data) { - return ContributorsGraph.prototype.y_domain = [ - 0, d3.max(data, function(d) { - return d.commits = d.commits || d.additions || d.deletions; - }) - ]; + return (ContributorsGraph.prototype.y_domain = [ + 0, + d3.max(data, function(d) { + return (d.commits = d.commits || d.additions || d.deletions); + }), + ]); }; ContributorsGraph.init_x_domain = function(data) { - return ContributorsGraph.prototype.x_domain = d3.extent(data, function(d) { + return (ContributorsGraph.prototype.x_domain = d3.extent(data, function(d) { return d.date; - }); + })); }; ContributorsGraph.init_y_domain = function(data) { - return ContributorsGraph.prototype.y_domain = [ - 0, d3.max(data, function(d) { - return d.commits = d.commits || d.additions || d.deletions; - }) - ]; + return (ContributorsGraph.prototype.y_domain = [ + 0, + d3.max(data, function(d) { + return (d.commits = d.commits || d.additions || d.deletions); + }), + ]); }; ContributorsGraph.init_domain = function(data) { @@ -70,7 +96,7 @@ export const ContributorsGraph = (function() { }; ContributorsGraph.set_dates = function(data) { - return ContributorsGraph.prototype.dates = data; + return (ContributorsGraph.prototype.dates = data); }; ContributorsGraph.prototype.set_x_domain = function() { @@ -87,20 +113,33 @@ export const ContributorsGraph = (function() { }; ContributorsGraph.prototype.create_scale = function(width, height) { - this.x = d3.scaleTime().range([0, width]).clamp(true); - return this.y = d3.scaleLinear().range([height, 0]).nice(); + this.x = d3 + .scaleTime() + .range([0, width]) + .clamp(true); + return (this.y = d3 + .scaleLinear() + .range([height, 0]) + .nice()); }; ContributorsGraph.prototype.draw_x_axis = function() { - return this.svg.append("g").attr("class", "x axis").attr("transform", "translate(0, " + this.height + ")").call(this.x_axis); + return this.svg + .append('g') + .attr('class', 'x axis') + .attr('transform', 'translate(0, ' + this.height + ')') + .call(this.x_axis); }; ContributorsGraph.prototype.draw_y_axis = function() { - return this.svg.append("g").attr("class", "y axis").call(this.y_axis); + return this.svg + .append('g') + .attr('class', 'y axis') + .call(this.y_axis); }; ContributorsGraph.prototype.set_data = function(data) { - return this.data = data; + return (this.data = data); }; return ContributorsGraph; @@ -137,9 +176,9 @@ export const ContributorsMasterGraph = (function(superClass) { }; ContributorsMasterGraph.prototype.parse_dates = function(data) { - const parseDate = d3.timeParse("%Y-%m-%d"); + const parseDate = d3.timeParse('%Y-%m-%d'); return data.forEach(function(d) { - return d.date = parseDate(d.date); + return (d.date = parseDate(d.date)); }); }; @@ -148,42 +187,63 @@ export const ContributorsMasterGraph = (function(superClass) { }; ContributorsMasterGraph.prototype.create_axes = function() { - this.x_axis = d3.axisBottom() + this.x_axis = d3 + .axisBottom() .scale(this.x) .tickFormat(dateTickFormat); - return this.y_axis = d3.axisLeft().scale(this.y).ticks(5); + return (this.y_axis = d3 + .axisLeft() + .scale(this.y) + .ticks(5)); }; ContributorsMasterGraph.prototype.create_svg = function() { - this.svg = d3.select("#contributors-master") - .append("svg") - .attr("width", this.width + this.MARGIN.left + this.MARGIN.right) - .attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom) - .attr("class", "tint-box") - .append("g") - .attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + this.svg = d3 + .select('#contributors-master') + .append('svg') + .attr('width', this.width + this.MARGIN.left + this.MARGIN.right) + .attr('height', this.height + this.MARGIN.top + this.MARGIN.bottom) + .attr('class', 'tint-box') + .append('g') + .attr('transform', 'translate(' + this.MARGIN.left + ',' + this.MARGIN.top + ')'); return this.svg; }; ContributorsMasterGraph.prototype.create_area = function(x, y) { - return this.area = d3.area().x(function(d) { - return x(d.date); - }).y0(this.height).y1(function(d) { - d.commits = d.commits || d.additions || d.deletions; - return y(d.commits); - }); + return (this.area = d3 + .area() + .x(function(d) { + return x(d.date); + }) + .y0(this.height) + .y1(function(d) { + d.commits = d.commits || d.additions || d.deletions; + return y(d.commits); + })); }; ContributorsMasterGraph.prototype.create_brush = function() { - return this.brush = d3.brushX(this.x).extent([[this.x.range()[0], 0], [this.x.range()[1], this.height]]).on("end", this.update_content); + return (this.brush = d3 + .brushX(this.x) + .extent([[this.x.range()[0], 0], [this.x.range()[1], this.height]]) + .on('end', this.update_content)); }; ContributorsMasterGraph.prototype.draw_path = function(data) { - return this.svg.append("path").datum(data).attr("class", "area").attr("d", this.area); + return this.svg + .append('path') + .datum(data) + .attr('class', 'area') + .attr('d', this.area); }; ContributorsMasterGraph.prototype.add_brush = function() { - return this.svg.append("g").attr("class", "selection").call(this.brush).selectAll("rect").attr("height", this.height); + return this.svg + .append('g') + .attr('class', 'selection') + .call(this.brush) + .selectAll('rect') + .attr('height', this.height); }; ContributorsMasterGraph.prototype.update_content = function() { @@ -193,7 +253,7 @@ export const ContributorsMasterGraph = (function(superClass) { } else { ContributorsGraph.set_x_domain(this.x_max_domain); } - return $("#brush_change").trigger('change'); + return $('#brush_change').trigger('change'); }; ContributorsMasterGraph.prototype.draw = function() { @@ -216,9 +276,9 @@ export const ContributorsMasterGraph = (function(superClass) { this.process_dates(this.data); ContributorsGraph.set_y_domain(this.data); this.set_y_domain(); - this.svg.select("path").datum(this.data); - this.svg.select("path").attr("d", this.area); - return this.svg.select(".y.axis").call(this.y_axis); + this.svg.select('path').datum(this.data); + this.svg.select('path').attr('d', this.area); + return this.svg.select('.y.axis').call(this.y_axis); }; return ContributorsMasterGraph; @@ -252,43 +312,58 @@ export const ContributorsAuthorGraph = (function(superClass) { }; ContributorsAuthorGraph.prototype.create_axes = function() { - this.x_axis = d3.axisBottom() + this.x_axis = d3 + .axisBottom() .scale(this.x) .ticks(8) .tickFormat(dateTickFormat); - return this.y_axis = d3.axisLeft().scale(this.y).ticks(5); + return (this.y_axis = d3 + .axisLeft() + .scale(this.y) + .ticks(5)); }; ContributorsAuthorGraph.prototype.create_area = function(x, y) { - return this.area = d3.area().x(function(d) { - const parseDate = d3.timeParse("%Y-%m-%d"); - return x(parseDate(d)); - }).y0(this.height).y1((function(_this) { - return function(d) { - if (_this.data[d] != null) { - return y(_this.data[d]); - } else { - return y(0); - } - }; - })(this)); + return (this.area = d3 + .area() + .x(function(d) { + const parseDate = d3.timeParse('%Y-%m-%d'); + return x(parseDate(d)); + }) + .y0(this.height) + .y1( + (function(_this) { + return function(d) { + if (_this.data[d] != null) { + return y(_this.data[d]); + } else { + return y(0); + } + }; + })(this), + )); }; ContributorsAuthorGraph.prototype.create_svg = function() { const persons = document.querySelectorAll('.person'); this.list_item = persons[persons.length - 1]; - this.svg = d3.select(this.list_item) - .append("svg") - .attr("width", this.width + this.MARGIN.left + this.MARGIN.right) - .attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom) - .attr("class", "spark") - .append("g") - .attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + this.svg = d3 + .select(this.list_item) + .append('svg') + .attr('width', this.width + this.MARGIN.left + this.MARGIN.right) + .attr('height', this.height + this.MARGIN.top + this.MARGIN.bottom) + .attr('class', 'spark') + .append('g') + .attr('transform', 'translate(' + this.MARGIN.left + ',' + this.MARGIN.top + ')'); return this.svg; }; ContributorsAuthorGraph.prototype.draw_path = function(data) { - return this.svg.append("path").datum(data).attr("class", "area-contributor").attr("d", this.area); + return this.svg + .append('path') + .datum(data) + .attr('class', 'area-contributor') + .attr('d', this.area); }; ContributorsAuthorGraph.prototype.draw = function() { @@ -304,10 +379,10 @@ export const ContributorsAuthorGraph = (function(superClass) { ContributorsAuthorGraph.prototype.redraw = function() { this.set_domain(); - this.svg.select("path").datum(this.dates); - this.svg.select("path").attr("d", this.area); - this.svg.select(".x.axis").call(this.x_axis); - return this.svg.select(".y.axis").call(this.y_axis); + this.svg.select('path').datum(this.dates); + this.svg.select('path').attr('d', this.area); + this.svg.select('.x.axis').call(this.x_axis); + return this.svg.select('.y.axis').call(this.y_axis); }; return ContributorsAuthorGraph; diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js index cd0e2bc023c..988ae164955 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js @@ -26,12 +26,12 @@ export default { by_author = _.toArray(by_author); return { total: total, - by_author: by_author + by_author: by_author, }; }, add_date: function(date, collection) { collection[date] = {}; - return collection[date].date = date; + return (collection[date].date = date); }, add_author: function(author, by_author, by_email) { var data, normalized_email; @@ -49,28 +49,28 @@ export default { return this.store_deletions(entry, total, by_author); }, store_commits: function(total, by_author) { - this.add(total, "commits", 1); - return this.add(by_author, "commits", 1); + this.add(total, 'commits', 1); + return this.add(by_author, 'commits', 1); }, add: function(collection, field, value) { if (collection[field] == null) { collection[field] = 0; } - return collection[field] += value; + return (collection[field] += value); }, store_additions: function(entry, total, by_author) { if (entry.additions == null) { entry.additions = 0; } - this.add(total, "additions", entry.additions); - return this.add(by_author, "additions", entry.additions); + this.add(total, 'additions', entry.additions); + return this.add(by_author, 'additions', entry.additions); }, store_deletions: function(entry, total, by_author) { if (entry.deletions == null) { entry.deletions = 0; } - this.add(total, "deletions", entry.deletions); - return this.add(by_author, "deletions", entry.deletions); + this.add(total, 'deletions', entry.deletions); + return this.add(by_author, 'deletions', entry.deletions); }, get_total_data: function(parsed_log, field) { var log, total_data; @@ -95,15 +95,18 @@ export default { } log = parsed_log.by_author; author_data = []; - _.each(log, (function(_this) { - return function(log_entry) { - var parsed_log_entry; - parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range); - if (!_.isEmpty(parsed_log_entry.dates)) { - return author_data.push(parsed_log_entry); - } - }; - })(this)); + _.each( + log, + (function(_this) { + return function(log_entry) { + var parsed_log_entry; + parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range); + if (!_.isEmpty(parsed_log_entry.dates)) { + return author_data.push(parsed_log_entry); + } + }; + })(this), + ); return _.sortBy(author_data, function(d) { return d[field]; }).reverse(); @@ -120,16 +123,19 @@ export default { parsed_entry.additions = 0; parsed_entry.deletions = 0; - _.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) { - return function(value, key) { - if (_this.in_range(value.date, date_range)) { - parsed_entry.dates[value.date] = value[field]; - parsed_entry.commits += value.commits; - parsed_entry.additions += value.additions; - return parsed_entry.deletions += value.deletions; - } - }; - })(this)); + _.each( + _.omit(log_entry, 'author_name', 'author_email'), + (function(_this) { + return function(value, key) { + if (_this.in_range(value.date, date_range)) { + parsed_entry.dates[value.date] = value[field]; + parsed_entry.commits += value.commits; + parsed_entry.additions += value.additions; + return (parsed_entry.deletions += value.deletions); + } + }; + })(this), + ); return parsed_entry; }, in_range: function(date, date_range) { @@ -139,5 +145,5 @@ export default { } else { return false; } - } + }, }; diff --git a/app/assets/javascripts/pages/projects/init_blob.js b/app/assets/javascripts/pages/projects/init_blob.js index bc08ccf3584..bd8afa2d5ba 100644 --- a/app/assets/javascripts/pages/projects/init_blob.js +++ b/app/assets/javascripts/pages/projects/init_blob.js @@ -16,7 +16,8 @@ export default () => { ); const fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url'); - const fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href'); + const fileBlobPermalinkUrl = + fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href'); new ShortcutsNavigation(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/projects/init_form.js b/app/assets/javascripts/pages/projects/init_form.js index 9f20a3e4e46..019efe077f7 100644 --- a/app/assets/javascripts/pages/projects/init_form.js +++ b/app/assets/javascripts/pages/projects/init_form.js @@ -1,7 +1,7 @@ import ZenMode from '~/zen_mode'; import GLForm from '~/gl_form'; -export default function ($formEl) { +export default function($formEl) { new ZenMode(); // eslint-disable-line no-new new GLForm($formEl); // eslint-disable-line no-new } diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js index ef65196872c..8987c8e3f47 100644 --- a/app/assets/javascripts/pages/projects/issues/show.js +++ b/app/assets/javascripts/pages/projects/issues/show.js @@ -5,7 +5,7 @@ import ZenMode from '~/zen_mode'; import '~/notes/index'; import initIssueableApp from '~/issue_show'; -export default function () { +export default function() { initIssueableApp(); new Issue(); // eslint-disable-line no-new new ShortcutsIssuable(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/projects/jobs/index/index.js b/app/assets/javascripts/pages/projects/jobs/index/index.js new file mode 100644 index 00000000000..1b57c67f16b --- /dev/null +++ b/app/assets/javascripts/pages/projects/jobs/index/index.js @@ -0,0 +1,16 @@ +import Vue from 'vue'; +import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; + +document.addEventListener('DOMContentLoaded', () => { + const remainingTimeElements = document.querySelectorAll('.js-remaining-time'); + remainingTimeElements.forEach( + el => + new Vue({ + ...GlCountdown, + el, + propsData: { + endDateString: el.dateTime, + }, + }), + ); +}); diff --git a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue index 5d2247f6c6d..e8b646f3f6e 100644 --- a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue +++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue @@ -1,72 +1,86 @@ <script> - import _ from 'underscore'; - import axios from '~/lib/utils/axios_utils'; - import createFlash from '~/flash'; - import GlModal from '~/vue_shared/components/gl_modal.vue'; - import { s__, sprintf } from '~/locale'; - import { visitUrl } from '~/lib/utils/url_utility'; - import eventHub from '../event_hub'; +import _ from 'underscore'; +import axios from '~/lib/utils/axios_utils'; +import createFlash from '~/flash'; +import GlModal from '~/vue_shared/components/gl_modal.vue'; +import { s__, sprintf } from '~/locale'; +import { visitUrl } from '~/lib/utils/url_utility'; +import eventHub from '../event_hub'; - export default { - components: { - GlModal, +export default { + components: { + GlModal, + }, + props: { + url: { + type: String, + required: true, }, - props: { - url: { - type: String, - required: true, - }, - labelTitle: { - type: String, - required: true, - }, - labelColor: { - type: String, - required: true, - }, - labelTextColor: { - type: String, - required: true, - }, - groupName: { - type: String, - required: true, - }, + labelTitle: { + type: String, + required: true, }, - computed: { - text() { - return sprintf(s__(`Labels|Promoting %{labelTitle} will make it available for all projects inside %{groupName}. - Existing project labels with the same title will be merged. This action cannot be reversed.`), { + labelColor: { + type: String, + required: true, + }, + labelTextColor: { + type: String, + required: true, + }, + groupName: { + type: String, + required: true, + }, + }, + computed: { + text() { + return sprintf( + s__(`Labels|Promoting %{labelTitle} will make it available for all projects inside %{groupName}. + Existing project labels with the same title will be merged. This action cannot be reversed.`), + { labelTitle: this.labelTitle, groupName: this.groupName, - }); - }, - title() { - const label = `<span + }, + ); + }, + title() { + const label = `<span class="label color-label" style="background-color: ${this.labelColor}; color: ${this.labelTextColor};" >${_.escape(this.labelTitle)}</span>`; - return sprintf(s__('Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>'), { + return sprintf( + s__('Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>'), + { labelTitle: label, - }, false); - }, + }, + false, + ); }, - methods: { - onSubmit() { - eventHub.$emit('promoteLabelModal.requestStarted', this.url); - return axios.post(this.url, { params: { format: 'json' } }) - .then((response) => { - eventHub.$emit('promoteLabelModal.requestFinished', { labelUrl: this.url, successful: true }); - visitUrl(response.data.url); - }) - .catch((error) => { - eventHub.$emit('promoteLabelModal.requestFinished', { labelUrl: this.url, successful: false }); - createFlash(error); + }, + methods: { + onSubmit() { + eventHub.$emit('promoteLabelModal.requestStarted', this.url); + return axios + .post(this.url, { params: { format: 'json' } }) + .then(response => { + eventHub.$emit('promoteLabelModal.requestFinished', { + labelUrl: this.url, + successful: true, + }); + visitUrl(response.data.url); + }) + .catch(error => { + eventHub.$emit('promoteLabelModal.requestFinished', { + labelUrl: this.url, + successful: false, }); - }, + createFlash(error); + }); }, - }; + }, +}; </script> <template> <gl-modal diff --git a/app/assets/javascripts/pages/projects/labels/index/index.js b/app/assets/javascripts/pages/projects/labels/index/index.js index 03cfef61311..36cf485f33d 100644 --- a/app/assets/javascripts/pages/projects/labels/index/index.js +++ b/app/assets/javascripts/pages/projects/labels/index/index.js @@ -10,20 +10,24 @@ const initLabelIndex = () => { initLabels(); const onRequestFinished = ({ labelUrl, successful }) => { - const button = document.querySelector(`.js-promote-project-label-button[data-url="${labelUrl}"]`); + const button = document.querySelector( + `.js-promote-project-label-button[data-url="${labelUrl}"]`, + ); if (!successful) { button.removeAttribute('disabled'); } }; - const onRequestStarted = (labelUrl) => { - const button = document.querySelector(`.js-promote-project-label-button[data-url="${labelUrl}"]`); + const onRequestStarted = labelUrl => { + const button = document.querySelector( + `.js-promote-project-label-button[data-url="${labelUrl}"]`, + ); button.setAttribute('disabled', ''); eventHub.$once('promoteLabelModal.requestFinished', onRequestFinished); }; - const onDeleteButtonClick = (event) => { + const onDeleteButtonClick = event => { const button = event.currentTarget; const modalProps = { labelTitle: button.dataset.labelTitle, @@ -37,12 +41,12 @@ const initLabelIndex = () => { }; const promoteLabelButtons = document.querySelectorAll('.js-promote-project-label-button'); - promoteLabelButtons.forEach((button) => { + promoteLabelButtons.forEach(button => { button.addEventListener('click', onDeleteButtonClick); }); eventHub.$once('promoteLabelModal.mounted', () => { - promoteLabelButtons.forEach((button) => { + promoteLabelButtons.forEach(button => { button.removeAttribute('disabled'); }); }); diff --git a/app/assets/javascripts/pages/projects/network/network.js b/app/assets/javascripts/pages/projects/network/network.js index 70fbb3f301c..226d63f05c4 100644 --- a/app/assets/javascripts/pages/projects/network/network.js +++ b/app/assets/javascripts/pages/projects/network/network.js @@ -6,13 +6,15 @@ import BranchGraph from '../../../network/branch_graph'; export default (function() { function Network(opts) { var vph; - $("#filter_ref").click(function() { - return $(this).closest('form').submit(); + $('#filter_ref').click(function() { + return $(this) + .closest('form') + .submit(); }); - this.branch_graph = new BranchGraph($(".network-graph"), opts); + this.branch_graph = new BranchGraph($('.network-graph'), opts); vph = $(window).height() - 250; $('.network-graph').css({ - 'height': vph + 'px' + height: vph + 'px', }); } diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js index 544360dcd51..6197dc8a9db 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js @@ -1,12 +1,16 @@ import Vue from 'vue'; import PipelineSchedulesCallout from '../shared/components/pipeline_schedules_callout.vue'; -document.addEventListener('DOMContentLoaded', () => new Vue({ - el: '#pipeline-schedules-callout', - components: { - 'pipeline-schedules-callout': PipelineSchedulesCallout, - }, - render(createElement) { - return createElement('pipeline-schedules-callout'); - }, -})); +document.addEventListener( + 'DOMContentLoaded', + () => + new Vue({ + el: '#pipeline-schedules-callout', + components: { + 'pipeline-schedules-callout': PipelineSchedulesCallout, + }, + render(createElement) { + return createElement('pipeline-schedules-callout'); + }, + }), +); diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue index ef53d67e7cb..ab6f42d928c 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue @@ -1,63 +1,63 @@ <script> - import _ from 'underscore'; +import _ from 'underscore'; - export default { - props: { - initialCronInterval: { - type: String, - required: false, - default: '', - }, - }, - data() { - return { - inputNameAttribute: 'schedule[cron]', - cronInterval: this.initialCronInterval, - cronIntervalPresets: { - everyDay: '0 4 * * *', - everyWeek: '0 4 * * 0', - everyMonth: '0 4 1 * *', - }, - cronSyntaxUrl: 'https://en.wikipedia.org/wiki/Cron', - customInputEnabled: false, - }; +export default { + props: { + initialCronInterval: { + type: String, + required: false, + default: '', }, - computed: { - intervalIsPreset() { - return _.contains(this.cronIntervalPresets, this.cronInterval); - }, - // The text input is editable when there's a custom interval, or when it's - // a preset interval and the user clicks the 'custom' radio button - isEditable() { - return !!(this.customInputEnabled || !this.intervalIsPreset); + }, + data() { + return { + inputNameAttribute: 'schedule[cron]', + cronInterval: this.initialCronInterval, + cronIntervalPresets: { + everyDay: '0 4 * * *', + everyWeek: '0 4 * * 0', + everyMonth: '0 4 1 * *', }, + cronSyntaxUrl: 'https://en.wikipedia.org/wiki/Cron', + customInputEnabled: false, + }; + }, + computed: { + intervalIsPreset() { + return _.contains(this.cronIntervalPresets, this.cronInterval); }, - watch: { - cronInterval() { - // updates field validation state when model changes, as - // glFieldError only updates on input. - this.$nextTick(() => { - gl.pipelineScheduleFieldErrors.updateFormValidityState(); - }); - }, + // The text input is editable when there's a custom interval, or when it's + // a preset interval and the user clicks the 'custom' radio button + isEditable() { + return !!(this.customInputEnabled || !this.intervalIsPreset); }, - created() { - if (this.intervalIsPreset) { - this.enableCustomInput = false; - } + }, + watch: { + cronInterval() { + // updates field validation state when model changes, as + // glFieldError only updates on input. + this.$nextTick(() => { + gl.pipelineScheduleFieldErrors.updateFormValidityState(); + }); }, - methods: { - toggleCustomInput(shouldEnable) { - this.customInputEnabled = shouldEnable; + }, + created() { + if (this.intervalIsPreset) { + this.enableCustomInput = false; + } + }, + methods: { + toggleCustomInput(shouldEnable) { + this.customInputEnabled = shouldEnable; - if (shouldEnable) { - // We need to change the value so other radios don't remain selected - // because the model (cronInterval) hasn't changed. The server trims it. - this.cronInterval = `${this.cronInterval} `; - } - }, + if (shouldEnable) { + // We need to change the value so other radios don't remain selected + // because the model (cronInterval) hasn't changed. The server trims it. + this.cronInterval = `${this.cronInterval} `; + } }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue index 77508e62cef..33fc2420e4d 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue @@ -1,31 +1,31 @@ <script> - import Vue from 'vue'; - import Cookies from 'js-cookie'; - import Translate from '../../../../../vue_shared/translate'; - import illustrationSvg from '../icons/intro_illustration.svg'; +import Vue from 'vue'; +import Cookies from 'js-cookie'; +import Translate from '../../../../../vue_shared/translate'; +import illustrationSvg from '../icons/intro_illustration.svg'; - Vue.use(Translate); +Vue.use(Translate); - const cookieKey = 'pipeline_schedules_callout_dismissed'; +const cookieKey = 'pipeline_schedules_callout_dismissed'; - export default { - name: 'PipelineSchedulesCallout', - data() { - return { - docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl, - calloutDismissed: Cookies.get(cookieKey) === 'true', - }; +export default { + name: 'PipelineSchedulesCallout', + data() { + return { + docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl, + calloutDismissed: Cookies.get(cookieKey) === 'true', + }; + }, + created() { + this.illustrationSvg = illustrationSvg; + }, + methods: { + dismissCallout() { + this.calloutDismissed = true; + Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 }); }, - created() { - this.illustrationSvg = illustrationSvg; - }, - methods: { - dismissCallout() { - this.calloutDismissed = true; - Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 }); - }, - }, - }; + }, +}; </script> <template> <div diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js index 4ef0d11dd36..0057700c1b3 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js @@ -26,8 +26,7 @@ export default class TargetBranchDropdown { } formatBranchesList() { - return this.$dropdown.data('data') - .map(val => ({ name: val })); + return this.$dropdown.data('data').map(val => ({ name: val })); } setDropdownToggle() { diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js index c3ac54733a3..4d494efef6c 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js @@ -11,7 +11,9 @@ Vue.use(Translate); function initIntervalPatternInput() { const intervalPatternMount = document.getElementById('interval-pattern-input'); - const initialCronInterval = intervalPatternMount ? intervalPatternMount.dataset.initialInterval : ''; + const initialCronInterval = intervalPatternMount + ? intervalPatternMount.dataset.initialInterval + : ''; return new Vue({ el: intervalPatternMount, diff --git a/app/assets/javascripts/pages/projects/pipelines/charts/index.js b/app/assets/javascripts/pages/projects/pipelines/charts/index.js index 07b6992eba1..48353f3b4ef 100644 --- a/app/assets/javascripts/pages/projects/pipelines/charts/index.js +++ b/app/assets/javascripts/pages/projects/pipelines/charts/index.js @@ -7,26 +7,29 @@ const options = { maintainAspectRatio: false, }; -const buildChart = (chartScope) => { +const buildChart = chartScope => { const data = { labels: chartScope.labels, - datasets: [{ - fillColor: '#707070', - strokeColor: '#707070', - pointColor: '#707070', - pointStrokeColor: '#EEE', - data: chartScope.totalValues, - }, - { - fillColor: '#1aaa55', - strokeColor: '#1aaa55', - pointColor: '#1aaa55', - pointStrokeColor: '#fff', - data: chartScope.successValues, - }, + datasets: [ + { + fillColor: '#707070', + strokeColor: '#707070', + pointColor: '#707070', + pointStrokeColor: '#EEE', + data: chartScope.totalValues, + }, + { + fillColor: '#1aaa55', + strokeColor: '#1aaa55', + pointColor: '#1aaa55', + pointStrokeColor: '#fff', + data: chartScope.successValues, + }, ], }; - const ctx = $(`#${chartScope.scope}Chart`).get(0).getContext('2d'); + const ctx = $(`#${chartScope.scope}Chart`) + .get(0) + .getContext('2d'); new Chart(ctx).Line(data, options); }; @@ -36,14 +39,16 @@ document.addEventListener('DOMContentLoaded', () => { const chartsData = JSON.parse(document.getElementById('pipelinesChartsData').innerHTML); const data = { labels: chartTimesData.labels, - datasets: [{ - fillColor: 'rgba(220,220,220,0.5)', - strokeColor: 'rgba(220,220,220,1)', - barStrokeWidth: 1, - barValueSpacing: 1, - barDatasetSpacing: 1, - data: chartTimesData.values, - }], + datasets: [ + { + fillColor: 'rgba(220,220,220,0.5)', + strokeColor: 'rgba(220,220,220,1)', + barStrokeWidth: 1, + barValueSpacing: 1, + barDatasetSpacing: 1, + data: chartTimesData.values, + }, + ], }; if (window.innerWidth < 768) { @@ -51,7 +56,11 @@ document.addEventListener('DOMContentLoaded', () => { options.scaleFontSize = 8; } - new Chart($('#build_timesChart').get(0).getContext('2d')).Bar(data, options); + new Chart( + $('#build_timesChart') + .get(0) + .getContext('2d'), + ).Bar(data, options); chartsData.forEach(scope => buildChart(scope)); }); diff --git a/app/assets/javascripts/pages/projects/pipelines/index/index.js b/app/assets/javascripts/pages/projects/pipelines/index/index.js index a84e2790680..fc337a7609b 100644 --- a/app/assets/javascripts/pages/projects/pipelines/index/index.js +++ b/app/assets/javascripts/pages/projects/pipelines/index/index.js @@ -6,35 +6,39 @@ import { convertPermissionToBoolean } from '../../../../lib/utils/common_utils'; Vue.use(Translate); -document.addEventListener('DOMContentLoaded', () => new Vue({ - el: '#pipelines-list-vue', - components: { - pipelinesComponent, - }, - data() { - return { - store: new PipelinesStore(), - }; - }, - created() { - this.dataset = document.querySelector(this.$options.el).dataset; - }, - render(createElement) { - return createElement('pipelines-component', { - props: { - store: this.store, - endpoint: this.dataset.endpoint, - helpPagePath: this.dataset.helpPagePath, - emptyStateSvgPath: this.dataset.emptyStateSvgPath, - errorStateSvgPath: this.dataset.errorStateSvgPath, - noPipelinesSvgPath: this.dataset.noPipelinesSvgPath, - autoDevopsPath: this.dataset.helpAutoDevopsPath, - newPipelinePath: this.dataset.newPipelinePath, - canCreatePipeline: convertPermissionToBoolean(this.dataset.canCreatePipeline), - hasGitlabCi: convertPermissionToBoolean(this.dataset.hasGitlabCi), - ciLintPath: this.dataset.ciLintPath, - resetCachePath: this.dataset.resetCachePath, +document.addEventListener( + 'DOMContentLoaded', + () => + new Vue({ + el: '#pipelines-list-vue', + components: { + pipelinesComponent, }, - }); - }, -})); + data() { + return { + store: new PipelinesStore(), + }; + }, + created() { + this.dataset = document.querySelector(this.$options.el).dataset; + }, + render(createElement) { + return createElement('pipelines-component', { + props: { + store: this.store, + endpoint: this.dataset.endpoint, + helpPagePath: this.dataset.helpPagePath, + emptyStateSvgPath: this.dataset.emptyStateSvgPath, + errorStateSvgPath: this.dataset.errorStateSvgPath, + noPipelinesSvgPath: this.dataset.noPipelinesSvgPath, + autoDevopsPath: this.dataset.helpAutoDevopsPath, + newPipelinePath: this.dataset.newPipelinePath, + canCreatePipeline: convertPermissionToBoolean(this.dataset.canCreatePipeline), + hasGitlabCi: convertPermissionToBoolean(this.dataset.hasGitlabCi), + ciLintPath: this.dataset.ciLintPath, + resetCachePath: this.dataset.resetCachePath, + }, + }); + }, + }), +); diff --git a/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js b/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js index 94dfeb96e8c..ba4ae04ab3d 100644 --- a/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js +++ b/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js @@ -2,9 +2,12 @@ import Pipelines from '~/pipelines'; export default () => { const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; - const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`; + const pipelineStatusUrl = `${document + .querySelector('.js-pipeline-tab-link a') + .getAttribute('href')}/status.json`; - new Pipelines({ // eslint-disable-line no-new + // eslint-disable-next-line no-new + new Pipelines({ initTabs: true, pipelineStatusUrl, tabsOptions: { diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js index 52d66beefc9..a6bee49a6b1 100644 --- a/app/assets/javascripts/pages/projects/project.js +++ b/app/assets/javascripts/pages/projects/project.js @@ -64,7 +64,9 @@ export default class Project { const projectId = $(this).data('project-id'); const cookieKey = `hide_auto_devops_implicitly_enabled_banner_${projectId}`; Cookies.set(cookieKey, 'false'); - $(this).parents('.auto-devops-implicitly-enabled-banner').remove(); + $(this) + .parents('.auto-devops-implicitly-enabled-banner') + .remove(); return e.preventDefault(); }); Project.projectSelectDropdown(); 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 8f5ac3d8082..15c6fb550c1 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 @@ -18,7 +18,7 @@ document.addEventListener('DOMContentLoaded', () => { // eslint-disable-next-line no-new new AjaxVariableList({ container: variableListEl, - saveButton: variableListEl.querySelector('.js-secret-variables-save-button'), + saveButton: variableListEl.querySelector('.js-ci-variables-save-button'), errorBox: variableListEl.querySelector('.js-ci-variable-error-box'), saveEndpoint: variableListEl.dataset.saveEndpoint, }); diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue index 06101290f6c..dced839c883 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue @@ -1,73 +1,71 @@ <script> - import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue'; +import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue'; - export default { - components: { - projectFeatureToggle, - }, +export default { + components: { + projectFeatureToggle, + }, - model: { - prop: 'value', - event: 'change', - }, + model: { + prop: 'value', + event: 'change', + }, - props: { - name: { - type: String, - required: false, - default: '', - }, - options: { - type: Array, - required: false, - default: () => [], - }, - value: { - type: Number, - required: false, - default: 0, - }, - disabledInput: { - type: Boolean, - required: false, - default: false, - }, + props: { + name: { + type: String, + required: false, + default: '', + }, + options: { + type: Array, + required: false, + default: () => [], + }, + value: { + type: Number, + required: false, + default: 0, }, + disabledInput: { + type: Boolean, + required: false, + default: false, + }, + }, - computed: { - featureEnabled() { - return this.value !== 0; - }, + computed: { + featureEnabled() { + return this.value !== 0; + }, - displayOptions() { - if (this.featureEnabled) { - return this.options; - } - return [ - [0, 'Enable feature to choose access level'], - ]; - }, + displayOptions() { + if (this.featureEnabled) { + return this.options; + } + return [[0, 'Enable feature to choose access level']]; + }, - displaySelectInput() { - return this.disabledInput || !this.featureEnabled || this.displayOptions.length < 2; - }, + displaySelectInput() { + return this.disabledInput || !this.featureEnabled || this.displayOptions.length < 2; }, + }, - methods: { - toggleFeature(featureEnabled) { - if (featureEnabled === false || this.options.length < 1) { - this.$emit('change', 0); - } else { - const [firstOptionValue] = this.options[this.options.length - 1]; - this.$emit('change', firstOptionValue); - } - }, + methods: { + toggleFeature(featureEnabled) { + if (featureEnabled === false || this.options.length < 1) { + this.$emit('change', 0); + } else { + const [firstOptionValue] = this.options[this.options.length - 1]; + this.$emit('change', firstOptionValue); + } + }, - selectOption(e) { - this.$emit('change', Number(e.target.value)); - }, + selectOption(e) { + this.$emit('change', Number(e.target.value)); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue index 83437363af5..898d605463f 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue @@ -1,23 +1,23 @@ <script> - export default { - props: { - label: { - type: String, - required: false, - default: null, - }, - helpPath: { - type: String, - required: false, - default: null, - }, - helpText: { - type: String, - required: false, - default: null, - }, +export default { + props: { + label: { + type: String, + required: false, + default: null, }, - }; + helpPath: { + type: String, + required: false, + default: null, + }, + helpText: { + type: String, + required: false, + default: null, + }, + }, +}; </script> <template> diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue index a16f7e6b77c..c0ec7a5dc94 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue @@ -1,202 +1,200 @@ <script> - import projectFeatureSetting from './project_feature_setting.vue'; - import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue'; - import projectSettingRow from './project_setting_row.vue'; - import { visibilityOptions, visibilityLevelDescriptions } from '../constants'; - import { toggleHiddenClassBySelector } from '../external'; +import projectFeatureSetting from './project_feature_setting.vue'; +import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue'; +import projectSettingRow from './project_setting_row.vue'; +import { visibilityOptions, visibilityLevelDescriptions } from '../constants'; +import { toggleHiddenClassBySelector } from '../external'; - export default { - components: { - projectFeatureSetting, - projectFeatureToggle, - projectSettingRow, - }, +export default { + components: { + projectFeatureSetting, + projectFeatureToggle, + projectSettingRow, + }, - props: { - currentSettings: { - type: Object, - required: true, - }, - canChangeVisibilityLevel: { - type: Boolean, - required: false, - default: false, - }, - allowedVisibilityOptions: { - type: Array, - required: false, - default: () => [0, 10, 20], - }, - lfsAvailable: { - type: Boolean, - required: false, - default: false, - }, - registryAvailable: { - type: Boolean, - required: false, - default: false, - }, - visibilityHelpPath: { - type: String, - required: false, - default: '', - }, - lfsHelpPath: { - type: String, - required: false, - default: '', - }, - registryHelpPath: { - type: String, - required: false, - default: '', - }, - pagesAvailable: { - type: Boolean, - required: false, - default: false, - }, - pagesAccessControlEnabled: { - type: Boolean, - required: false, - default: false, - }, - pagesHelpPath: { - type: String, - required: false, - default: '', - }, + props: { + currentSettings: { + type: Object, + required: true, + }, + canChangeVisibilityLevel: { + type: Boolean, + required: false, + default: false, + }, + allowedVisibilityOptions: { + type: Array, + required: false, + default: () => [0, 10, 20], + }, + lfsAvailable: { + type: Boolean, + required: false, + default: false, + }, + registryAvailable: { + type: Boolean, + required: false, + default: false, + }, + visibilityHelpPath: { + type: String, + required: false, + default: '', }, + lfsHelpPath: { + type: String, + required: false, + default: '', + }, + registryHelpPath: { + type: String, + required: false, + default: '', + }, + pagesAvailable: { + type: Boolean, + required: false, + default: false, + }, + pagesAccessControlEnabled: { + type: Boolean, + required: false, + default: false, + }, + pagesHelpPath: { + type: String, + required: false, + default: '', + }, + }, - data() { - const defaults = { - visibilityOptions, - visibilityLevel: visibilityOptions.PUBLIC, - issuesAccessLevel: 20, - repositoryAccessLevel: 20, - mergeRequestsAccessLevel: 20, - buildsAccessLevel: 20, - wikiAccessLevel: 20, - snippetsAccessLevel: 20, - pagesAccessLevel: 20, - containerRegistryEnabled: true, - lfsEnabled: true, - requestAccessEnabled: true, - highlightChangesClass: false, - }; + data() { + const defaults = { + visibilityOptions, + visibilityLevel: visibilityOptions.PUBLIC, + issuesAccessLevel: 20, + repositoryAccessLevel: 20, + mergeRequestsAccessLevel: 20, + buildsAccessLevel: 20, + wikiAccessLevel: 20, + snippetsAccessLevel: 20, + pagesAccessLevel: 20, + containerRegistryEnabled: true, + lfsEnabled: true, + requestAccessEnabled: true, + highlightChangesClass: false, + }; - return { ...defaults, ...this.currentSettings }; - }, + return { ...defaults, ...this.currentSettings }; + }, - computed: { - featureAccessLevelOptions() { - const options = [ - [10, 'Only Project Members'], - ]; - if (this.visibilityLevel !== visibilityOptions.PRIVATE) { - options.push([20, 'Everyone With Access']); - } - return options; - }, + computed: { + featureAccessLevelOptions() { + const options = [[10, 'Only Project Members']]; + if (this.visibilityLevel !== visibilityOptions.PRIVATE) { + options.push([20, 'Everyone With Access']); + } + return options; + }, - repoFeatureAccessLevelOptions() { - return this.featureAccessLevelOptions.filter( - ([value]) => value <= this.repositoryAccessLevel, - ); - }, + repoFeatureAccessLevelOptions() { + return this.featureAccessLevelOptions.filter( + ([value]) => value <= this.repositoryAccessLevel, + ); + }, - pagesFeatureAccessLevelOptions() { - if (this.visibilityLevel !== visibilityOptions.PUBLIC) { - return this.featureAccessLevelOptions.concat([[30, 'Everyone']]); - } - return this.featureAccessLevelOptions; - }, + pagesFeatureAccessLevelOptions() { + if (this.visibilityLevel !== visibilityOptions.PUBLIC) { + return this.featureAccessLevelOptions.concat([[30, 'Everyone']]); + } + return this.featureAccessLevelOptions; + }, - repositoryEnabled() { - return this.repositoryAccessLevel > 0; - }, + repositoryEnabled() { + return this.repositoryAccessLevel > 0; + }, - visibilityLevelDescription() { - return visibilityLevelDescriptions[this.visibilityLevel]; - }, + visibilityLevelDescription() { + return visibilityLevelDescriptions[this.visibilityLevel]; }, + }, - watch: { - visibilityLevel(value, oldValue) { - if (value === visibilityOptions.PRIVATE) { - // when private, features are restricted to "only team members" - this.issuesAccessLevel = Math.min(10, this.issuesAccessLevel); - this.repositoryAccessLevel = Math.min(10, this.repositoryAccessLevel); - this.mergeRequestsAccessLevel = Math.min(10, this.mergeRequestsAccessLevel); - this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel); - this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel); - this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel); - if (this.pagesAccessLevel === 20) { - // When from Internal->Private narrow access for only members - this.pagesAccessLevel = 10; - } - this.highlightChanges(); - } else if (oldValue === visibilityOptions.PRIVATE) { - // if changing away from private, make enabled features more permissive - if (this.issuesAccessLevel > 0) this.issuesAccessLevel = 20; - if (this.repositoryAccessLevel > 0) this.repositoryAccessLevel = 20; - if (this.mergeRequestsAccessLevel > 0) this.mergeRequestsAccessLevel = 20; - if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20; - if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20; - if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20; - if (this.pagesAccessLevel === 10) this.pagesAccessLevel = 20; - this.highlightChanges(); + watch: { + visibilityLevel(value, oldValue) { + if (value === visibilityOptions.PRIVATE) { + // when private, features are restricted to "only team members" + this.issuesAccessLevel = Math.min(10, this.issuesAccessLevel); + this.repositoryAccessLevel = Math.min(10, this.repositoryAccessLevel); + this.mergeRequestsAccessLevel = Math.min(10, this.mergeRequestsAccessLevel); + this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel); + this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel); + this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel); + if (this.pagesAccessLevel === 20) { + // When from Internal->Private narrow access for only members + this.pagesAccessLevel = 10; } - }, + this.highlightChanges(); + } else if (oldValue === visibilityOptions.PRIVATE) { + // if changing away from private, make enabled features more permissive + if (this.issuesAccessLevel > 0) this.issuesAccessLevel = 20; + if (this.repositoryAccessLevel > 0) this.repositoryAccessLevel = 20; + if (this.mergeRequestsAccessLevel > 0) this.mergeRequestsAccessLevel = 20; + if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20; + if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20; + if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20; + if (this.pagesAccessLevel === 10) this.pagesAccessLevel = 20; + this.highlightChanges(); + } + }, - repositoryAccessLevel(value, oldValue) { - if (value < oldValue) { - // sub-features cannot have more premissive access level - this.mergeRequestsAccessLevel = Math.min(this.mergeRequestsAccessLevel, value); - this.buildsAccessLevel = Math.min(this.buildsAccessLevel, value); + repositoryAccessLevel(value, oldValue) { + if (value < oldValue) { + // sub-features cannot have more premissive access level + this.mergeRequestsAccessLevel = Math.min(this.mergeRequestsAccessLevel, value); + this.buildsAccessLevel = Math.min(this.buildsAccessLevel, value); - if (value === 0) { - this.containerRegistryEnabled = false; - this.lfsEnabled = false; - } - } else if (oldValue === 0) { - this.mergeRequestsAccessLevel = value; - this.buildsAccessLevel = value; - this.containerRegistryEnabled = true; - this.lfsEnabled = true; + if (value === 0) { + this.containerRegistryEnabled = false; + this.lfsEnabled = false; } - }, + } else if (oldValue === 0) { + this.mergeRequestsAccessLevel = value; + this.buildsAccessLevel = value; + this.containerRegistryEnabled = true; + this.lfsEnabled = true; + } + }, - issuesAccessLevel(value, oldValue) { - if (value === 0) toggleHiddenClassBySelector('.issues-feature', true); - else if (oldValue === 0) toggleHiddenClassBySelector('.issues-feature', false); - }, + issuesAccessLevel(value, oldValue) { + if (value === 0) toggleHiddenClassBySelector('.issues-feature', true); + else if (oldValue === 0) toggleHiddenClassBySelector('.issues-feature', false); + }, - mergeRequestsAccessLevel(value, oldValue) { - if (value === 0) toggleHiddenClassBySelector('.merge-requests-feature', true); - else if (oldValue === 0) toggleHiddenClassBySelector('.merge-requests-feature', false); - }, + mergeRequestsAccessLevel(value, oldValue) { + if (value === 0) toggleHiddenClassBySelector('.merge-requests-feature', true); + else if (oldValue === 0) toggleHiddenClassBySelector('.merge-requests-feature', false); + }, - buildsAccessLevel(value, oldValue) { - if (value === 0) toggleHiddenClassBySelector('.builds-feature', true); - else if (oldValue === 0) toggleHiddenClassBySelector('.builds-feature', false); - }, + buildsAccessLevel(value, oldValue) { + if (value === 0) toggleHiddenClassBySelector('.builds-feature', true); + else if (oldValue === 0) toggleHiddenClassBySelector('.builds-feature', false); }, + }, - methods: { - highlightChanges() { - this.highlightChangesClass = true; - this.$nextTick(() => { - this.highlightChangesClass = false; - }); - }, + methods: { + highlightChanges() { + this.highlightChangesClass = true; + this.$nextTick(() => { + this.highlightChangesClass = false; + }); + }, - visibilityAllowed(option) { - return this.allowedVisibilityOptions.includes(option); - }, + visibilityAllowed(option) { + return this.allowedVisibilityOptions.includes(option); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/pages/projects/shared/permissions/constants.js b/app/assets/javascripts/pages/projects/shared/permissions/constants.js index ce47562f259..bc5c29d12b5 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/constants.js +++ b/app/assets/javascripts/pages/projects/shared/permissions/constants.js @@ -5,7 +5,9 @@ export const visibilityOptions = { }; export const visibilityLevelDescriptions = { - [visibilityOptions.PRIVATE]: 'The project is accessible only by members of the project. Access must be granted explicitly to each user.', + [visibilityOptions.PRIVATE]: + 'The project is accessible only by members of the project. Access must be granted explicitly to each user.', [visibilityOptions.INTERNAL]: 'The project can be accessed by any user who is logged in.', - [visibilityOptions.PUBLIC]: 'The project can be accessed by anyone, regardless of authentication.', + [visibilityOptions.PUBLIC]: + 'The project can be accessed by anyone, regardless of authentication.', }; diff --git a/app/assets/javascripts/pages/projects/shared/project_avatar.js b/app/assets/javascripts/pages/projects/shared/project_avatar.js index 447877752fe..1e69ecb481d 100644 --- a/app/assets/javascripts/pages/projects/shared/project_avatar.js +++ b/app/assets/javascripts/pages/projects/shared/project_avatar.js @@ -8,8 +8,9 @@ export default function projectAvatar() { $('.js-project-avatar-input').bind('change', function onClickAvatarInput() { const form = $(this).closest('form'); - // eslint-disable-next-line no-useless-escape - const filename = $(this).val().replace(/^.*[\\\/]/, ''); + const filename = $(this) + .val() + .replace(/^.*[\\\/]/, ''); // eslint-disable-line no-useless-escape return form.find('.js-avatar-filename').text(filename); }); } diff --git a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue index 75cb6374ad5..f970a5ebb64 100644 --- a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue +++ b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue @@ -1,8 +1,15 @@ <script> import _ from 'underscore'; import { s__, sprintf } from '~/locale'; +import { GlModal, GlModalDirective } from '@gitlab-org/gitlab-ui'; export default { + components: { + GlModal, + }, + directives: { + 'gl-modal': GlModalDirective, + }, props: { deleteWikiUrl: { type: String, @@ -54,7 +61,7 @@ export default { > {{ __('Delete') }} </button> - <gl-ui-modal + <gl-modal :title="title" :ok-title="s__('WikiPageConfirmDelete|Delete page')" :modal-id="modalId" @@ -81,6 +88,6 @@ export default { name="authenticity_token" /> </form> - </gl-ui-modal> + </gl-modal> </div> </template> diff --git a/app/assets/javascripts/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js index c2629090f01..f5fd84d69ac 100644 --- a/app/assets/javascripts/pages/projects/wikis/index.js +++ b/app/assets/javascripts/pages/projects/wikis/index.js @@ -21,7 +21,8 @@ document.addEventListener('DOMContentLoaded', () => { const { deleteWikiUrl, pageTitle } = deleteWikiModalWrapperEl.dataset; - new Vue({ // eslint-disable-line no-new + // eslint-disable-next-line no-new + new Vue({ el: deleteWikiModalWrapperEl, data: { deleteWikiUrl: '', diff --git a/app/assets/javascripts/pages/search/show/search.js b/app/assets/javascripts/pages/search/show/search.js index e3e0ab91993..0c896c8599e 100644 --- a/app/assets/javascripts/pages/search/show/search.js +++ b/app/assets/javascripts/pages/search/show/search.js @@ -22,7 +22,7 @@ export default class Search { fields: ['full_name'], }, data(term, callback) { - return Api.groups(term, {}, (data) => { + return Api.groups(term, {}, data => { data.unshift({ full_name: 'Any', }); @@ -37,7 +37,7 @@ export default class Search { return obj.full_name; }, toggleLabel(obj) { - return `${($groupDropdown.data('defaultLabel'))} ${obj.full_name}`; + return `${$groupDropdown.data('defaultLabel')} ${obj.full_name}`; }, clicked: () => Search.submitSearch(), }); @@ -52,7 +52,7 @@ export default class Search { }, data: (term, callback) => { this.getProjectsData(term) - .then((data) => { + .then(data => { data.unshift({ name_with_namespace: 'Any', }); @@ -70,7 +70,7 @@ export default class Search { return obj.name_with_namespace; }, toggleLabel(obj) { - return `${($projectDropdown.data('defaultLabel'))} ${obj.name_with_namespace}`; + return `${$projectDropdown.data('defaultLabel')} ${obj.name_with_namespace}`; }, clicked: () => Search.submitSearch(), }); @@ -99,17 +99,24 @@ export default class Search { } clearSearchField() { - return $(this.searchInput).val('').trigger('keyup').focus(); + return $(this.searchInput) + .val('') + .trigger('keyup') + .focus(); } getProjectsData(term) { - return new Promise((resolve) => { + return new Promise(resolve => { if (this.groupId) { Api.groupProjects(this.groupId, term, {}, resolve); } else { - Api.projects(term, { - order_by: 'id', - }, resolve); + Api.projects( + term, + { + order_by: 'id', + }, + resolve, + ); } }); } diff --git a/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js index 1e7c29aefaa..2b8f1e8b0ef 100644 --- a/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js +++ b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js @@ -20,7 +20,7 @@ export default class SigninTabsMemoizer { bootstrap() { const tabs = document.querySelectorAll(this.tabSelector); if (tabs.length > 0) { - tabs[0].addEventListener('click', (e) => { + tabs[0].addEventListener('click', e => { if (e.target && e.target.nodeName === 'A') { const anchorName = e.target.getAttribute('href'); this.saveData(anchorName); diff --git a/app/assets/javascripts/pages/sessions/new/username_validator.js b/app/assets/javascripts/pages/sessions/new/username_validator.js index d621f988d86..7a41805bada 100644 --- a/app/assets/javascripts/pages/sessions/new/username_validator.js +++ b/app/assets/javascripts/pages/sessions/new/username_validator.js @@ -22,10 +22,10 @@ export default class UsernameValidator { available: false, valid: false, pending: false, - empty: true + empty: true, }; - const debounceTimeout = _.debounce((username) => { + const debounceTimeout = _.debounce(username => { this.validateUsername(username); }, debounceTimeoutDuration); @@ -81,7 +81,8 @@ export default class UsernameValidator { this.state.pending = true; this.state.available = false; this.renderState(); - axios.get(`${gon.relative_url_root}/users/${username}/exists`) + axios + .get(`${gon.relative_url_root}/users/${username}/exists`) .then(({ data }) => this.setAvailabilityState(data.exists)) .catch(() => flash(__('An error occurred while validating username'))); } @@ -100,8 +101,7 @@ export default class UsernameValidator { clearFieldValidationState() { this.inputElement.siblings('p').hide(); - this.inputElement.removeClass(invalidInputClass) - .removeClass(successInputClass); + this.inputElement.removeClass(invalidInputClass).removeClass(successInputClass); } setUnavailableState() { diff --git a/app/assets/javascripts/pages/users/index.js b/app/assets/javascripts/pages/users/index.js index 6b1626b0161..a191df00dfa 100644 --- a/app/assets/javascripts/pages/users/index.js +++ b/app/assets/javascripts/pages/users/index.js @@ -13,10 +13,12 @@ function initUserProfile(action) { new UserTabs({ parentEl: '.user-profile', action }); // hide project limit message - $('.hide-project-limit-message').on('click', (e) => { + $('.hide-project-limit-message').on('click', e => { e.preventDefault(); Cookies.set('hide_project_limit_message', 'false'); - $(this).parents('.project-limit-message').remove(); + $(this) + .parents('.project-limit-message') + .remove(); }); } diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js index 1de9945baad..04bcb16f036 100644 --- a/app/assets/javascripts/pages/users/user_tabs.js +++ b/app/assets/javascripts/pages/users/user_tabs.js @@ -170,7 +170,7 @@ export default class UserTabs { this.loadActivityCalendar('activity'); // eslint-disable-next-line no-new - new Activities(); + new Activities('#activity'); this.loaded.activity = true; } diff --git a/app/assets/javascripts/performance_bar/components/simple_metric.vue b/app/assets/javascripts/performance_bar/components/simple_metric.vue index 760ea8fe1e6..7a558558c4d 100644 --- a/app/assets/javascripts/performance_bar/components/simple_metric.vue +++ b/app/assets/javascripts/performance_bar/components/simple_metric.vue @@ -1,29 +1,29 @@ <script> - export default { - props: { - currentRequest: { - type: Object, - required: true, - }, - metric: { - type: String, - required: true, - }, +export default { + props: { + currentRequest: { + type: Object, + required: true, }, - computed: { - duration() { - return ( - this.currentRequest.details[this.metric] && - this.currentRequest.details[this.metric].duration - ); - }, - calls() { - return ( - this.currentRequest.details[this.metric] && this.currentRequest.details[this.metric].calls - ); - }, + metric: { + type: String, + required: true, }, - }; + }, + computed: { + duration() { + return ( + this.currentRequest.details[this.metric] && + this.currentRequest.details[this.metric].duration + ); + }, + calls() { + return ( + this.currentRequest.details[this.metric] && this.currentRequest.details[this.metric].calls + ); + }, + }, +}; </script> <template> <div diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js index 6e5ef0ac0b2..29bfb7ee5df 100644 --- a/app/assets/javascripts/performance_bar/index.js +++ b/app/assets/javascripts/performance_bar/index.js @@ -9,8 +9,7 @@ export default ({ container }) => performanceBarApp: () => import('./components/performance_bar_app.vue'), }, data() { - const performanceBarData = document.querySelector(this.$options.el) - .dataset; + const performanceBarData = document.querySelector(this.$options.el).dataset; const store = new PerformanceBarStore(); return { diff --git a/app/assets/javascripts/performance_bar/services/performance_bar_service.js b/app/assets/javascripts/performance_bar/services/performance_bar_service.js index 60d9ba62570..3a496fa2ed8 100644 --- a/app/assets/javascripts/performance_bar/services/performance_bar_service.js +++ b/app/assets/javascripts/performance_bar/services/performance_bar_service.js @@ -11,8 +11,10 @@ export default class PerformanceBarService { static registerInterceptor(peekUrl, callback) { const interceptor = response => { - const [fireCallback, requestId, requestUrl] = - PerformanceBarService.callbackParams(response, peekUrl); + const [fireCallback, requestId, requestUrl] = PerformanceBarService.callbackParams( + response, + peekUrl, + ); if (fireCallback) { callback(requestId, requestUrl); @@ -30,10 +32,7 @@ export default class PerformanceBarService { static removeInterceptor(interceptor) { axios.interceptors.response.eject(interceptor); - Vue.http.interceptors = _.without( - Vue.http.interceptors, - vueResourceInterceptor, - ); + Vue.http.interceptors = _.without(Vue.http.interceptors, vueResourceInterceptor); } static callbackParams(response, peekUrl) { diff --git a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js index c6b2f55243c..031e774d533 100644 --- a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js +++ b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js @@ -32,8 +32,6 @@ export default class PerformanceBarStore { } canTrackRequest(requestUrl) { - return ( - this.requests.filter(request => request.url === requestUrl).length < 2 - ); + return this.requests.filter(request => request.url === requestUrl).length < 2; } } diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue index 16e69759091..a7507fb3b6f 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -1,16 +1,17 @@ <script> import { s__, sprintf } from '~/locale'; -import { formatTime } from '~/lib/utils/datetime_utility'; import eventHub from '../event_hub'; -import icon from '../../vue_shared/components/icon.vue'; +import Icon from '../../vue_shared/components/icon.vue'; import tooltip from '../../vue_shared/directives/tooltip'; +import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; export default { directives: { tooltip, }, components: { - icon, + Icon, + GlCountdown, }, props: { actions: { @@ -51,11 +52,6 @@ export default { return !action.playable; }, - - remainingTime(action) { - const remainingMilliseconds = new Date(action.scheduled_at).getTime() - Date.now(); - return formatTime(Math.max(0, remainingMilliseconds)); - }, }, }; </script> @@ -100,7 +96,7 @@ export default { class="pull-right" > <icon name="clock" /> - {{ remainingTime(action) }} + <gl-countdown :end-date-string="action.scheduled_at" /> </span> </button> </li> diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue index d40de95e051..e0f0434e03d 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue @@ -1,13 +1,13 @@ <script> import tooltip from '../../vue_shared/directives/tooltip'; -import icon from '../../vue_shared/components/icon.vue'; +import Icon from '../../vue_shared/components/icon.vue'; export default { directives: { tooltip, }, components: { - icon, + Icon, }, props: { artifacts: { diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue index cb14d4400d1..3339b5c13ed 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue @@ -88,25 +88,25 @@ export default { class="table-section section-10 js-pipeline-status pipeline-status" role="rowheader" > - Status + {{ s__('Pipeline|Status') }} </div> <div class="table-section section-15 js-pipeline-info pipeline-info" role="rowheader" > - Pipeline + {{ s__('Pipeline|Pipeline') }} </div> <div class="table-section section-20 js-pipeline-commit pipeline-commit" role="rowheader" > - Commit + {{ s__('Pipeline|Commit') }} </div> <div class="table-section section-20 js-pipeline-stages pipeline-stages" role="rowheader" > - Stages + {{ s__('Pipeline|Stages') }} </div> </div> <pipelines-table-row-component diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index 4f89ee66023..026d533d10f 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -261,7 +261,7 @@ export default { class="table-mobile-header" role="rowheader" > - Status + {{ s__('Pipeline|Status') }} </div> <div class="table-mobile-content"> <ci-badge @@ -279,8 +279,9 @@ export default { <div class="table-section section-20"> <div class="table-mobile-header" - role="rowheader"> - Commit + role="rowheader" + > + {{ s__('Pipeline|Commit') }} </div> <div class="table-mobile-content"> <commit-component @@ -298,8 +299,9 @@ export default { <div class="table-section section-wrap section-20 stage-cell"> <div class="table-mobile-header" - role="rowheader"> - Stages + role="rowheader" + > + {{ s__('Pipeline|Stages') }} </div> <div class="table-mobile-content"> <template v-if="pipeline.details.stages.length > 0"> diff --git a/app/assets/javascripts/pipelines/components/time_ago.vue b/app/assets/javascripts/pipelines/components/time_ago.vue index cd43d78de40..bed690200b8 100644 --- a/app/assets/javascripts/pipelines/components/time_ago.vue +++ b/app/assets/javascripts/pipelines/components/time_ago.vue @@ -60,7 +60,7 @@ export default { class="table-mobile-header" role="rowheader" > - Duration + {{ s__('Pipeline|Duration') }} </div> <div class="table-mobile-content"> <p @@ -87,7 +87,8 @@ export default { v-tooltip :title="tooltipTitle(finishedTime)" data-placement="top" - data-container="body"> + data-container="body" + > {{ timeFormated(finishedTime) }} </time> </p> diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue index 974629fa2af..99b57f4c9d5 100644 --- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue +++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue @@ -1,78 +1,78 @@ <script> - import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; - import { __, s__, sprintf } from '~/locale'; - import csrf from '~/lib/utils/csrf'; +import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; +import { __, s__, sprintf } from '~/locale'; +import csrf from '~/lib/utils/csrf'; - export default { - components: { - DeprecatedModal, +export default { + components: { + DeprecatedModal, + }, + props: { + actionUrl: { + type: String, + required: true, }, - props: { - actionUrl: { - type: String, - required: true, - }, - confirmWithPassword: { - type: Boolean, - required: true, - }, - username: { - type: String, - required: true, - }, + confirmWithPassword: { + type: Boolean, + required: true, }, - data() { - return { - enteredPassword: '', - enteredUsername: '', - }; + username: { + type: String, + required: true, }, - computed: { - csrfToken() { - return csrf.token; - }, - inputLabel() { - let confirmationValue; - if (this.confirmWithPassword) { - confirmationValue = __('password'); - } else { - confirmationValue = __('username'); - } + }, + data() { + return { + enteredPassword: '', + enteredUsername: '', + }; + }, + computed: { + csrfToken() { + return csrf.token; + }, + inputLabel() { + let confirmationValue; + if (this.confirmWithPassword) { + confirmationValue = __('password'); + } else { + confirmationValue = __('username'); + } - confirmationValue = `<code>${confirmationValue}</code>`; + confirmationValue = `<code>${confirmationValue}</code>`; - return sprintf( - s__('Profiles|Type your %{confirmationValue} to confirm:'), - { confirmationValue }, - false, - ); - }, - text() { - return sprintf( - s__(`Profiles| + return sprintf( + s__('Profiles|Type your %{confirmationValue} to confirm:'), + { confirmationValue }, + false, + ); + }, + text() { + return sprintf( + s__(`Profiles| You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account. Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), - { - yourAccount: `<strong>${s__('Profiles|your account')}</strong>`, - deleteAccount: `<strong>${s__('Profiles|Delete Account')}</strong>`, - }, - false, - ); - }, + { + yourAccount: `<strong>${s__('Profiles|your account')}</strong>`, + deleteAccount: `<strong>${s__('Profiles|Delete Account')}</strong>`, + }, + false, + ); }, - methods: { - canSubmit() { - if (this.confirmWithPassword) { - return this.enteredPassword !== ''; - } + }, + methods: { + canSubmit() { + if (this.confirmWithPassword) { + return this.enteredPassword !== ''; + } - return this.enteredUsername === this.username; - }, - onSubmit() { - this.$refs.form.submit(); - }, + return this.enteredUsername === this.username; + }, + onSubmit() { + this.$refs.form.submit(); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js index af134881f31..befe91c332f 100644 --- a/app/assets/javascripts/profile/gl_crop.js +++ b/app/assets/javascripts/profile/gl_crop.js @@ -4,20 +4,35 @@ import $ from 'jquery'; import 'cropper'; import _ from 'underscore'; -((global) => { +(global => { // Matches everything but the file name const FILENAMEREGEX = /^.*[\\\/]/; class GitLabCrop { - constructor(input, { filename, previewImage, modalCrop, pickImageEl, uploadImageBtn, modalCropImg, - exportWidth = 200, exportHeight = 200, cropBoxWidth = 200, cropBoxHeight = 200 } = {}) { + constructor( + input, + { + filename, + previewImage, + modalCrop, + pickImageEl, + uploadImageBtn, + modalCropImg, + exportWidth = 200, + exportHeight = 200, + cropBoxWidth = 200, + cropBoxHeight = 200, + } = {}, + ) { this.onUploadImageBtnClick = this.onUploadImageBtnClick.bind(this); this.onModalHide = this.onModalHide.bind(this); this.onModalShow = this.onModalShow.bind(this); this.onPickImageClick = this.onPickImageClick.bind(this); this.fileInput = $(input); this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg; - this.fileInput.attr('name', `${this.fileInput.attr('name')}-trigger`).attr('id', `${this.fileInput.attr('id')}-trigger`); + this.fileInput + .attr('name', `${this.fileInput.attr('name')}-trigger`) + .attr('id', `${this.fileInput.attr('id')}-trigger`); this.exportWidth = exportWidth; this.exportHeight = exportHeight; this.cropBoxWidth = cropBoxWidth; @@ -59,7 +74,7 @@ import _ from 'underscore'; btn = this; return _this.onActionBtnClick(btn); }); - return this.croppedImageBlob = null; + return (this.croppedImageBlob = null); } onPickImageClick() { @@ -94,9 +109,9 @@ import _ from 'underscore'; width: cropBoxWidth, height: cropBoxHeight, left: (container.width - cropBoxWidth) / 2, - top: (container.height - cropBoxHeight) / 2 + top: (container.height - cropBoxHeight) / 2, }); - } + }, }); } @@ -116,7 +131,7 @@ import _ from 'underscore'; var data, result; data = $(btn).data(); if (this.modalCropImg.data('cropper') && data.method) { - return result = this.modalCropImg.cropper(data.method, data.option); + return (result = this.modalCropImg.cropper(data.method, data.option)); } } @@ -127,7 +142,7 @@ import _ from 'underscore'; readFile(input) { var _this, reader; _this = this; - reader = new FileReader; + reader = new FileReader(); reader.onload = () => { _this.modalCropImg.attr('src', reader.result); return _this.modalCrop.modal('show'); @@ -145,7 +160,7 @@ import _ from 'underscore'; array.push(binary.charCodeAt(i)); } return new Blob([new Uint8Array(array)], { - type: 'image/png' + type: 'image/png', }); } @@ -157,11 +172,13 @@ import _ from 'underscore'; } setBlob() { - this.dataURL = this.modalCropImg.cropper('getCroppedCanvas', { - width: 200, - height: 200 - }).toDataURL('image/png'); - return this.croppedImageBlob = this.dataURLtoBlob(this.dataURL); + this.dataURL = this.modalCropImg + .cropper('getCroppedCanvas', { + width: 200, + height: 200, + }) + .toDataURL('image/png'); + return (this.croppedImageBlob = this.dataURLtoBlob(this.dataURL)); } getBlob() { diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js index e49c67ffb5c..8704a655b28 100644 --- a/app/assets/javascripts/profile/profile.js +++ b/app/assets/javascripts/profile/profile.js @@ -26,11 +26,7 @@ export default class Profile { } bindEvents() { - $('.js-preferences-form').on( - 'change.preference', - 'input[type=radio]', - this.submitForm, - ); + $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm); $('#user_notification_email').on('change', this.submitForm); $('#user_notified_of_own_activity').on('change', this.submitForm); this.form.on('submit', this.onSubmitForm); diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index ebe18b47e4e..998554d1be5 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -4,8 +4,10 @@ import { slugifyWithHyphens } from '../lib/utils/text_utility'; let hasUserDefinedProjectPath = false; -const deriveProjectPathFromUrl = ($projectImportUrl) => { - const $currentProjectPath = $projectImportUrl.parents('.toggle-import-form').find('#project_path'); +const deriveProjectPathFromUrl = $projectImportUrl => { + const $currentProjectPath = $projectImportUrl + .parents('.toggle-import-form') + .find('#project_path'); if (hasUserDefinedProjectPath) { return; } @@ -52,9 +54,11 @@ const bindEvents = () => { return; } - $('.how_to_import_link').on('click', (e) => { + $('.how_to_import_link').on('click', e => { e.preventDefault(); - $(e.currentTarget).next('.modal').show(); + $(e.currentTarget) + .next('.modal') + .show(); }); $('.modal-header .close').on('click', () => { @@ -63,15 +67,21 @@ const bindEvents = () => { $('.btn_import_gitlab_project').on('click', () => { const importHref = $('a.btn_import_gitlab_project').attr('href'); - $('.btn_import_gitlab_project') - .attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&name=${$projectName.val()}&path=${$projectPath.val()}`); + $('.btn_import_gitlab_project').attr( + 'href', + `${importHref}?namespace_id=${$( + '#project_namespace_id', + ).val()}&name=${$projectName.val()}&path=${$projectPath.val()}`, + ); }); if ($pushNewProjectTipTrigger) { $pushNewProjectTipTrigger .removeAttr('rel') .removeAttr('target') - .on('click', (e) => { e.preventDefault(); }) + .on('click', e => { + e.preventDefault(); + }) .popover({ title: $pushNewProjectTipTrigger.data('title'), placement: 'bottom', @@ -79,13 +89,15 @@ const bindEvents = () => { content: $('.push-new-project-tip-template').html(), }) .on('shown.bs.popover', () => { - $(document).on('click.popover touchstart.popover', (event) => { + $(document).on('click.popover touchstart.popover', event => { if ($(event.target).closest('.popover').length === 0) { $pushNewProjectTipTrigger.trigger('click'); } }); - const target = $(`#${$pushNewProjectTipTrigger.attr('aria-describedby')}`).find('.js-select-on-focus'); + const target = $(`#${$pushNewProjectTipTrigger.attr('aria-describedby')}`).find( + '.js-select-on-focus', + ); addSelectOnFocusBehaviour(target); target.focus(); @@ -117,16 +129,18 @@ const bindEvents = () => { const selectedTemplate = templates[value]; $selectedTemplateText.text(selectedTemplate.text); - $(selectedTemplate.icon).clone().addClass('d-block').appendTo($selectedIcon); + $(selectedTemplate.icon) + .clone() + .addClass('d-block') + .appendTo($selectedIcon); const $activeTabProjectName = $('.tab-pane.active #project_name'); const $activeTabProjectPath = $('.tab-pane.active #project_path'); $activeTabProjectName.focus(); - $activeTabProjectName - .keyup(() => { - onProjectNameChange($activeTabProjectName, $activeTabProjectPath); - hasUserDefinedProjectPath = $activeTabProjectPath.val().trim().length > 0; - }); + $activeTabProjectName.keyup(() => { + onProjectNameChange($activeTabProjectName, $activeTabProjectPath); + hasUserDefinedProjectPath = $activeTabProjectPath.val().trim().length > 0; + }); } $useTemplateBtn.on('change', chooseTemplate); diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js index b601b19e7be..48343c8ba0a 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_create.js +++ b/app/assets/javascripts/protected_branches/protected_branch_create.js @@ -46,8 +46,12 @@ export default class ProtectedBranchCreate { onSelect() { // Enable submit button const $branchInput = this.$form.find('input[name="protected_branch[name]"]'); - const $allowedToMergeInput = this.$form.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]'); - const $allowedToPushInput = this.$form.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]'); + const $allowedToMergeInput = this.$form.find( + 'input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]', + ); + const $allowedToPushInput = this.$form.find( + 'input[name="protected_branch[push_access_levels_attributes][0][access_level]"]', + ); const completedForm = !( $branchInput.val() && $allowedToMergeInput.length && diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js index 54560d08ad7..5bc08f60d16 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit.js +++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js @@ -29,8 +29,12 @@ export default class ProtectedBranchEdit { } onSelect() { - const $allowedToMergeInput = this.$wrap.find(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`); - const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`); + const $allowedToMergeInput = this.$wrap.find( + `input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`, + ); + const $allowedToPushInput = this.$wrap.find( + `input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`, + ); // Do not update if one dropdown has not selected any option if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return; @@ -38,25 +42,36 @@ export default class ProtectedBranchEdit { this.$allowedToMergeDropdown.disable(); this.$allowedToPushDropdown.disable(); - axios.patch(this.$wrap.data('url'), { - protected_branch: { - merge_access_levels_attributes: [{ - id: this.$allowedToMergeDropdown.data('accessLevelId'), - access_level: $allowedToMergeInput.val(), - }], - push_access_levels_attributes: [{ - id: this.$allowedToPushDropdown.data('accessLevelId'), - access_level: $allowedToPushInput.val(), - }], - }, - }).then(() => { - this.$allowedToMergeDropdown.enable(); - this.$allowedToPushDropdown.enable(); - }).catch(() => { - this.$allowedToMergeDropdown.enable(); - this.$allowedToPushDropdown.enable(); - - flash('Failed to update branch!', 'alert', document.querySelector('.js-protected-branches-list')); - }); + axios + .patch(this.$wrap.data('url'), { + protected_branch: { + merge_access_levels_attributes: [ + { + id: this.$allowedToMergeDropdown.data('accessLevelId'), + access_level: $allowedToMergeInput.val(), + }, + ], + push_access_levels_attributes: [ + { + id: this.$allowedToPushDropdown.data('accessLevelId'), + access_level: $allowedToPushInput.val(), + }, + ], + }, + }) + .then(() => { + this.$allowedToMergeDropdown.enable(); + this.$allowedToPushDropdown.enable(); + }) + .catch(() => { + this.$allowedToMergeDropdown.enable(); + this.$allowedToPushDropdown.enable(); + + flash( + 'Failed to update branch!', + 'alert', + document.querySelector('.js-protected-branches-list'), + ); + }); } } diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js index 2f8116df0d2..fddf2674cbb 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_create.js +++ b/app/assets/javascripts/protected_tags/protected_tag_create.js @@ -40,7 +40,9 @@ export default class ProtectedTagCreate { const $tagInput = this.$form.find('input[name="protected_tag[name]"]'); const $allowedToCreateInput = this.$form.find('#create_access_levels_attributes'); - this.$form.find('input[type="submit"]').prop('disabled', !($tagInput.val() && $allowedToCreateInput.length)); + this.$form + .find('input[type="submit"]') + .prop('disabled', !($tagInput.val() && $allowedToCreateInput.length)); } static getProtectedTags(term, callback) { diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js index 8687b2a4044..c52497e62f2 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_edit.js +++ b/app/assets/javascripts/protected_tags/protected_tag_edit.js @@ -21,26 +21,33 @@ export default class ProtectedTagEdit { } onSelect() { - const $allowedToCreateInput = this.$wrap.find(`input[name="${this.$allowedToCreateDropdownButton.data('fieldName')}"]`); + const $allowedToCreateInput = this.$wrap.find( + `input[name="${this.$allowedToCreateDropdownButton.data('fieldName')}"]`, + ); // Do not update if one dropdown has not selected any option if (!$allowedToCreateInput.length) return; this.$allowedToCreateDropdownButton.disable(); - axios.patch(this.$wrap.data('url'), { - protected_tag: { - create_access_levels_attributes: [{ - id: this.$allowedToCreateDropdownButton.data('accessLevelId'), - access_level: $allowedToCreateInput.val(), - }], - }, - }).then(() => { - this.$allowedToCreateDropdownButton.enable(); - }).catch(() => { - this.$allowedToCreateDropdownButton.enable(); - - flash('Failed to update tag!', 'alert', document.querySelector('.js-protected-tags-list')); - }); + axios + .patch(this.$wrap.data('url'), { + protected_tag: { + create_access_levels_attributes: [ + { + id: this.$allowedToCreateDropdownButton.data('accessLevelId'), + access_level: $allowedToCreateInput.val(), + }, + ], + }, + }) + .then(() => { + this.$allowedToCreateDropdownButton.enable(); + }) + .catch(() => { + this.$allowedToCreateDropdownButton.enable(); + + flash('Failed to update tag!', 'alert', document.querySelector('.js-protected-tags-list')); + }); } } diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue index 7e2287ac4db..9dd1c87a87d 100644 --- a/app/assets/javascripts/registry/components/app.vue +++ b/app/assets/javascripts/registry/components/app.vue @@ -1,42 +1,35 @@ <script> - import { mapGetters, mapActions } from 'vuex'; - import Flash from '../../flash'; - import store from '../stores'; - import collapsibleContainer from './collapsible_container.vue'; - import { errorMessages, errorMessagesTypes } from '../constants'; +import { mapGetters, mapActions } from 'vuex'; +import Flash from '../../flash'; +import store from '../stores'; +import collapsibleContainer from './collapsible_container.vue'; +import { errorMessages, errorMessagesTypes } from '../constants'; - export default { - name: 'RegistryListApp', - components: { - collapsibleContainer, +export default { + name: 'RegistryListApp', + components: { + collapsibleContainer, + }, + props: { + endpoint: { + type: String, + required: true, }, - props: { - endpoint: { - type: String, - required: true, - }, - }, - store, - computed: { - ...mapGetters([ - 'isLoading', - 'repos', - ]), - }, - created() { - this.setMainEndpoint(this.endpoint); - }, - mounted() { - this.fetchRepos() - .catch(() => Flash(errorMessages[errorMessagesTypes.FETCH_REPOS])); - }, - methods: { - ...mapActions([ - 'setMainEndpoint', - 'fetchRepos', - ]), - }, - }; + }, + store, + computed: { + ...mapGetters(['isLoading', 'repos']), + }, + created() { + this.setMainEndpoint(this.endpoint); + }, + mounted() { + this.fetchRepos().catch(() => Flash(errorMessages[errorMessagesTypes.FETCH_REPOS])); + }, + methods: { + ...mapActions(['setMainEndpoint', 'fetchRepos']), + }, +}; </script> <template> <div> diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue index d9bf41924d1..501b2625ae5 100644 --- a/app/assets/javascripts/registry/components/collapsible_container.vue +++ b/app/assets/javascripts/registry/components/collapsible_container.vue @@ -1,62 +1,59 @@ <script> - import { mapActions } from 'vuex'; - import Flash from '../../flash'; - import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; - import tooltip from '../../vue_shared/directives/tooltip'; - import tableRegistry from './table_registry.vue'; - import { errorMessages, errorMessagesTypes } from '../constants'; - import { __ } from '../../locale'; +import { mapActions } from 'vuex'; +import Flash from '../../flash'; +import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; +import tooltip from '../../vue_shared/directives/tooltip'; +import tableRegistry from './table_registry.vue'; +import { errorMessages, errorMessagesTypes } from '../constants'; +import { __ } from '../../locale'; - export default { - name: 'CollapsibeContainerRegisty', - components: { - clipboardButton, - tableRegistry, +export default { + name: 'CollapsibeContainerRegisty', + components: { + clipboardButton, + tableRegistry, + }, + directives: { + tooltip, + }, + props: { + repo: { + type: Object, + required: true, }, - directives: { - tooltip, - }, - props: { - repo: { - type: Object, - required: true, - }, - }, - data() { - return { - isOpen: false, - }; - }, - methods: { - ...mapActions([ - 'fetchRepos', - 'fetchList', - 'deleteRepo', - ]), + }, + data() { + return { + isOpen: false, + }; + }, + methods: { + ...mapActions(['fetchRepos', 'fetchList', 'deleteRepo']), - toggleRepo() { - this.isOpen = !this.isOpen; + toggleRepo() { + this.isOpen = !this.isOpen; - if (this.isOpen) { - this.fetchList({ repo: this.repo }) - .catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY)); - } - }, + if (this.isOpen) { + this.fetchList({ repo: this.repo }).catch(() => + this.showError(errorMessagesTypes.FETCH_REGISTRY), + ); + } + }, - handleDeleteRepository() { - this.deleteRepo(this.repo) - .then(() => { - Flash(__('This container registry has been scheduled for deletion.'), 'notice'); - this.fetchRepos(); - }) - .catch(() => this.showError(errorMessagesTypes.DELETE_REPO)); - }, + handleDeleteRepository() { + this.deleteRepo(this.repo) + .then(() => { + Flash(__('This container registry has been scheduled for deletion.'), 'notice'); + this.fetchRepos(); + }) + .catch(() => this.showError(errorMessagesTypes.DELETE_REPO)); + }, - showError(message) { - Flash(errorMessages[message]); - }, + showError(message) { + Flash(errorMessages[message]); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue index fafb35bd69a..bb6c977fc63 100644 --- a/app/assets/javascripts/registry/components/table_registry.vue +++ b/app/assets/javascripts/registry/components/table_registry.vue @@ -1,66 +1,62 @@ <script> - import { mapActions } from 'vuex'; - import { n__ } from '../../locale'; - import Flash from '../../flash'; - import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; - import tablePagination from '../../vue_shared/components/table_pagination.vue'; - import tooltip from '../../vue_shared/directives/tooltip'; - import timeagoMixin from '../../vue_shared/mixins/timeago'; - import { errorMessages, errorMessagesTypes } from '../constants'; - import { numberToHumanSize } from '../../lib/utils/number_utils'; +import { mapActions } from 'vuex'; +import { n__ } from '../../locale'; +import Flash from '../../flash'; +import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; +import tablePagination from '../../vue_shared/components/table_pagination.vue'; +import tooltip from '../../vue_shared/directives/tooltip'; +import timeagoMixin from '../../vue_shared/mixins/timeago'; +import { errorMessages, errorMessagesTypes } from '../constants'; +import { numberToHumanSize } from '../../lib/utils/number_utils'; - export default { - components: { - clipboardButton, - tablePagination, +export default { + components: { + clipboardButton, + tablePagination, + }, + directives: { + tooltip, + }, + mixins: [timeagoMixin], + props: { + repo: { + type: Object, + required: true, }, - directives: { - tooltip, + }, + computed: { + shouldRenderPagination() { + return this.repo.pagination.total > this.repo.pagination.perPage; }, - mixins: [ - timeagoMixin, - ], - props: { - repo: { - type: Object, - required: true, - }, - }, - computed: { - shouldRenderPagination() { - return this.repo.pagination.total > this.repo.pagination.perPage; - }, - }, - methods: { - ...mapActions([ - 'fetchList', - 'deleteRegistry', - ]), + }, + methods: { + ...mapActions(['fetchList', 'deleteRegistry']), - layers(item) { - return item.layers ? n__('%d layer', '%d layers', item.layers) : ''; - }, + layers(item) { + return item.layers ? n__('%d layer', '%d layers', item.layers) : ''; + }, - formatSize(size) { - return numberToHumanSize(size); - }, + formatSize(size) { + return numberToHumanSize(size); + }, - handleDeleteRegistry(registry) { - this.deleteRegistry(registry) - .then(() => this.fetchList({ repo: this.repo })) - .catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY)); - }, + handleDeleteRegistry(registry) { + this.deleteRegistry(registry) + .then(() => this.fetchList({ repo: this.repo })) + .catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY)); + }, - onPageChange(pageNumber) { - this.fetchList({ repo: this.repo, page: pageNumber }) - .catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY)); - }, + onPageChange(pageNumber) { + this.fetchList({ repo: this.repo, page: pageNumber }).catch(() => + this.showError(errorMessagesTypes.FETCH_REGISTRY), + ); + }, - showError(message) { - Flash(errorMessages[message]); - }, + showError(message) { + Flash(errorMessages[message]); }, - }; + }, +}; </script> <template> <div> diff --git a/app/assets/javascripts/registry/index.js b/app/assets/javascripts/registry/index.js index e15cd94a915..025afefe7f0 100644 --- a/app/assets/javascripts/registry/index.js +++ b/app/assets/javascripts/registry/index.js @@ -4,22 +4,23 @@ import Translate from '../vue_shared/translate'; Vue.use(Translate); -export default () => new Vue({ - el: '#js-vue-registry-images', - components: { - registryApp, - }, - data() { - const { dataset } = document.querySelector(this.$options.el); - return { - endpoint: dataset.endpoint, - }; - }, - render(createElement) { - return createElement('registry-app', { - props: { - endpoint: this.endpoint, - }, - }); - }, -}); +export default () => + new Vue({ + el: '#js-vue-registry-images', + components: { + registryApp, + }, + data() { + const { dataset } = document.querySelector(this.$options.el); + return { + endpoint: dataset.endpoint, + }; + }, + render(createElement) { + return createElement('registry-app', { + props: { + endpoint: this.endpoint, + }, + }); + }, + }); diff --git a/app/assets/javascripts/registry/stores/mutations.js b/app/assets/javascripts/registry/stores/mutations.js index 208c3c39866..69c051cd2d6 100644 --- a/app/assets/javascripts/registry/stores/mutations.js +++ b/app/assets/javascripts/registry/stores/mutations.js @@ -2,7 +2,6 @@ import * as types from './mutation_types'; import { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils'; export default { - [types.SET_MAIN_ENDPOINT](state, endpoint) { Object.assign(state, { endpoint }); }, diff --git a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue index b373d83a44b..bd204503cc7 100644 --- a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue +++ b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue @@ -1,79 +1,72 @@ <script> - import { mapActions, mapGetters, mapState } from 'vuex'; - import { s__ } from '~/locale'; - import { componentNames } from './issue_body'; - import ReportSection from './report_section.vue'; - import SummaryRow from './summary_row.vue'; - import IssuesList from './issues_list.vue'; - import Modal from './modal.vue'; - import createStore from '../store'; - import { summaryTextBuilder, reportTextBuilder, statusIcon } from '../store/utils'; +import { mapActions, mapGetters, mapState } from 'vuex'; +import { s__ } from '~/locale'; +import { componentNames } from './issue_body'; +import ReportSection from './report_section.vue'; +import SummaryRow from './summary_row.vue'; +import IssuesList from './issues_list.vue'; +import Modal from './modal.vue'; +import createStore from '../store'; +import { summaryTextBuilder, reportTextBuilder, statusIcon } from '../store/utils'; - export default { - name: 'GroupedTestReportsApp', - store: createStore(), - components: { - ReportSection, - SummaryRow, - IssuesList, - Modal, +export default { + name: 'GroupedTestReportsApp', + store: createStore(), + components: { + ReportSection, + SummaryRow, + IssuesList, + Modal, + }, + props: { + endpoint: { + type: String, + required: true, }, - props: { - endpoint: { - type: String, - required: true, - }, - }, - componentNames, - computed: { - ...mapState([ - 'reports', - 'isLoading', - 'hasError', - 'summary', - ]), - ...mapState({ - modalTitle: state => state.modal.title || '', - modalData: state => state.modal.data || {}, - }), - ...mapGetters([ - 'summaryStatus', - ]), - groupedSummaryText() { - if (this.isLoading) { - return s__('Reports|Test summary results are being parsed'); - } + }, + componentNames, + computed: { + ...mapState(['reports', 'isLoading', 'hasError', 'summary']), + ...mapState({ + modalTitle: state => state.modal.title || '', + modalData: state => state.modal.data || {}, + }), + ...mapGetters(['summaryStatus']), + groupedSummaryText() { + if (this.isLoading) { + return s__('Reports|Test summary results are being parsed'); + } - if (this.hasError) { - return s__('Reports|Test summary failed loading results'); - } + if (this.hasError) { + return s__('Reports|Test summary failed loading results'); + } - return summaryTextBuilder(s__('Reports|Test summary'), this.summary); - }, + return summaryTextBuilder(s__('Reports|Test summary'), this.summary); }, - created() { - this.setEndpoint(this.endpoint); + }, + created() { + this.setEndpoint(this.endpoint); - this.fetchReports(); + this.fetchReports(); + }, + methods: { + ...mapActions(['setEndpoint', 'fetchReports']), + reportText(report) { + const summary = report.summary || {}; + return reportTextBuilder(report.name, summary); + }, + getReportIcon(report) { + return statusIcon(report.status); }, - methods: { - ...mapActions(['setEndpoint', 'fetchReports']), - reportText(report) { - const summary = report.summary || {}; - return reportTextBuilder(report.name, summary); - }, - getReportIcon(report) { - return statusIcon(report.status); - }, - shouldRenderIssuesList(report) { - return ( - report.existing_failures.length > 0 || - report.new_failures.length > 0 || - report.resolved_failures.length > 0 - ); - }, + shouldRenderIssuesList(report) { + return ( + report.existing_failures.length > 0 || + report.new_failures.length > 0 || + report.resolved_failures.length > 0 + ); }, - }; + }, +}; </script> <template> <report-section diff --git a/app/assets/javascripts/reports/components/issue_status_icon.vue b/app/assets/javascripts/reports/components/issue_status_icon.vue index 85811698a37..6e143c4f98c 100644 --- a/app/assets/javascripts/reports/components/issue_status_icon.vue +++ b/app/assets/javascripts/reports/components/issue_status_icon.vue @@ -1,10 +1,6 @@ <script> import Icon from '~/vue_shared/components/icon.vue'; -import { - STATUS_FAILED, - STATUS_NEUTRAL, - STATUS_SUCCESS, -} from '../constants'; +import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '../constants'; export default { name: 'IssueStatusIcon', diff --git a/app/assets/javascripts/reports/components/issues_list.vue b/app/assets/javascripts/reports/components/issues_list.vue index df42201b5de..3b425ee2fed 100644 --- a/app/assets/javascripts/reports/components/issues_list.vue +++ b/app/assets/javascripts/reports/components/issues_list.vue @@ -1,10 +1,6 @@ <script> import IssuesBlock from '~/reports/components/report_issues.vue'; -import { - STATUS_SUCCESS, - STATUS_FAILED, - STATUS_NEUTRAL, -} from '~/reports/constants'; +import { STATUS_SUCCESS, STATUS_FAILED, STATUS_NEUTRAL } from '~/reports/constants'; /** * Renders block of issues diff --git a/app/assets/javascripts/reports/components/modal.vue b/app/assets/javascripts/reports/components/modal.vue index acc5c6d85e2..5f9e4072b2d 100644 --- a/app/assets/javascripts/reports/components/modal.vue +++ b/app/assets/javascripts/reports/components/modal.vue @@ -1,27 +1,27 @@ <script> - import Modal from '~/vue_shared/components/gl_modal.vue'; - import LoadingButton from '~/vue_shared/components/loading_button.vue'; - import CodeBlock from '~/vue_shared/components/code_block.vue'; - import { fieldTypes } from '../constants'; +import Modal from '~/vue_shared/components/gl_modal.vue'; +import LoadingButton from '~/vue_shared/components/loading_button.vue'; +import CodeBlock from '~/vue_shared/components/code_block.vue'; +import { fieldTypes } from '../constants'; - export default { - components: { - Modal, - LoadingButton, - CodeBlock, +export default { + components: { + Modal, + LoadingButton, + CodeBlock, + }, + props: { + title: { + type: String, + required: true, }, - props: { - title: { - type: String, - required: true, - }, - modalData: { - type: Object, - required: true, - }, + modalData: { + type: Object, + required: true, }, - fieldTypes, - }; + }, + fieldTypes, +}; </script> <template> <modal diff --git a/app/assets/javascripts/reports/components/test_issue_body.vue b/app/assets/javascripts/reports/components/test_issue_body.vue index cd443a49b52..1a87822fcc3 100644 --- a/app/assets/javascripts/reports/components/test_issue_body.vue +++ b/app/assets/javascripts/reports/components/test_issue_body.vue @@ -1,28 +1,28 @@ <script> - import { mapActions } from 'vuex'; +import { mapActions } from 'vuex'; - export default { - name: 'TestIssueBody', - props: { - issue: { - type: Object, - required: true, - }, - // failed || success - status: { - type: String, - required: true, - }, - isNew: { - type: Boolean, - required: false, - default: false, - }, +export default { + name: 'TestIssueBody', + props: { + issue: { + type: Object, + required: true, }, - methods: { - ...mapActions(['openModal']), + // failed || success + status: { + type: String, + required: true, }, - }; + isNew: { + type: Boolean, + required: false, + default: false, + }, + }, + methods: { + ...mapActions(['openModal']), + }, +}; </script> <template> <div class="report-block-list-issue-description prepend-top-5 append-bottom-5"> diff --git a/app/assets/javascripts/reports/store/actions.js b/app/assets/javascripts/reports/store/actions.js index acabcc1d193..db8ab5ccb80 100644 --- a/app/assets/javascripts/reports/store/actions.js +++ b/app/assets/javascripts/reports/store/actions.js @@ -43,9 +43,11 @@ export const fetchReports = ({ state, dispatch }) => { }, data: state.endpoint, method: 'getReports', - successCallback: ({ data, status }) => dispatch('receiveReportsSuccess', { - data, status, - }), + successCallback: ({ data, status }) => + dispatch('receiveReportsSuccess', { + data, + status, + }), errorCallback: () => dispatch('receiveReportsError'), }); diff --git a/app/assets/javascripts/reports/store/index.js b/app/assets/javascripts/reports/store/index.js index 9d8f7dc3b74..467c692b438 100644 --- a/app/assets/javascripts/reports/store/index.js +++ b/app/assets/javascripts/reports/store/index.js @@ -7,9 +7,10 @@ import state from './state'; Vue.use(Vuex); -export default () => new Vuex.Store({ - actions, - mutations, - getters, - state: state(), -}); +export default () => + new Vuex.Store({ + actions, + mutations, + getters, + state: state(), + }); diff --git a/app/assets/javascripts/reports/store/mutation_types.js b/app/assets/javascripts/reports/store/mutation_types.js index 82bda31df5d..599d4862dfe 100644 --- a/app/assets/javascripts/reports/store/mutation_types.js +++ b/app/assets/javascripts/reports/store/mutation_types.js @@ -4,4 +4,3 @@ export const REQUEST_REPORTS = 'REQUEST_REPORTS'; export const RECEIVE_REPORTS_SUCCESS = 'RECEIVE_REPORTS_SUCCESS'; export const RECEIVE_REPORTS_ERROR = 'RECEIVE_REPORTS_ERROR'; export const SET_ISSUE_MODAL_DATA = 'SET_ISSUE_MODAL_DATA'; - diff --git a/app/assets/javascripts/reports/store/mutations.js b/app/assets/javascripts/reports/store/mutations.js index b88bff97075..2a37f5b74fa 100644 --- a/app/assets/javascripts/reports/store/mutations.js +++ b/app/assets/javascripts/reports/store/mutations.js @@ -19,7 +19,6 @@ export default { state.status = response.status; state.reports = response.suites; - }, [types.RECEIVE_REPORTS_ERROR](state) { state.isLoading = false; @@ -36,7 +35,7 @@ export default { [types.SET_ISSUE_MODAL_DATA](state, payload) { state.modal.title = payload.issue.name; - Object.keys(payload.issue).forEach((key) => { + Object.keys(payload.issue).forEach(key => { if (Object.prototype.hasOwnProperty.call(state.modal.data, key)) { state.modal.data[key] = { ...state.modal.data[key], diff --git a/app/assets/javascripts/reports/store/state.js b/app/assets/javascripts/reports/store/state.js index 4cab2e27a16..5484900276c 100644 --- a/app/assets/javascripts/reports/store/state.js +++ b/app/assets/javascripts/reports/store/state.js @@ -57,5 +57,4 @@ export default () => ({ }, }, }, - }); diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index 6b3753f7966..225e21ad322 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -21,7 +21,7 @@ Sidebar.initialize = function(currentUser) { } }; -Sidebar.prototype.removeListeners = function () { +Sidebar.prototype.removeListeners = function() { this.sidebar.off('click', '.sidebar-collapsed-icon'); this.sidebar.off('hidden.gl.dropdown'); $('.dropdown').off('loading.gl.dropdown'); @@ -38,10 +38,12 @@ Sidebar.prototype.addEventListeners = function() { $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded); $document.on('click', '.js-sidebar-toggle', this.sidebarToggleClicked); - return $(document).off('click', '.js-issuable-todo').on('click', '.js-issuable-todo', this.toggleTodo); + return $(document) + .off('click', '.js-issuable-todo') + .on('click', '.js-issuable-todo', this.toggleTodo); }; -Sidebar.prototype.sidebarToggleClicked = function (e, triggered) { +Sidebar.prototype.sidebarToggleClicked = function(e, triggered) { var $allGutterToggleIcons, $this, isExpanded, tooltipLabel; e.preventDefault(); $this = $(this); @@ -51,18 +53,26 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) { if (isExpanded) { $allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left'); - $('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); - $('.layout-page').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); + $('aside.right-sidebar') + .removeClass('right-sidebar-expanded') + .addClass('right-sidebar-collapsed'); + $('.layout-page') + .removeClass('right-sidebar-expanded') + .addClass('right-sidebar-collapsed'); } else { $allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right'); - $('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); - $('.layout-page').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); + $('aside.right-sidebar') + .removeClass('right-sidebar-collapsed') + .addClass('right-sidebar-expanded'); + $('.layout-page') + .removeClass('right-sidebar-collapsed') + .addClass('right-sidebar-expanded'); } $this.attr('data-original-title', tooltipLabel); if (!triggered) { - Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed')); + Cookies.set('collapsed_gutter', $('.right-sidebar').hasClass('right-sidebar-collapsed')); } }; @@ -71,21 +81,27 @@ Sidebar.prototype.toggleTodo = function(e) { $this = $(e.currentTarget); ajaxType = $this.attr('data-delete-path') ? 'delete' : 'post'; if ($this.attr('data-delete-path')) { - url = "" + ($this.attr('data-delete-path')); + url = '' + $this.attr('data-delete-path'); } else { - url = "" + ($this.data('url')); + url = '' + $this.data('url'); } $this.tooltip('hide'); - $('.js-issuable-todo').disable().addClass('is-loading'); + $('.js-issuable-todo') + .disable() + .addClass('is-loading'); axios[ajaxType](url, { issuable_id: $this.data('issuableId'), issuable_type: $this.data('issuableType'), - }).then(({ data }) => { - this.todoUpdateDone(data); - }).catch(() => flash(`There was an error ${ajaxType === 'post' ? 'adding a' : 'deleting the'} todo.`)); + }) + .then(({ data }) => { + this.todoUpdateDone(data); + }) + .catch(() => + flash(`There was an error ${ajaxType === 'post' ? 'adding a' : 'deleting the'} todo.`), + ); }; Sidebar.prototype.todoUpdateDone = function(data) { @@ -99,7 +115,8 @@ Sidebar.prototype.todoUpdateDone = function(data) { const $el = $(el); const $elText = $el.find('.js-issuable-todo-inner'); - $el.removeClass('is-loading') + $el + .removeClass('is-loading') .enable() .attr('aria-label', $el.data(`${attrPrefix}Text`)) .attr('data-delete-path', deletePath) @@ -119,7 +136,9 @@ Sidebar.prototype.todoUpdateDone = function(data) { Sidebar.prototype.sidebarDropdownLoading = function(e) { var $loading, $sidebarCollapsedIcon, i, img; - $sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon'); + $sidebarCollapsedIcon = $(this) + .closest('.block') + .find('.sidebar-collapsed-icon'); img = $sidebarCollapsedIcon.find('img'); i = $sidebarCollapsedIcon.find('i'); $loading = $('<i class="fa fa-spinner fa-spin"></i>'); @@ -134,7 +153,9 @@ Sidebar.prototype.sidebarDropdownLoading = function(e) { Sidebar.prototype.sidebarDropdownLoaded = function(e) { var $sidebarCollapsedIcon, i, img; - $sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon'); + $sidebarCollapsedIcon = $(this) + .closest('.block') + .find('.sidebar-collapsed-icon'); img = $sidebarCollapsedIcon.find('img'); $sidebarCollapsedIcon.find('i.fa-spin').remove(); i = $sidebarCollapsedIcon.find('i'); @@ -220,7 +241,7 @@ Sidebar.prototype.isOpen = function() { }; Sidebar.prototype.getBlock = function(name) { - return this.sidebar.find(".block." + name); + return this.sidebar.find('.block.' + name); }; export default Sidebar; diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index 7bde4860973..17def77b2d7 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -226,7 +226,7 @@ export class SearchAutocomplete { icon, text: term, template: s__('SearchAutocomplete|in all GitLab'), - url: `/search?search=${term}`, + url: `${gon.relative_url_root}/search?search=${term}`, }); if (template) { @@ -234,7 +234,9 @@ export class SearchAutocomplete { icon, text: term, template, - url: `/search?search=${term}&project_id=${this.projectInputEl.val()}&group_id=${this.groupInputEl.val()}`, + url: `${ + gon.relative_url_root + }/search?search=${term}&project_id=${this.projectInputEl.val()}&group_id=${this.groupInputEl.val()}`, }); } } diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue index 43f0b6651b9..8950ae31627 100644 --- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue +++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue @@ -5,6 +5,7 @@ import Icon from '~/vue_shared/components/icon.vue'; import GfmAutoComplete from '~/gfm_auto_complete'; import { __, s__ } from '~/locale'; import Api from '~/api'; +import { GlModal } from '@gitlab-org/gitlab-ui'; import eventHub from './event_hub'; import EmojiMenuInModal from './emoji_menu_in_modal'; @@ -13,6 +14,7 @@ const emojiMenuClass = 'js-modal-status-emoji-menu'; export default { components: { Icon, + GlModal, }, props: { currentEmoji: { @@ -152,7 +154,7 @@ export default { </script> <template> - <gl-ui-modal + <gl-modal :title="s__('SetStatusModal|Set a status')" :modal-id="modalId" :ok-title="s__('SetStatusModal|Set status')" @@ -237,5 +239,5 @@ export default { </div> </div> </div> - </gl-ui-modal> + </gl-modal> </template> diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue index dd155c133ce..f1ea6aacdb2 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue @@ -74,8 +74,8 @@ export default { } if (!this.users.length) { - const emptyTooltipLabel = this.issuableType === 'issue' ? - __('Assignee(s)') : __('Assignee'); + const emptyTooltipLabel = + this.issuableType === 'issue' ? __('Assignee(s)') : __('Assignee'); names.push(emptyTooltipLabel); } @@ -248,4 +248,3 @@ export default { </div> </div> </template> - diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue index 123c92aff64..cfa7029b388 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue @@ -69,7 +69,8 @@ export default { this.loading = false; } - this.mediator.saveAssignees(this.field) + this.mediator + .saveAssignees(this.field) .then(setLoadingFalse.bind(this)) .catch(() => { setLoadingFalse(); diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue index 2b8d6207dea..439e8a69df0 100644 --- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue @@ -56,11 +56,7 @@ export default { .update('issue', { confidential }) .then(() => window.location.reload()) .catch(() => { - Flash( - __( - 'Something went wrong trying to change the confidentiality of this issue', - ), - ); + Flash(__('Something went wrong trying to change the confidentiality of this issue')); }); }, }, diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue index cdff4105335..48a2b9194aa 100644 --- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue @@ -34,11 +34,7 @@ export default { required: true, type: Object, validator(mediatorObject) { - return ( - mediatorObject.service && - mediatorObject.service.update && - mediatorObject.store - ); + return mediatorObject.service && mediatorObject.service.update && mediatorObject.store; }, }, }, @@ -67,8 +63,7 @@ export default { methods: { toggleForm() { - this.mediator.store.isLockDialogOpen = !this.mediator.store - .isLockDialogOpen; + this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen; }, updateLockedAttribute(locked) { @@ -79,9 +74,14 @@ export default { .then(() => window.location.reload()) .catch(() => Flash( - sprintf(__('Something went wrong trying to change the locked state of this %{issuableDisplayName}'), { - issuableDisplayName: this.issuableDisplayName, - }), + sprintf( + __( + 'Something went wrong trying to change the locked state of this %{issuableDisplayName}', + ), + { + issuableDisplayName: this.issuableDisplayName, + }, + ), ), ); }, diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue index 286a16f7bbf..11b5dbe5f8e 100644 --- a/app/assets/javascripts/sidebar/components/participants/participants.vue +++ b/app/assets/javascripts/sidebar/components/participants/participants.vue @@ -1,78 +1,78 @@ <script> - import { __, n__, sprintf } from '~/locale'; - import tooltip from '~/vue_shared/directives/tooltip'; - import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; +import { __, n__, sprintf } from '~/locale'; +import tooltip from '~/vue_shared/directives/tooltip'; +import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; - export default { - directives: { - tooltip, +export default { + directives: { + tooltip, + }, + components: { + userAvatarImage, + }, + props: { + loading: { + type: Boolean, + required: false, + default: false, }, - components: { - userAvatarImage, + participants: { + type: Array, + required: false, + default: () => [], }, - props: { - loading: { - type: Boolean, - required: false, - default: false, - }, - participants: { - type: Array, - required: false, - default: () => [], - }, - numberOfLessParticipants: { - type: Number, - required: false, - default: 7, - }, + numberOfLessParticipants: { + type: Number, + required: false, + default: 7, }, - data() { - return { - isShowingMoreParticipants: false, - }; + }, + data() { + return { + isShowingMoreParticipants: false, + }; + }, + computed: { + lessParticipants() { + return this.participants.slice(0, this.numberOfLessParticipants); }, - computed: { - lessParticipants() { - return this.participants.slice(0, this.numberOfLessParticipants); - }, - visibleParticipants() { - return this.isShowingMoreParticipants ? this.participants : this.lessParticipants; - }, - hasMoreParticipants() { - return this.participants.length > this.numberOfLessParticipants; - }, - toggleLabel() { - let label = ''; - if (this.isShowingMoreParticipants) { - label = __('- show less'); - } else { - label = sprintf(__('+ %{moreCount} more'), { - moreCount: this.participants.length - this.numberOfLessParticipants, - }); - } + visibleParticipants() { + return this.isShowingMoreParticipants ? this.participants : this.lessParticipants; + }, + hasMoreParticipants() { + return this.participants.length > this.numberOfLessParticipants; + }, + toggleLabel() { + let label = ''; + if (this.isShowingMoreParticipants) { + label = __('- show less'); + } else { + label = sprintf(__('+ %{moreCount} more'), { + moreCount: this.participants.length - this.numberOfLessParticipants, + }); + } - return label; - }, - participantLabel() { - return sprintf( - n__('%{count} participant', '%{count} participants', this.participants.length), - { count: this.loading ? '' : this.participantCount }, - ); - }, - participantCount() { - return this.participants.length; - }, + return label; + }, + participantLabel() { + return sprintf( + n__('%{count} participant', '%{count} participants', this.participants.length), + { count: this.loading ? '' : this.participantCount }, + ); + }, + participantCount() { + return this.participants.length; + }, + }, + methods: { + toggleMoreParticipants() { + this.isShowingMoreParticipants = !this.isShowingMoreParticipants; }, - methods: { - toggleMoreParticipants() { - this.isShowingMoreParticipants = !this.isShowingMoreParticipants; - }, - onClickCollapsedIcon() { - this.$emit('toggleSidebar'); - }, + onClickCollapsedIcon() { + this.$emit('toggleSidebar'); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue index 5c1ead1a8ac..4ac515e552a 100644 --- a/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue +++ b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue @@ -1,23 +1,23 @@ <script> - import Store from '../../stores/sidebar_store'; - import participants from './participants.vue'; +import Store from '../../stores/sidebar_store'; +import participants from './participants.vue'; - export default { - components: { - participants, +export default { + components: { + participants, + }, + props: { + mediator: { + type: Object, + required: true, }, - props: { - mediator: { - type: Object, - required: true, - }, - }, - data() { - return { - store: new Store(), - }; - }, - }; + }, + data() { + return { + store: new Store(), + }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue index 385717e7c1e..95a2c8cce6e 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue @@ -21,10 +21,9 @@ export default { }, methods: { onToggleSubscription() { - this.mediator.toggleSubscription() - .catch(() => { - Flash(__('Error occurred when toggling the notification subscription')); - }); + this.mediator.toggleSubscription().catch(() => { + Flash(__('Error occurred when toggling the notification subscription')); + }); }, }, }; diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue index 448c8fc3602..b6151aa6c64 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue @@ -1,74 +1,74 @@ <script> - import { __ } from '~/locale'; - import icon from '~/vue_shared/components/icon.vue'; - import toggleButton from '~/vue_shared/components/toggle_button.vue'; - import tooltip from '~/vue_shared/directives/tooltip'; - import eventHub from '../../event_hub'; +import { __ } from '~/locale'; +import icon from '~/vue_shared/components/icon.vue'; +import toggleButton from '~/vue_shared/components/toggle_button.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; +import eventHub from '../../event_hub'; - const ICON_ON = 'notifications'; - const ICON_OFF = 'notifications-off'; - const LABEL_ON = __('Notifications on'); - const LABEL_OFF = __('Notifications off'); +const ICON_ON = 'notifications'; +const ICON_OFF = 'notifications-off'; +const LABEL_ON = __('Notifications on'); +const LABEL_OFF = __('Notifications off'); - export default { - directives: { - tooltip, +export default { + directives: { + tooltip, + }, + components: { + icon, + toggleButton, + }, + props: { + loading: { + type: Boolean, + required: false, + default: false, }, - components: { - icon, - toggleButton, + subscribed: { + type: Boolean, + required: false, + default: null, }, - props: { - loading: { - type: Boolean, - required: false, - default: false, - }, - subscribed: { - type: Boolean, - required: false, - default: null, - }, - id: { - type: Number, - required: false, - default: null, - }, + id: { + type: Number, + required: false, + default: null, }, - computed: { - showLoadingState() { - return this.subscribed === null; - }, - notificationIcon() { - return this.subscribed ? ICON_ON : ICON_OFF; - }, - notificationTooltip() { - return this.subscribed ? LABEL_ON : LABEL_OFF; - }, + }, + computed: { + showLoadingState() { + return this.subscribed === null; }, - methods: { - /** - * We need to emit this event on both component & eventHub - * for 2 dependencies; - * - * 1. eventHub: This component is used in Issue Boards sidebar - * where component template is part of HAML - * and event listeners are tied to app's eventHub. - * 2. Component: This compone is also used in Epics in EE - * where listeners are tied to component event. - */ - toggleSubscription() { - // App's eventHub event emission. - eventHub.$emit('toggleSubscription', this.id); + notificationIcon() { + return this.subscribed ? ICON_ON : ICON_OFF; + }, + notificationTooltip() { + return this.subscribed ? LABEL_ON : LABEL_OFF; + }, + }, + methods: { + /** + * We need to emit this event on both component & eventHub + * for 2 dependencies; + * + * 1. eventHub: This component is used in Issue Boards sidebar + * where component template is part of HAML + * and event listeners are tied to app's eventHub. + * 2. Component: This compone is also used in Epics in EE + * where listeners are tied to component event. + */ + toggleSubscription() { + // App's eventHub event emission. + eventHub.$emit('toggleSubscription', this.id); - // Component event emission. - this.$emit('toggleSubscription', this.id); - }, - onClickCollapsedIcon() { - this.$emit('toggleSidebar'); - }, + // Component event emission. + this.$emit('toggleSubscription', this.id); + }, + onClickCollapsedIcon() { + this.$emit('toggleSidebar'); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue index 1d030c4f67f..259858e4b46 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue @@ -1,111 +1,111 @@ <script> - import { __, sprintf } from '~/locale'; - import { abbreviateTime } from '~/lib/utils/pretty_time'; - import icon from '~/vue_shared/components/icon.vue'; - import tooltip from '~/vue_shared/directives/tooltip'; +import { __, sprintf } from '~/locale'; +import { abbreviateTime } from '~/lib/utils/datetime_utility'; +import icon from '~/vue_shared/components/icon.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; - export default { - name: 'TimeTrackingCollapsedState', - components: { - icon, +export default { + name: 'TimeTrackingCollapsedState', + components: { + icon, + }, + directives: { + tooltip, + }, + props: { + showComparisonState: { + type: Boolean, + required: true, }, - directives: { - tooltip, + showSpentOnlyState: { + type: Boolean, + required: true, }, - props: { - showComparisonState: { - type: Boolean, - required: true, - }, - showSpentOnlyState: { - type: Boolean, - required: true, - }, - showEstimateOnlyState: { - type: Boolean, - required: true, - }, - showNoTimeTrackingState: { - type: Boolean, - required: true, - }, - timeSpentHumanReadable: { - type: String, - required: false, - default: '', - }, - timeEstimateHumanReadable: { - type: String, - required: false, - default: '', - }, + showEstimateOnlyState: { + type: Boolean, + required: true, }, - computed: { - timeSpent() { - return this.abbreviateTime(this.timeSpentHumanReadable); - }, - timeEstimate() { - return this.abbreviateTime(this.timeEstimateHumanReadable); - }, - divClass() { - if (this.showComparisonState) { - return 'compare'; - } else if (this.showEstimateOnlyState) { - return 'estimate-only'; - } else if (this.showSpentOnlyState) { - return 'spend-only'; - } else if (this.showNoTimeTrackingState) { - return 'no-tracking'; - } + showNoTimeTrackingState: { + type: Boolean, + required: true, + }, + timeSpentHumanReadable: { + type: String, + required: false, + default: '', + }, + timeEstimateHumanReadable: { + type: String, + required: false, + default: '', + }, + }, + computed: { + timeSpent() { + return this.abbreviateTime(this.timeSpentHumanReadable); + }, + timeEstimate() { + return this.abbreviateTime(this.timeEstimateHumanReadable); + }, + divClass() { + if (this.showComparisonState) { + return 'compare'; + } else if (this.showEstimateOnlyState) { + return 'estimate-only'; + } else if (this.showSpentOnlyState) { + return 'spend-only'; + } else if (this.showNoTimeTrackingState) { + return 'no-tracking'; + } + return ''; + }, + spanClass() { + if (this.showComparisonState) { return ''; - }, - spanClass() { - if (this.showComparisonState) { - return ''; - } else if (this.showEstimateOnlyState || this.showSpentOnlyState) { - return 'bold'; - } else if (this.showNoTimeTrackingState) { - return 'no-value'; - } + } else if (this.showEstimateOnlyState || this.showSpentOnlyState) { + return 'bold'; + } else if (this.showNoTimeTrackingState) { + return 'no-value'; + } - return ''; - }, - text() { - if (this.showComparisonState) { - return `${this.timeSpent} / ${this.timeEstimate}`; - } else if (this.showEstimateOnlyState) { - return `-- / ${this.timeEstimate}`; - } else if (this.showSpentOnlyState) { - return `${this.timeSpent} / --`; - } else if (this.showNoTimeTrackingState) { - return 'None'; - } + return ''; + }, + text() { + if (this.showComparisonState) { + return `${this.timeSpent} / ${this.timeEstimate}`; + } else if (this.showEstimateOnlyState) { + return `-- / ${this.timeEstimate}`; + } else if (this.showSpentOnlyState) { + return `${this.timeSpent} / --`; + } else if (this.showNoTimeTrackingState) { + return 'None'; + } - return ''; - }, - timeTrackedTooltipText() { - let title; - if (this.showComparisonState) { - title = __('Time remaining'); - } else if (this.showEstimateOnlyState) { - title = __('Estimated'); - } else if (this.showSpentOnlyState) { - title = __('Time spent'); - } + return ''; + }, + timeTrackedTooltipText() { + let title; + if (this.showComparisonState) { + title = __('Time remaining'); + } else if (this.showEstimateOnlyState) { + title = __('Estimated'); + } else if (this.showSpentOnlyState) { + title = __('Time spent'); + } - return sprintf('%{title}: %{text}', ({ title, text: this.text })); - }, - tooltipText() { - return this.showNoTimeTrackingState ? __('Time tracking') : this.timeTrackedTooltipText; - }, + return sprintf('%{title}: %{text}', { title, text: this.text }); + }, + tooltipText() { + return this.showNoTimeTrackingState ? __('Time tracking') : this.timeTrackedTooltipText; }, - methods: { - abbreviateTime(timeStr) { - return abbreviateTime(timeStr); - }, + }, + methods: { + abbreviateTime(timeStr) { + return abbreviateTime(timeStr); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue index dc599e1b9fc..e74912d628f 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue @@ -1,5 +1,5 @@ <script> -import { parseSeconds, stringifyTime } from '../../../lib/utils/pretty_time'; +import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility'; import tooltip from '../../../vue_shared/directives/tooltip'; export default { diff --git a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue index 19ec0f05a26..91909cd49b8 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue @@ -15,16 +15,22 @@ export default { }, estimateText() { return sprintf( - s__('estimateCommand|%{slash_command} will update the estimated time with the latest command.'), { + s__( + 'estimateCommand|%{slash_command} will update the estimated time with the latest command.', + ), + { slash_command: '<code>/estimate</code>', - }, false, + }, + false, ); }, spendText() { return sprintf( - s__('spendCommand|%{slash_command} will update the sum of the time spent.'), { + s__('spendCommand|%{slash_command} will update the sum of the time spent.'), + { slash_command: '<code>/spend</code>', - }, false, + }, + false, ); }, }, diff --git a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue index 8660b0546cf..8e8b9f19b6e 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue @@ -26,7 +26,7 @@ export default { methods: { listenForQuickActions() { $(document).on('ajax:success', '.gfm-form', this.quickActionListened); - eventHub.$on('timeTrackingUpdated', (data) => { + eventHub.$on('timeTrackingUpdated', data => { this.quickActionListened(null, data); }); }, @@ -34,9 +34,7 @@ export default { const subscribedCommands = ['spend_time', 'time_estimate']; let changedCommands; if (data !== undefined) { - changedCommands = data.commands_changes - ? Object.keys(data.commands_changes) - : []; + changedCommands = data.commands_changes ? Object.keys(data.commands_changes) : []; } else { changedCommands = []; } diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue index a6b3a674952..bc59774f0a8 100644 --- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue +++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue @@ -41,9 +41,9 @@ export default { }, computed: { buttonClasses() { - return this.collapsed ? - 'btn-blank btn-todo sidebar-collapsed-icon dont-change-state' : - 'btn btn-default btn-todo issuable-header-btn float-right'; + return this.collapsed + ? 'btn-blank btn-todo sidebar-collapsed-icon dont-change-state' + : 'btn btn-default btn-todo issuable-header-btn float-right'; }, buttonLabel() { return this.isTodo ? MARK_TEXT : TODO_TEXT; diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js index b267422cd97..225ebb61195 100644 --- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js +++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js @@ -37,7 +37,8 @@ class SidebarMoveIssue { // Keep the dropdown open after selecting an option shouldPropagate: false, data: (searchTerm, callback) => { - this.mediator.fetchAutocompleteProjects(searchTerm) + this.mediator + .fetchAutocompleteProjects(searchTerm) .then(callback) .catch(() => new window.Flash('An error occurred while fetching projects autocomplete.')); }, @@ -48,7 +49,7 @@ class SidebarMoveIssue { </a> </li> `, - clicked: (options) => { + clicked: options => { const project = options.selectedObj; const selectedProjectId = options.isMarking ? project.id : 0; this.mediator.setMoveToProjectId(selectedProjectId); @@ -68,17 +69,12 @@ class SidebarMoveIssue { onConfirmClicked() { if (isValidProjectId(this.mediator.store.moveToProjectId)) { - this.$confirmButton - .disable() - .addClass('is-loading'); + this.$confirmButton.disable().addClass('is-loading'); - this.mediator.moveIssue() - .catch(() => { - window.Flash('An error occurred while moving the issue.'); - this.$confirmButton - .enable() - .removeClass('is-loading'); - }); + this.mediator.moveIssue().catch(() => { + window.Flash('An error occurred while moving the issue.'); + this.$confirmButton.enable().removeClass('is-loading'); + }); } } } diff --git a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js index 87da65a1b1f..1ebdbec7bc9 100644 --- a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js @@ -15,15 +15,16 @@ export default class SidebarMilestone { components: { timeTracker, }, - render: createElement => createElement('timeTracker', { - props: { - timeEstimate: parseInt(timeEstimate, 10), - timeSpent: parseInt(timeSpent, 10), - humanTimeEstimate, - humanTimeSpent, - rootPath: '/', - }, - }), + render: createElement => + createElement('timeTracker', { + props: { + timeEstimate: parseInt(timeEstimate, 10), + timeSpent: parseInt(timeSpent, 10), + humanTimeEstimate, + humanTimeSpent, + rootPath: '/', + }, + }), }); } } diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index 655bf9198b7..6f8214b18ee 100644 --- a/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -22,14 +22,15 @@ function mountAssigneesComponent(mediator) { components: { SidebarAssignees, }, - render: createElement => createElement('sidebar-assignees', { - props: { - mediator, - field: el.dataset.field, - signedIn: el.hasAttribute('data-signed-in'), - issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request', - }, - }), + render: createElement => + createElement('sidebar-assignees', { + props: { + mediator, + field: el.dataset.field, + signedIn: el.hasAttribute('data-signed-in'), + issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request', + }, + }), }); } @@ -83,11 +84,12 @@ function mountParticipantsComponent(mediator) { components: { sidebarParticipants, }, - render: createElement => createElement('sidebar-participants', { - props: { - mediator, - }, - }), + render: createElement => + createElement('sidebar-participants', { + props: { + mediator, + }, + }), }); } @@ -102,11 +104,12 @@ function mountSubscriptionsComponent(mediator) { components: { sidebarSubscriptions, }, - render: createElement => createElement('sidebar-subscriptions', { - props: { - mediator, - }, - }), + render: createElement => + createElement('sidebar-subscriptions', { + props: { + mediator, + }, + }), }); } diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js index 37c97225bfd..cbe20f761ff 100644 --- a/app/assets/javascripts/sidebar/services/sidebar_service.js +++ b/app/assets/javascripts/sidebar/services/sidebar_service.js @@ -22,11 +22,15 @@ export default class SidebarService { } update(key, data) { - return Vue.http.put(this.endpoint, { - [key]: data, - }, { - emulateJSON: true, - }); + return Vue.http.put( + this.endpoint, + { + [key]: data, + }, + { + emulateJSON: true, + }, + ); } getProjectsAutocomplete(searchTerm) { diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js index d9ca5e46770..3e040ec8428 100644 --- a/app/assets/javascripts/sidebar/sidebar_mediator.js +++ b/app/assets/javascripts/sidebar/sidebar_mediator.js @@ -39,9 +39,10 @@ export default class SidebarMediator { } fetch() { - return this.service.get() + return this.service + .get() .then(response => response.json()) - .then((data) => { + .then(data => { this.processFetchedData(data); }) .catch(() => new Flash('Error occurred when fetching sidebar data')); @@ -56,30 +57,33 @@ export default class SidebarMediator { toggleSubscription() { this.store.setFetchingState('subscriptions', true); - return this.service.toggleSubscription() + return this.service + .toggleSubscription() .then(() => { this.store.setSubscribedState(!this.store.subscribed); this.store.setFetchingState('subscriptions', false); }) - .catch((err) => { + .catch(err => { this.store.setFetchingState('subscriptions', false); throw err; }); } fetchAutocompleteProjects(searchTerm) { - return this.service.getProjectsAutocomplete(searchTerm) + return this.service + .getProjectsAutocomplete(searchTerm) .then(response => response.json()) - .then((data) => { + .then(data => { this.store.setAutocompleteProjects(data); return this.store.autocompleteProjects; }); } moveIssue() { - return this.service.moveIssue(this.store.moveToProjectId) + return this.service + .moveIssue(this.store.moveToProjectId) .then(response => response.json()) - .then((data) => { + .then(data => { if (window.location.pathname !== data.web_url) { visitUrl(data.web_url); } diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue index 6c87287a4c4..57c52a2016a 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue @@ -2,6 +2,7 @@ import Icon from '~/vue_shared/components/icon.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import FilteredSearchDropdown from '~/vue_shared/components/filtered_search_dropdown.vue'; +import { __ } from '~/locale'; import timeagoMixin from '../../vue_shared/mixins/timeago'; import tooltip from '../../vue_shared/directives/tooltip'; import LoadingButton from '../../vue_shared/components/loading_button.vue'; @@ -9,6 +10,7 @@ import { visitUrl } from '../../lib/utils/url_utility'; import createFlash from '../../flash'; import MemoryUsage from './memory_usage.vue'; import StatusIcon from './mr_widget_status_icon.vue'; +import ReviewAppLink from './review_app_link.vue'; import MRWidgetService from '../services/mr_widget_service'; export default { @@ -20,6 +22,7 @@ export default { Icon, TooltipOnTruncate, FilteredSearchDropdown, + ReviewAppLink, }, directives: { tooltip, @@ -31,6 +34,11 @@ export default { required: true, }, }, + deployedTextMap: { + running: __('Deploying to'), + success: __('Deployed to'), + failed: __('Failed to deploy to'), + }, data() { const features = window.gon.features || {}; return { @@ -54,10 +62,19 @@ export default { hasMetrics() { return !!this.deployment.metrics_url; }, + deployedText() { + return this.$options.deployedTextMap[this.deployment.status]; + }, + shouldRenderDropdown() { + return ( + this.enableCiEnvironmentsStatusChanges && + (this.deployment.changes && this.deployment.changes.length > 0) + ); + }, }, methods: { stopEnvironment() { - const msg = 'Are you sure you want to stop this environment?'; + const msg = __('Are you sure you want to stop this environment?'); const isConfirmed = confirm(msg); // eslint-disable-line if (isConfirmed) { @@ -87,10 +104,10 @@ export default { <div class="ci-widget media"> <div class="media-body"> <div class="deploy-body"> - <div class="deployment-info"> + <div class="js-deployment-info deployment-info"> <template v-if="hasDeploymentMeta"> <span> - Deployed to + {{ deployedText }} </span> <tooltip-on-truncate :title="deployment.name" @@ -124,7 +141,7 @@ export default { <div> <template v-if="hasExternalUrls"> <filtered-search-dropdown - v-if="enableCiEnvironmentsStatusChanges" + v-if="shouldRenderDropdown" class="js-mr-wigdet-deployment-dropdown inline" :items="deployment.changes" :main-action-link="deployment.external_url" @@ -134,18 +151,10 @@ export default { slot="mainAction" slot-scope="slotProps" > - <a - :href="deployment.external_url" - target="_blank" - rel="noopener noreferrer nofollow" - class="deploy-link js-deploy-url inline" - :class="slotProps.className" - > - <span> - {{ __('View app') }} - <icon name="external-link" /> - </span> - </a> + <review-app-link + :link="deployment.external_url" + :css-class="`deploy-link js-deploy-url inline ${slotProps.className}`" + /> </template> <template @@ -168,18 +177,11 @@ export default { </a> </template> </filtered-search-dropdown> - <a + <review-app-link v-else - :href="deployment.external_url" - target="_blank" - rel="noopener noreferrer nofollow" - class="js-deploy-url js-deploy-url-feature-flag deploy-link btn btn-default btn-sm inline" - > - <span> - {{ __('View app') }} - <icon name="external-link" /> - </span> - </a> + :link="deployment.external_url" + css-class="js-deploy-url js-deploy-url-feature-flag deploy-link btn btn-default btn-sm inlin" + /> </template> <loading-button v-if="deployment.stop_url" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue index fee41b239e8..8bcabc10225 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue @@ -1,5 +1,6 @@ <script> /* eslint-disable vue/require-default-prop */ +import { sprintf, __ } from '~/locale'; import PipelineStage from '~/pipelines/components/stage.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import Icon from '~/vue_shared/components/icon.vue'; @@ -36,6 +37,10 @@ export default { type: String, required: false, }, + troubleshootingDocsPath: { + type: String, + required: true, + }, }, computed: { hasPipeline() { @@ -57,6 +62,17 @@ export default { hasCommitInfo() { return this.pipeline.commit && Object.keys(this.pipeline.commit).length > 0; }, + errorText() { + return sprintf( + __( + 'Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation.%{linkEnd}', + ), + { + linkStart: `<a href="${this.troubleshootingDocsPath}">`, + linkEnd: '</a>', + }, + ); + }, }, }; </script> @@ -77,8 +93,10 @@ export default { name="status_failed_borderless" /> </div> - <div class="media-body"> - Could not connect to the CI server. Please check your settings and try again + <div + class="media-body" + v-html="errorText" + > </div> </template> <template v-else-if="hasPipeline"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue b/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue new file mode 100644 index 00000000000..b007d4f4dcb --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue @@ -0,0 +1,30 @@ +<script> +import Icon from '~/vue_shared/components/icon.vue'; + +export default { + components: { + Icon, + }, + props: { + link: { + type: String, + required: true, + }, + cssClass: { + type: String, + required: true, + }, + }, +}; +</script> +<template> + <a + :href="link" + target="_blank" + rel="noopener noreferrer nofollow" + :class="cssClass" + > + {{ __('View app') }} + <icon name="external-link" /> + </a> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index c8ad2aa30a6..e7baecbcde4 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -71,7 +71,12 @@ export default { return defaultClass; }, iconClass() { - if (this.status === 'failed' || !this.commitMessage.length || !this.mr.isMergeAllowed || this.mr.preventMerge) { + if ( + this.status === 'failed' || + !this.commitMessage.length || + !this.mr.isMergeAllowed || + this.mr.preventMerge + ) { return 'warning'; } return 'success'; @@ -90,10 +95,12 @@ export default { }, isMergeButtonDisabled() { const { commitMessage } = this; - return Boolean(!commitMessage.length - || !this.shouldShowMergeControls() - || this.isMakingRequest - || this.mr.preventMerge); + return Boolean( + !commitMessage.length || + !this.shouldShowMergeControls() || + this.isMakingRequest || + this.mr.preventMerge, + ); }, isRemoveSourceBranchButtonDisabled() { return this.isMergeButtonDisabled; @@ -140,9 +147,10 @@ export default { }; this.isMakingRequest = true; - this.service.merge(options) + this.service + .merge(options) .then(res => res.data) - .then((data) => { + .then(data => { const hasError = data.status === 'failed' || data.status === 'hook_validation_error'; if (data.status === 'merge_when_pipeline_succeeds') { @@ -167,9 +175,10 @@ export default { }); }, handleMergePolling(continuePolling, stopPolling) { - this.service.poll() + this.service + .poll() .then(res => res.data) - .then((data) => { + .then(data => { if (data.state === 'merged') { // If state is merged we should update the widget and stop the polling eventHub.$emit('MRWidgetUpdateRequested'); @@ -205,9 +214,10 @@ export default { }); }, handleRemoveBranchPolling(continuePolling, stopPolling) { - this.service.poll() + this.service + .poll() .then(res => res.data) - .then((data) => { + .then(data => { // If source branch exists then we should continue polling // because removing a source branch is a background task and takes time if (data.source_branch_exists) { diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js deleted file mode 100644 index a23496c6bf5..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * This file is the centerpiece of an attempt to reduce potential conflicts - * between the CE and EE versions of the MR widget. EE additions to the MR widget should - * be contained in the ee/vue_merge_request_widget directory, and should **extend** - * rather than mutate CE MR Widget code. - * - * This file should be the only source of conflicts between EE and CE. EE-only components should - * imported directly where they are needed, and import paths for EE extensions of CE components - * should overwrite import paths **without** changing the order of dependencies listed here. - */ - -export { default as Vue } from 'vue'; -export { default as SmartInterval } from '~/smart_interval'; -export { default as WidgetHeader } from './components/mr_widget_header.vue'; -export { default as WidgetMergeHelp } from './components/mr_widget_merge_help.vue'; -export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue'; -export { default as Deployment } from './components/deployment.vue'; -export { default as WidgetRelatedLinks } from './components/mr_widget_related_links.vue'; -export { default as MergedState } from './components/states/mr_widget_merged.vue'; -export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue'; -export { default as ClosedState } from './components/states/mr_widget_closed.vue'; -export { default as MergingState } from './components/states/mr_widget_merging.vue'; -export { default as WorkInProgressState } from './components/states/work_in_progress.vue'; -export { default as ArchivedState } from './components/states/mr_widget_archived.vue'; -export { default as ConflictsState } from './components/states/mr_widget_conflicts.vue'; -export { default as NothingToMergeState } from './components/states/nothing_to_merge.vue'; -export { default as MissingBranchState } from './components/states/mr_widget_missing_branch.vue'; -export { default as NotAllowedState } from './components/states/mr_widget_not_allowed.vue'; -export { default as ReadyToMergeState } from './components/states/ready_to_merge.vue'; -export { default as ShaMismatchState } from './components/states/sha_mismatch.vue'; -export { default as UnresolvedDiscussionsState } from './components/states/unresolved_discussions.vue'; -export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked.vue'; -export { default as PipelineFailedState } from './components/states/pipeline_failed.vue'; -export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds.vue'; -export { default as RebaseState } from './components/states/mr_widget_rebase.vue'; -export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed.vue'; -export { default as CheckingState } from './components/states/mr_widget_checking.vue'; -export { default as MRWidgetStore } from './stores/mr_widget_store'; -export { default as MRWidgetService } from './services/mr_widget_service'; -export { default as eventHub } from './event_hub'; -export { default as getStateKey } from './stores/get_state_key'; -export { default as stateMaps } from './stores/state_maps'; -export { default as SquashBeforeMerge } from './components/states/squash_before_merge.vue'; -export { default as notify } from '../lib/utils/notify'; -export { default as SourceBranchRemovalStatus } from './components/source_branch_removal_status.vue'; - -export { default as mrWidgetOptions } from './mr_widget_options.vue'; diff --git a/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js new file mode 100644 index 00000000000..8780aa4bd1c --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js @@ -0,0 +1,3 @@ +import MRWidgetOptions from './mr_widget_options.vue'; + +export default MRWidgetOptions; diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js index cc6e620f365..60cebbfc2b2 100644 --- a/app/assets/javascripts/vue_merge_request_widget/index.js +++ b/app/assets/javascripts/vue_merge_request_widget/index.js @@ -1,4 +1,5 @@ -import { Vue, mrWidgetOptions } from './dependencies'; +import Vue from 'vue'; +import MrWidgetOptions from './ee_switch_mr_widget_options'; import Translate from '../vue_shared/translate'; Vue.use(Translate); @@ -6,7 +7,7 @@ Vue.use(Translate); export default () => { gl.mrWidgetData.gitlabLogo = gon.gitlab_logo; - const vm = new Vue(mrWidgetOptions); + const vm = new Vue(MrWidgetOptions); window.gl.mrWidget = { checkStatus: vm.checkStatus, 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 8180f13a7cb..063d1e15544 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 @@ -1,40 +1,40 @@ <script> +import _ from 'underscore'; +import { __ } from '~/locale'; import Project from '~/pages/projects/project'; import SmartInterval from '~/smart_interval'; import createFlash from '../flash'; -import { - WidgetHeader, - WidgetMergeHelp, - WidgetPipeline, - Deployment, - WidgetRelatedLinks, - MergedState, - ClosedState, - MergingState, - RebaseState, - WorkInProgressState, - ArchivedState, - ConflictsState, - NothingToMergeState, - MissingBranchState, - NotAllowedState, - ReadyToMergeState, - ShaMismatchState, - UnresolvedDiscussionsState, - PipelineBlockedState, - PipelineFailedState, - FailedToMerge, - MergeWhenPipelineSucceedsState, - AutoMergeFailed, - CheckingState, - MRWidgetStore, - MRWidgetService, - eventHub, - stateMaps, - SquashBeforeMerge, - notify, - SourceBranchRemovalStatus, -} from './dependencies'; +import WidgetHeader from './components/mr_widget_header.vue'; +import WidgetMergeHelp from './components/mr_widget_merge_help.vue'; +import WidgetPipeline from './components/mr_widget_pipeline.vue'; +import Deployment from './components/deployment.vue'; +import WidgetRelatedLinks from './components/mr_widget_related_links.vue'; +import MergedState from './components/states/mr_widget_merged.vue'; +import ClosedState from './components/states/mr_widget_closed.vue'; +import MergingState from './components/states/mr_widget_merging.vue'; +import RebaseState from './components/states/mr_widget_rebase.vue'; +import WorkInProgressState from './components/states/work_in_progress.vue'; +import ArchivedState from './components/states/mr_widget_archived.vue'; +import ConflictsState from './components/states/mr_widget_conflicts.vue'; +import NothingToMergeState from './components/states/nothing_to_merge.vue'; +import MissingBranchState from './components/states/mr_widget_missing_branch.vue'; +import NotAllowedState from './components/states/mr_widget_not_allowed.vue'; +import ReadyToMergeState from './components/states/ready_to_merge.vue'; +import ShaMismatchState from './components/states/sha_mismatch.vue'; +import UnresolvedDiscussionsState from './components/states/unresolved_discussions.vue'; +import PipelineBlockedState from './components/states/mr_widget_pipeline_blocked.vue'; +import PipelineFailedState from './components/states/pipeline_failed.vue'; +import FailedToMerge from './components/states/mr_widget_failed_to_merge.vue'; +import MergeWhenPipelineSucceedsState from './components/states/mr_widget_merge_when_pipeline_succeeds.vue'; +import AutoMergeFailed from './components/states/mr_widget_auto_merge_failed.vue'; +import CheckingState from './components/states/mr_widget_checking.vue'; +import MRWidgetStore from './stores/ee_switch_mr_widget_store'; +import MRWidgetService from './services/ee_switch_mr_widget_service'; +import eventHub from './event_hub'; +import stateMaps from './stores/ee_switch_state_maps'; +import SquashBeforeMerge from './components/states/squash_before_merge.vue'; +import notify from '~/lib/utils/notify'; +import SourceBranchRemovalStatus from './components/source_branch_removal_status.vue'; import GroupedTestReportsApp from '../reports/components/grouped_test_reports_app.vue'; import { setFaviconOverlay } from '../lib/utils/common_utils'; @@ -82,6 +82,7 @@ export default { const service = this.createService(store); return { mr: store, + state: store.state, service, }; }, @@ -105,6 +106,17 @@ export default { (!this.mr.isNothingToMergeState && !this.mr.isMergedState) ); }, + shouldRenderMergedPipeline() { + return this.mr.state === 'merged' && !_.isEmpty(this.mr.mergePipeline); + }, + }, + watch: { + state(newVal, oldVal) { + if (newVal !== oldVal && this.shouldRenderMergedPipeline) { + // init polling + this.initPostMergeDeploymentsPolling(); + } + }, }, created() { this.initPolling(); @@ -114,11 +126,19 @@ export default { mounted() { this.setFaviconHelper(); this.initDeploymentsPolling(); + + if (this.shouldRenderMergedPipeline) { + this.initPostMergeDeploymentsPolling(); + } }, beforeDestroy() { eventHub.$off('mr.discussion.updated', this.checkStatus); this.pollingInterval.destroy(); this.deploymentsInterval.destroy(); + + if (this.postMergeDeploymentsInterval) { + this.postMergeDeploymentsInterval.destroy(); + } }, methods: { createService(store) { @@ -148,7 +168,13 @@ export default { cb.call(null, data); } }) - .catch(() => createFlash('Something went wrong. Please try again.')); + .catch(() => createFlash(__('Something went wrong. Please try again.'))); + }, + setFaviconHelper() { + if (this.mr.ciStatusFaviconPath) { + return setFaviconOverlay(this.mr.ciStatusFaviconPath); + } + return Promise.resolve(); }, initPolling() { this.pollingInterval = new SmartInterval({ @@ -160,8 +186,14 @@ export default { }); }, initDeploymentsPolling() { - this.deploymentsInterval = new SmartInterval({ - callback: this.fetchDeployments, + this.deploymentsInterval = this.deploymentsPoll(this.fetchPreMergeDeployments); + }, + initPostMergeDeploymentsPolling() { + this.postMergeDeploymentsInterval = this.deploymentsPoll(this.fetchPostMergeDeployments); + }, + deploymentsPoll(callback) { + return new SmartInterval({ + callback, startingInterval: 30000, maxInterval: 120000, hiddenInterval: 240000, @@ -169,26 +201,33 @@ export default { immediateExecution: true, }); }, - setFaviconHelper() { - if (this.mr.ciStatusFaviconPath) { - return setFaviconOverlay(this.mr.ciStatusFaviconPath); - } - return Promise.resolve(); + fetchDeployments(target) { + return this.service.fetchDeployments(target); }, - fetchDeployments() { - return this.service - .fetchDeployments() - .then(res => res.data) - .then(data => { + fetchPreMergeDeployments() { + return this.fetchDeployments() + .then(({ data }) => { if (data.length) { this.mr.deployments = data; } }) - .catch(() => { - createFlash( - 'Something went wrong while fetching the environments for this merge request. Please try again.', - ); - }); + .catch(() => this.throwDeploymentsError()); + }, + fetchPostMergeDeployments() { + return this.fetchDeployments('merge_commit') + .then(({ data }) => { + if (data.length) { + this.mr.postMergeDeployments = data; + } + }) + .catch(() => this.throwDeploymentsError()); + }, + throwDeploymentsError() { + createFlash( + __( + 'Something went wrong while fetching the environments for this merge request. Please try again.', + ), + ); }, fetchActionsContent() { this.service @@ -201,7 +240,7 @@ export default { Project.initRefSwitcher(); } }) - .catch(() => createFlash('Something went wrong. Please try again.')); + .catch(() => createFlash(__('Something went wrong. Please try again.'))); }, handleNotification(data) { if (data.ci_status === this.mr.ciStatus) return; @@ -266,10 +305,12 @@ export default { :has-ci="mr.hasCI" :source-branch="mr.sourceBranch" :source-branch-link="mr.sourceBranchLink" + :troubleshooting-docs-path="mr.troubleshootingDocsPath" /> <deployment v-for="deployment in mr.deployments" - :key="deployment.id" + :key="`pre-merge-deploy-${deployment.id}`" + class="js-pre-merge-deploy" :deployment="deployment" /> <div class="mr-section-container"> @@ -310,5 +351,23 @@ export default { <mr-widget-merge-help /> </div> </div> + + <template v-if="shouldRenderMergedPipeline"> + <mr-widget-pipeline + class="js-post-merge-pipeline prepend-top-default" + :pipeline="mr.mergePipeline" + :ci-status="mr.ciStatus" + :has-ci="mr.hasCI" + :source-branch="mr.targetBranch" + :source-branch-link="mr.targetBranch" + :troubleshooting-docs-path="mr.troubleshootingDocsPath" + /> + <deployment + v-for="postMergeDeployment in mr.postMergeDeployments" + :key="`post-merge-deploy-${postMergeDeployment.id}`" + :deployment="postMergeDeployment" + class="js-post-deployment" + /> + </template> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js new file mode 100644 index 00000000000..ea2aabb78fe --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js @@ -0,0 +1,3 @@ +import MRWidgetService from './mr_widget_service'; + +export default MRWidgetService; diff --git a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js index fecbfec2214..0bb70bfd658 100644 --- a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js +++ b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js @@ -21,8 +21,12 @@ export default class MRWidgetService { return axios.delete(this.endpoints.sourceBranchPath); } - fetchDeployments() { - return axios.get(this.endpoints.ciEnvironmentsStatusPath); + fetchDeployments(targetParam) { + return axios.get(this.endpoints.ciEnvironmentsStatusPath, { + params: { + environment_target: targetParam, + }, + }); } poll() { diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js new file mode 100644 index 00000000000..ebef30e3eab --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js @@ -0,0 +1,3 @@ +import getStateKey from './get_state_key'; + +export default getStateKey; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js new file mode 100644 index 00000000000..92a07c53f2d --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js @@ -0,0 +1,3 @@ +import MergeRequestStore from './mr_widget_store'; + +export default MergeRequestStore; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js new file mode 100644 index 00000000000..50cf9503ea7 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js @@ -0,0 +1,3 @@ +import stateMaps from './state_maps'; + +export default stateMaps; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index 672e5280b5e..5c9a7133a6e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -1,5 +1,5 @@ import Timeago from 'timeago.js'; -import { getStateKey } from '../dependencies'; +import getStateKey from './ee_switch_get_state_key'; import { stateKey } from './state_maps'; import { formatDate } from '../../lib/utils/datetime_utility'; @@ -18,6 +18,7 @@ export default class MergeRequestStore { this.squash = data.squash; this.squashBeforeMergeHelpPath = this.squashBeforeMergeHelpPath || data.squash_before_merge_help_path; + this.troubleshootingDocsPath = this.troubleshootingDocsPath || data.troubleshooting_docs_path; this.enableSquashBeforeMerge = this.enableSquashBeforeMerge || true; this.iid = data.iid; @@ -32,7 +33,9 @@ export default class MergeRequestStore { this.commitsCount = data.commits_count; this.divergedCommitsCount = data.diverged_commits_count; this.pipeline = data.pipeline || {}; + this.mergePipeline = data.merge_pipeline || {}; this.deployments = this.deployments || data.deployments || []; + this.postMergeDeployments = this.postMergeDeployments || []; this.initRebase(data); if (data.issues_links) { diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue index 13bca99dcb3..151eee75d44 100644 --- a/app/assets/javascripts/vue_shared/components/commit.vue +++ b/app/assets/javascripts/vue_shared/components/commit.vue @@ -13,7 +13,7 @@ export default { }, props: { /** - * Indicates the existance of a tag. + * Indicates the existence of a tag. * Used to render the correct icon, if true will render `fa-tag` icon, * if false will render a svg sprite fork icon */ diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue index a07d63a495d..c78b96695cf 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue @@ -1,11 +1,11 @@ <script> -import { Link } from '@gitlab-org/gitlab-ui'; +import { GlLink } from '@gitlab-org/gitlab-ui'; import Icon from '../../icon.vue'; import { numberToHumanSize } from '../../../../lib/utils/number_utils'; export default { components: { - 'gl-link': Link, + GlLink, Icon, }, props: { 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 807e049caf6..419987d2c50 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 @@ -2,14 +2,14 @@ import axios from '~/lib/utils/axios_utils'; import { __ } from '~/locale'; import $ from 'jquery'; -import { SkeletonLoading } from '@gitlab-org/gitlab-ui'; +import { GlSkeletonLoading } from '@gitlab-org/gitlab-ui'; const { CancelToken } = axios; let axiosSource; export default { components: { - SkeletonLoading, + GlSkeletonLoading, }, props: { content: { @@ -81,7 +81,7 @@ export default { <div ref="markdown-preview" class="md md-previewer"> - <skeleton-loading v-if="isLoading" /> + <gl-skeleton-loading v-if="isLoading" /> <div v-else v-html="previewContent"> diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue index 36a345130c0..2d89a156117 100644 --- a/app/assets/javascripts/vue_shared/components/file_row.vue +++ b/app/assets/javascripts/vue_shared/components/file_row.vue @@ -34,10 +34,21 @@ export default { required: false, default: false, }, + displayTextKey: { + type: String, + required: false, + default: 'name', + }, + shouldTruncateStart: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { mouseOver: false, + truncateStart: 0, }; }, computed: { @@ -60,6 +71,15 @@ export default { 'is-open': this.file.opened, }; }, + outputText() { + const text = this.file[this.displayTextKey]; + + if (this.truncateStart === 0) { + return text; + } + + return `...${text.substring(this.truncateStart, text.length)}`; + }, }, watch: { 'file.active': function fileActiveWatch(active) { @@ -72,6 +92,15 @@ export default { if (this.hasPathAtCurrentRoute()) { this.scrollIntoView(true); } + + if (this.shouldTruncateStart) { + const { scrollWidth, offsetWidth } = this.$refs.textOutput; + const textOverflow = scrollWidth - offsetWidth; + + if (textOverflow > 0) { + this.truncateStart = Math.ceil(textOverflow / 5) + 3; + } + } }, methods: { toggleTreeOpen(path) { @@ -139,6 +168,7 @@ export default { class="file-row-name-container" > <span + ref="textOutput" :style="levelIndentation" class="file-row-name str-truncated" > @@ -156,7 +186,7 @@ export default { :size="16" class="append-right-5" /> - {{ file.name }} + {{ outputText }} </span> <component :is="extraComponent" @@ -175,6 +205,8 @@ export default { :hide-extra-on-tree="hideExtraOnTree" :extra-component="extraComponent" :show-changed-icon="showChangedIcon" + :display-text-key="displayTextKey" + :should-truncate-start="shouldTruncateStart" @toggleTreeOpen="toggleTreeOpen" @clickFile="clickedFile" /> diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue b/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue index 460fa6ad72e..388a2f4ca36 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue @@ -56,12 +56,14 @@ export default { filteredResults() { if (this.filter !== '') { return this.items.filter( - item => item[this.filterKey] && item[this.filterKey].toLowerCase().includes(this.filter.toLowerCase()), + item => + item[this.filterKey] && + item[this.filterKey].toLowerCase().includes(this.filter.toLowerCase()), ); } return this.items.slice(0, this.visibleItems); - } + }, }, mounted() { /** diff --git a/app/assets/javascripts/vue_shared/components/gl_countdown.vue b/app/assets/javascripts/vue_shared/components/gl_countdown.vue new file mode 100644 index 00000000000..9327a2a4a6c --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/gl_countdown.vue @@ -0,0 +1,49 @@ +<script> +import { calculateRemainingMilliseconds, formatTime } from '~/lib/utils/datetime_utility'; + +/** + * Counts down to a given end date. + */ +export default { + props: { + endDateString: { + type: String, + required: true, + validator(value) { + return !Number.isNaN(new Date(value).getTime()); + }, + }, + }, + + data() { + return { + remainingTime: formatTime(0), + countdownUpdateIntervalId: null, + }; + }, + + mounted() { + const updateRemainingTime = () => { + const remainingMilliseconds = calculateRemainingMilliseconds(this.endDateString); + this.remainingTime = formatTime(remainingMilliseconds); + }; + + updateRemainingTime(); + this.countdownUpdateIntervalId = window.setInterval(updateRemainingTime, 1000); + }, + + beforeDestroy() { + window.clearInterval(this.countdownUpdateIntervalId); + }, +}; +</script> + +<template> + <time + v-gl-tooltip + :datetime="endDateString" + :title="endDateString" + > + {{ remainingTime }} + </time> +</template> diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue index 26f9d5ddc91..cddebfae115 100644 --- a/app/assets/javascripts/vue_shared/components/icon.vue +++ b/app/assets/javascripts/vue_shared/components/icon.vue @@ -8,7 +8,7 @@ let iconValidator = () => true; */ if (process.env.NODE_ENV !== 'production') { // eslint-disable-next-line global-require - const data = require('@gitlab-org/gitlab-svgs/dist/icons.json'); + const data = require('@gitlab/svgs/dist/icons.json'); const { icons } = data; iconValidator = value => { if (icons.includes(value)) { diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue index feb7b8f227e..b0a93794013 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue @@ -1,9 +1,9 @@ <script> -import { Link } from '@gitlab-org/gitlab-ui'; +import { GlLink } from '@gitlab-org/gitlab-ui'; export default { components: { - 'gl-link': Link, + GlLink, }, props: { markdownDocsPath: { diff --git a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue index 1d9c9220469..f56414c3c63 100644 --- a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue @@ -1,10 +1,10 @@ <script> -import { SkeletonLoading } from '@gitlab-org/gitlab-ui'; +import { GlSkeletonLoading } from '@gitlab-org/gitlab-ui'; export default { name: 'SkeletonNote', components: { - SkeletonLoading, + GlSkeletonLoading, }, }; </script> @@ -17,7 +17,7 @@ export default { <div class="timeline-content"> <div class="note-header"></div> <div class="note-body"> - <skeleton-loading /> + <gl-skeleton-loading /> </div> </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/pagination_links.vue b/app/assets/javascripts/vue_shared/components/pagination_links.vue index 1f2a679c145..89dcf049f6e 100644 --- a/app/assets/javascripts/vue_shared/components/pagination_links.vue +++ b/app/assets/javascripts/vue_shared/components/pagination_links.vue @@ -1,7 +1,11 @@ <script> +import { GlPagination } from '@gitlab-org/gitlab-ui'; import { s__ } from '../../locale'; export default { + components: { + GlPagination, + }, props: { change: { type: Function, diff --git a/app/assets/javascripts/vue_shared/components/pikaday.vue b/app/assets/javascripts/vue_shared/components/pikaday.vue index 782d8e3abf6..26c99aecae4 100644 --- a/app/assets/javascripts/vue_shared/components/pikaday.vue +++ b/app/assets/javascripts/vue_shared/components/pikaday.vue @@ -1,6 +1,6 @@ <script> import Pikaday from 'pikaday'; -import { parsePikadayDate, pikadayToString } from '../../lib/utils/datefix'; +import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility'; export default { name: 'DatePicker', diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue index 7f1eb6bcec4..5841db52704 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue @@ -1,34 +1,50 @@ <script> - export default { - name: 'CollapsedCalendarIcon', - props: { - containerClass: { - type: String, - required: false, - default: '', - }, - text: { - type: String, - required: false, - default: '', - }, - showIcon: { - type: Boolean, - required: false, - default: true, - }, +import tooltip from '~/vue_shared/directives/tooltip'; + +export default { + name: 'CollapsedCalendarIcon', + directives: { + tooltip, + }, + props: { + containerClass: { + type: String, + required: false, + default: '', + }, + text: { + type: String, + required: false, + default: '', + }, + showIcon: { + type: Boolean, + required: false, + default: true, + }, + tooltipText: { + type: String, + required: false, + default: '', }, - methods: { - click() { - this.$emit('click'); - }, + }, + methods: { + click() { + this.$emit('click'); }, - }; + }, +}; </script> <template> <div + v-tooltip :class="containerClass" + :title="tooltipText" + data-container="body" + data-placement="left" + data-html="true" + data-boundary="viewport" @click="click" > <i diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue index dac438a702d..174c29809ac 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue @@ -1,88 +1,87 @@ <script> - import { dateInWords } from '../../../lib/utils/datetime_utility'; - import toggleSidebar from './toggle_sidebar.vue'; - import collapsedCalendarIcon from './collapsed_calendar_icon.vue'; +import { __ } from '~/locale'; +import timeagoMixin from '~/vue_shared/mixins/timeago'; +import { dateInWords, timeFor } from '~/lib/utils/datetime_utility'; +import collapsedCalendarIcon from './collapsed_calendar_icon.vue'; - export default { - name: 'SidebarCollapsedGroupedDatePicker', - components: { - toggleSidebar, - collapsedCalendarIcon, +export default { + name: 'SidebarCollapsedGroupedDatePicker', + components: { + collapsedCalendarIcon, + }, + mixins: [timeagoMixin], + props: { + collapsed: { + type: Boolean, + required: false, + default: true, }, - props: { - collapsed: { - type: Boolean, - required: false, - default: true, - }, - showToggleSidebar: { - type: Boolean, - required: false, - default: false, - }, - minDate: { - type: Date, - required: false, - default: null, - }, - maxDate: { - type: Date, - required: false, - default: null, - }, - disableClickableIcons: { - type: Boolean, - required: false, - default: false, - }, + minDate: { + type: Date, + required: false, + default: null, }, - computed: { - hasMinAndMaxDates() { - return this.minDate && this.maxDate; - }, - hasNoMinAndMaxDates() { - return !this.minDate && !this.maxDate; - }, - showMinDateBlock() { - return this.minDate || this.hasNoMinAndMaxDates; - }, - showFromText() { - return !this.maxDate && this.minDate; - }, - iconClass() { - const disabledClass = this.disableClickableIcons ? 'disabled' : ''; - return `block sidebar-collapsed-icon calendar-icon ${disabledClass}`; - }, + maxDate: { + type: Date, + required: false, + default: null, }, - methods: { - toggleSidebar() { - this.$emit('toggleCollapse'); - }, - dateText(dateType = 'min') { - const date = this[`${dateType}Date`]; - const dateWords = dateInWords(date, true); - const parsedDateWords = dateWords ? dateWords.replace(',', '') : dateWords; + disableClickableIcons: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + hasMinAndMaxDates() { + return this.minDate && this.maxDate; + }, + hasNoMinAndMaxDates() { + return !this.minDate && !this.maxDate; + }, + showMinDateBlock() { + return this.minDate || this.hasNoMinAndMaxDates; + }, + showFromText() { + return !this.maxDate && this.minDate; + }, + iconClass() { + const disabledClass = this.disableClickableIcons ? 'disabled' : ''; + return `sidebar-collapsed-icon calendar-icon ${disabledClass}`; + }, + }, + methods: { + toggleSidebar() { + this.$emit('toggleCollapse'); + }, + dateText(dateType = 'min') { + const date = this[`${dateType}Date`]; + const dateWords = dateInWords(date, true); + const parsedDateWords = dateWords ? dateWords.replace(',', '') : dateWords; + + return date ? parsedDateWords : __('None'); + }, + tooltipText(dateType = 'min') { + const defaultText = dateType === 'min' ? __('Start date') : __('Due date'); + const date = this[`${dateType}Date`]; + const timeAgo = dateType === 'min' ? this.timeFormated(date) : timeFor(date); + const dateText = date ? [this.dateText(dateType), `(${timeAgo})`].join(' ') : ''; - return date ? parsedDateWords : 'None'; - }, + if (date) { + return [defaultText, dateText].join('<br />'); + } + return __('Start and due date'); }, - }; + }, +}; </script> <template> <div class="block sidebar-grouped-item"> - <div - v-if="showToggleSidebar" - class="issuable-sidebar-header" - > - <toggle-sidebar - :collapsed="collapsed" - @toggle="toggleSidebar" - /> - </div> <collapsed-calendar-icon v-if="showMinDateBlock" :container-class="iconClass" + :tooltip-text="tooltipText('min')" @click="toggleSidebar" > <span class="sidebar-collapsed-value"> @@ -99,7 +98,7 @@ <collapsed-calendar-icon v-if="maxDate" :container-class="iconClass" - :show-icon="!minDate" + :tooltip-text="tooltipText('max')" @click="toggleSidebar" > <span class="sidebar-collapsed-value"> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue index af297f3c408..0d5fc07e6e3 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue @@ -14,7 +14,10 @@ export default { }, computed: { labelsList() { - const labelsString = this.labels.slice(0, 5).map(label => label.title).join(', '); + const labelsString = this.labels + .slice(0, 5) + .map(label => label.title) + .join(', '); if (this.labels.length > 5) { return sprintf(s__('LabelSelect|%{labelsString}, and %{remainingLabelCount} more'), { diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue index 14cb44b8619..86c7498a092 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue @@ -17,14 +17,14 @@ */ -import { Link } from '@gitlab-org/gitlab-ui'; +import { GlLink } from '@gitlab-org/gitlab-ui'; import userAvatarImage from './user_avatar_image.vue'; import tooltip from '../../directives/tooltip'; export default { name: 'UserAvatarLink', components: { - 'gl-link': Link, + GlLink, userAvatarImage, }, directives: { diff --git a/app/assets/javascripts/vue_shared/directives/tooltip.js b/app/assets/javascripts/vue_shared/directives/tooltip.js index 4f2412ce520..549d27e96d9 100644 --- a/app/assets/javascripts/vue_shared/directives/tooltip.js +++ b/app/assets/javascripts/vue_shared/directives/tooltip.js @@ -9,6 +9,14 @@ export default { componentUpdated(el) { $(el).tooltip('_fixTitle'); + + // update visible tooltips + const tooltipInstance = $(el).data('bs.tooltip'); + const tip = tooltipInstance.getTipElement(); + tooltipInstance.setElementContent( + $(tip.querySelectorAll('.tooltip-inner')), + tooltipInstance.getTitle(), + ); }, unbind(el) { diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 3c9505a21d6..fa753b13e5f 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -334,6 +334,14 @@ img.emoji { } } +.outline-0 { + outline: 0; + + &:focus { + outline: 0; + } +} + /** COMMON CLASSES **/ .prepend-top-0 { margin-top: 0; } .prepend-top-2 { margin-top: 2px; } @@ -369,3 +377,5 @@ img.emoji { .flex-align-self-center { align-self: center; } .flex-grow { flex-grow: 1; } .flex-no-shrink { flex-shrink: 0; } +.mw-460 { max-width: 460px; } +.ws-initial { white-space: initial; } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index cdfad30e7ca..dca89981d81 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -158,7 +158,7 @@ color: $gl-text-color; outline: 0; - // make sure the text color is not overriden + // make sure the text color is not overridden &.text-danger { color: $brand-danger; } @@ -184,7 +184,7 @@ text-align: left; width: 100%; - // make sure the text color is not overriden + // make sure the text color is not overridden &.text-danger { color: $brand-danger; } diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 1c84baf68ed..c030d75f5a4 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -250,6 +250,100 @@ max-width: 100%; } +/* +* Mixin that handles the container for the job logs (CI/CD and kubernetes pod logs) +*/ +@mixin build-trace { + background: $black; + color: $gray-darkest; + white-space: pre; + overflow-x: auto; + font-size: 12px; + border-radius: 0; + border: 0; + padding: $grid-size; + + .bash { + display: block; + } + + &.build-trace-rounded { + border-radius: $border-radius-base; + } +} + +@mixin build-trace-top-bar($height) { + height: $height; + min-height: $height; + background: $gray-light; + border: 1px solid $border-color; + color: $gl-text-color; + position: sticky; + position: -webkit-sticky; + top: $header-height; + padding: $grid-size; + + .with-performance-bar & { + top: $header-height + $performance-bar-height; + } +} + +/* +* Mixin that handles the position of the controls placed on the top bar +*/ +@mixin build-controllers($control-font-size, $flex-direction, $with-grow, $flex-grow-size) { + display: flex; + font-size: $control-font-size; + justify-content: $flex-direction; + align-items: center; + align-self: baseline; + @if $with-grow { + flex-grow: $flex-grow-size; + } + + svg { + width: 15px; + height: 15px; + display: block; + fill: $gl-text-color; + } + + .controllers-buttons { + color: $gl-text-color; + margin: 0 $grid-size; + + &:last-child { + margin-right: 0; + } + } + + .btn-scroll.animate { + .first-triangle { + animation: blinking-scroll-button 1s ease infinite; + animation-delay: 0.3s; + } + + .second-triangle { + animation: blinking-scroll-button 1s ease infinite; + animation-delay: 0.2s; + } + + .third-triangle { + animation: blinking-scroll-button 1s ease infinite; + } + + &:disabled { + opacity: 1; + } + } + + .btn-scroll:disabled, + .btn-refresh:disabled { + opacity: 0.35; + cursor: not-allowed; + } +} + @mixin build-loader-animation { position: relative; white-space: initial; diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss index bf6f66d30ff..de9e7c37695 100644 --- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss +++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss @@ -1,6 +1,6 @@ // For tabbed navigation links, scrolling tabs, etc. For all top/main navigation, // please check nav.scss -.nav-links { +.nav-links:not(.quick-links) { display: flex; padding: 0; margin: 0; @@ -37,6 +37,7 @@ button { padding-top: 0; + background-color: transparent; } &.active a, @@ -105,7 +106,7 @@ display: inline-block; float: right; text-align: right; - padding: 11px 0; + padding: $gl-padding-8 0; margin-bottom: 0; > .btn, diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 6d891e21556..e261bd7c0ca 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -34,7 +34,7 @@ margin-bottom: 0; } - *:first-child:not(.katex-display) { + *:first-child { margin-top: 0; } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 227f49ec595..31b258e56dd 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -50,35 +50,13 @@ position: relative; } - .build-trace { - background: $black; - color: $gray-darkest; - white-space: pre; - overflow-x: auto; - font-size: 12px; - border-radius: 0; - border: 0; - padding: $grid-size; - - .bash { - display: block; - } - &.build-trace-rounded { - border-radius: $border-radius-base; - } + .build-trace { + @include build-trace(); } .top-bar { - height: 35px; - min-height: 35px; - background: $gray-light; - border: 1px solid $border-color; - color: $gl-text-color; - position: sticky; - position: -webkit-sticky; - top: $header-height; - padding: $grid-size; + @include build-trace-top-bar(35px); &.affix { top: $header-height; @@ -116,49 +94,7 @@ } .controllers { - display: flex; - justify-content: center; - align-items: center; - - svg { - height: 15px; - display: block; - fill: $gl-text-color; - } - - .controllers-buttons { - color: $gl-text-color; - margin: 0 $grid-size; - - &:last-child { - margin-right: 0; - } - } - - .btn-scroll.animate { - .first-triangle { - animation: blinking-scroll-button 1s ease infinite; - animation-delay: 0.3s; - } - - .second-triangle { - animation: blinking-scroll-button 1s ease infinite; - animation-delay: 0.2s; - } - - .third-triangle { - animation: blinking-scroll-button 1s ease infinite; - } - - &:disabled { - opacity: 1; - } - } - - .btn-scroll:disabled { - opacity: 0.35; - cursor: not-allowed; - } + @include build-controllers(15px, center, false, 0); } } @@ -183,12 +119,8 @@ } .with-performance-bar .build-page { - .top-bar { + .top-bar.affix { top: $header-height + $performance-bar-height; - - &.affix { - top: $header-height + $performance-bar-height; - } } } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 8d884ad6891..52c91266ff4 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -1027,8 +1027,12 @@ overflow-x: auto; } -.tree-list-search .form-control { - padding-left: 30px; +.tree-list-search { + flex: 0 0 34px; + + .form-control { + padding-left: 30px; + } } .tree-list-icon { @@ -1063,3 +1067,9 @@ } } } + +.tree-list-view-toggle { + svg { + top: 0; + } +} diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb index fdd3b4126ff..e3226c86b0b 100644 --- a/app/controllers/admin/appearances_controller.rb +++ b/app/controllers/admin/appearances_controller.rb @@ -33,21 +33,21 @@ class Admin::AppearancesController < Admin::ApplicationController @appearance.save - redirect_to admin_appearances_path, notice: 'Logo was succesfully removed.' + redirect_to admin_appearances_path, notice: 'Logo was successfully removed.' end def header_logos @appearance.remove_header_logo! @appearance.save - redirect_to admin_appearances_path, notice: 'Header logo was succesfully removed.' + redirect_to admin_appearances_path, notice: 'Header logo was successfully removed.' end def favicon @appearance.remove_favicon! @appearance.save - redirect_to admin_appearances_path, notice: 'Favicon was succesfully removed.' + redirect_to admin_appearances_path, notice: 'Favicon was successfully removed.' end private diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 8040a14ef56..8f683ca06ad 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -61,7 +61,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController format.html do usage_data_json = JSON.pretty_generate(Gitlab::UsageData.data) - render html: Gitlab::Highlight.highlight('payload.json', usage_data_json) + render html: Gitlab::Highlight.highlight('payload.json', usage_data_json, language: 'json') end format.json { render json: Gitlab::UsageData.to_json } end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index eeabcc0c9bb..7f4aa8244ac 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -46,6 +46,8 @@ class ApplicationController < ActionController::Base :git_import_enabled?, :gitlab_project_import_enabled?, :manifest_import_enabled? + DEFAULT_GITLAB_CACHE_CONTROL = "#{ActionDispatch::Http::Cache::Response::DEFAULT_CACHE_CONTROL}, no-store".freeze + rescue_from Encoding::CompatibilityError do |exception| log_exception(exception) render "errors/encoding", layout: "errors", status: 500 @@ -244,6 +246,13 @@ class ApplicationController < ActionController::Base headers['X-XSS-Protection'] = '1; mode=block' headers['X-UA-Compatible'] = 'IE=edge' headers['X-Content-Type-Options'] = 'nosniff' + + if current_user + # Adds `no-store` to the DEFAULT_CACHE_CONTROL, to prevent security + # concerns due to caching private data. + headers['Cache-Control'] = DEFAULT_GITLAB_CACHE_CONTROL + headers["Pragma"] = "no-cache" # HTTP 1.0 compatibility + end end def validate_user_service_ticket! diff --git a/app/controllers/concerns/boards_responses.rb b/app/controllers/concerns/boards_responses.rb index b7e4f9b81f1..3cdf4ddf8bb 100644 --- a/app/controllers/concerns/boards_responses.rb +++ b/app/controllers/concerns/boards_responses.rb @@ -50,7 +50,10 @@ module BoardsResponses end def authorize_create_issue - authorize_action_for!(project, :admin_issue) + list = List.find(issue_params[:list_id]) + action = list.backlog? ? :create_issue : :admin_issue + + authorize_action_for!(project, action) end def authorize_admin_list diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index b3777fd2b0f..f644702cbdb 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -86,10 +86,10 @@ module CreatesCommit def new_merge_request_path project_new_merge_request_path( @project_to_commit_into, + merge_request_source_branch: @branch_name, merge_request: { source_project_id: @project_to_commit_into.id, target_project_id: @project.id, - source_branch: @branch_name, target_branch: @start_branch } ) diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb index 6e17bc212e4..3802aa5f40f 100644 --- a/app/controllers/dashboard/milestones_controller.rb +++ b/app/controllers/dashboard/milestones_controller.rb @@ -4,12 +4,13 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController include MilestoneActions before_action :projects + before_action :groups, only: :index before_action :milestone, only: [:show, :merge_requests, :participants, :labels] def index respond_to do |format| format.html do - @milestone_states = GlobalMilestone.states_count(@projects) + @milestone_states = Milestone.states_count(@projects.select(:id), @groups.select(:id)) @milestones = Kaminari.paginate_array(milestones).page(params[:page]) end format.json do @@ -42,4 +43,8 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController @milestone = DashboardMilestone.build(@projects, params[:title]) render_404 unless @milestone end + + def groups + @groups ||= GroupsFinder.new(current_user, state_all: true).execute + end end diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb index 8d259b4052e..cdc6f53df8e 100644 --- a/app/controllers/groups/boards_controller.rb +++ b/app/controllers/groups/boards_controller.rb @@ -5,6 +5,7 @@ class Groups::BoardsController < Groups::ApplicationController before_action :assign_endpoint_vars before_action :boards, only: :index + before_action :redirect_to_recent_board, only: :index def index respond_with_boards @@ -13,6 +14,9 @@ class Groups::BoardsController < Groups::ApplicationController def show @board = boards.find(params[:id]) + # add/update the board in the recent visited table + Boards::Visits::CreateService.new(@board.group, current_user).execute(@board) if request.format.html? + respond_with_board end @@ -31,4 +35,18 @@ class Groups::BoardsController < Groups::ApplicationController def serialize_as_json(resource) resource.as_json(only: [:id]) end + + def includes_board?(board_id) + boards.any? { |board| board.id == board_id } + end + + def redirect_to_recent_board + return if request.format.json? + + recently_visited = Boards::Visits::LatestService.new(group, current_user).execute + + if recently_visited && includes_board?(recently_visited.board_id) + redirect_to(group_board_path(id: recently_visited.board_id), status: :found) + end + end end diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index a7cee426cf1..b42116b0f36 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -10,7 +10,7 @@ class Groups::MilestonesController < Groups::ApplicationController def index respond_to do |format| format.html do - @milestone_states = GlobalMilestone.states_count(group_projects, group) + @milestone_states = Milestone.states_count(group_projects, [group]) @milestones = Kaminari.paginate_array(milestones).page(params[:page]) end format.json do diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb index 93f3eb2be6d..c1dcc463de7 100644 --- a/app/controllers/groups/settings/ci_cd_controller.rb +++ b/app/controllers/groups/settings/ci_cd_controller.rb @@ -7,7 +7,7 @@ module Groups before_action :authorize_admin_pipeline! def show - define_secret_variables + define_ci_variables end def reset_registration_token @@ -19,7 +19,7 @@ module Groups private - def define_secret_variables + def define_ci_variables @variable = Ci::GroupVariable.new(group: group) .present(current_user: current_user) @variables = group.variables.order_key_asc diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb index 382c684a408..f067ef625aa 100644 --- a/app/controllers/import/gitea_controller.rb +++ b/app/controllers/import/gitea_controller.rb @@ -23,7 +23,7 @@ class Import::GiteaController < Import::GithubController :"#{provider}_host_url" end - # Overriden methods + # Overridden methods def provider :gitea end diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index e3eec5a020d..58565aaf8c9 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -103,7 +103,7 @@ class Import::GithubController < Import::BaseController { github_access_token: session[access_token_key] } end - # The following methods are overriden in subclasses + # The following methods are overridden in subclasses def provider :github end diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb index 894a6a431e3..705389749d8 100644 --- a/app/controllers/oauth/authorizations_controller.rb +++ b/app/controllers/oauth/authorizations_controller.rb @@ -3,7 +3,7 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController layout 'profile' - # Overriden from Doorkeeper::AuthorizationsController to + # Overridden from Doorkeeper::AuthorizationsController to # include the call to session.delete def new if pre_auth.authorizable? diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 56a884b8a2a..c02ec407262 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -92,7 +92,7 @@ class Projects::BlobController < Projects::ApplicationController apply_diff_view_cookie! @blob.load_all_data! - @lines = Gitlab::Highlight.highlight(@blob.path, @blob.data, repository: @repository).lines + @lines = @blob.present.highlight.lines @form = UnfoldForm.new(params) diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index 77b818347c7..8189b5d182a 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -8,6 +8,7 @@ class Projects::BoardsController < Projects::ApplicationController before_action :authorize_read_board!, only: [:index, :show] before_action :boards, only: :index before_action :assign_endpoint_vars + before_action :redirect_to_recent_board, only: :index def index respond_with_boards @@ -16,6 +17,9 @@ class Projects::BoardsController < Projects::ApplicationController def show @board = boards.find(params[:id]) + # add/update the board in the recent visited table + Boards::Visits::CreateService.new(@board.project, current_user).execute(@board) if request.format.html? + respond_with_board end @@ -33,10 +37,24 @@ class Projects::BoardsController < Projects::ApplicationController end def authorize_read_board! - return access_denied! unless can?(current_user, :read_board, project) + access_denied! unless can?(current_user, :read_board, project) end def serialize_as_json(resource) resource.as_json(only: [:id]) end + + def includes_board?(board_id) + boards.any? { |board| board.id == board_id } + end + + def redirect_to_recent_board + return if request.format.json? + + recently_visited = Boards::Visits::LatestService.new(project, current_user).execute + + if recently_visited && includes_board?(recently_visited.board_id) + redirect_to(namespace_project_board_path(id: recently_visited.board_id), status: :found) + end + end end diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index be708835e30..c0aa39d87c6 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -8,6 +8,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController rescue_from Gitlab::GitAccess::UnauthorizedError, with: :render_403 rescue_from Gitlab::GitAccess::NotFoundError, with: :render_404 rescue_from Gitlab::GitAccess::ProjectCreationError, with: :render_422 + rescue_from Gitlab::GitAccess::TimeoutError, with: :render_503 # GET /foo/bar.git/info/refs?service=git-upload-pack (git pull) # GET /foo/bar.git/info/refs?service=git-receive-pack (git push) @@ -62,6 +63,10 @@ class Projects::GitHttpController < Projects::GitHttpClientController render plain: exception.message, status: :unprocessable_entity end + def render_503(exception) + render plain: exception.message, status: :service_unavailable + end + def access @access ||= access_klass.new(access_actor, project, 'http', authentication_abilities: authentication_abilities, diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index b06a6f3bb0d..308f666394c 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -9,12 +9,25 @@ class Projects::IssuesController < Projects::ApplicationController include IssuesCalendar include SpammableActions - prepend_before_action :authenticate_user!, only: [:new] + def self.authenticate_user_only_actions + %i[new] + end + + def self.issue_except_actions + %i[index calendar new create bulk_update] + end + + def self.set_issuables_index_only_actions + %i[index calendar] + end + + prepend_before_action :authenticate_user!, only: authenticate_user_only_actions before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update] before_action :check_issues_available! - before_action :issue, except: [:index, :calendar, :new, :create, :bulk_update] - before_action :set_issuables_index, only: [:index, :calendar] + before_action :issue, except: issue_except_actions + + before_action :set_issuables_index, only: set_issuables_index_only_actions # Allow write(create) issue before_action :authorize_create_issue!, only: [:new, :create] diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb index 5639402a1e9..bbf662a63c8 100644 --- a/app/controllers/projects/merge_requests/creations_controller.rb +++ b/app/controllers/projects/merge_requests/creations_controller.rb @@ -89,6 +89,8 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap def build_merge_request params[:merge_request] ||= ActionController::Parameters.new(source_project: @project) + params[:merge_request][:source_branch] ||= params[:merge_request_source_branch].presence + @merge_request = ::MergeRequests::BuildService.new(project, current_user, merge_request_params.merge(diff_options: diff_options)).execute end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 757b03d0b0e..27b83da4f54 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -168,7 +168,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo end def merge - return access_denied! unless @merge_request.can_be_merged_by?(current_user) + access_check_result = merge_access_check + + return access_check_result if access_check_result status = merge! @@ -201,9 +203,11 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo end def ci_environments_status - environments = @merge_request.environments_for(current_user).map do |environment| - EnvironmentStatus.new(environment, @merge_request) - end + environments = if ci_environments_status_on_merge_result? + EnvironmentStatus.after_merge_request(@merge_request, current_user) + else + EnvironmentStatus.for_merge_request(@merge_request, current_user) + end render json: EnvironmentStatusSerializer.new(current_user: current_user).represent(environments) end @@ -241,6 +245,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo private + def ci_environments_status_on_merge_result? + params[:environment_target] == 'merge_commit' + end + def target_branch_missing? @merge_request.has_no_commits? && !@merge_request.target_branch_exists? end @@ -256,6 +264,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo return :failed end + merge_service = ::MergeRequests::MergeService.new(@project, current_user, merge_params) + + unless merge_service.hooks_validation_pass?(@merge_request) + return :hook_validation_error + end + return :sha_mismatch if params[:sha] != @merge_request.diff_head_sha @merge_request.update(merge_error: nil, squash: merge_params.fetch(:squash, false)) @@ -318,6 +332,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo access_denied! unless access_check end + def merge_access_check + access_denied! unless @merge_request.can_be_merged_by?(current_user) + end + def whitelist_query_limiting # Also see https://gitlab.com/gitlab-org/gitlab-ce/issues/42441 Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42438') diff --git a/app/controllers/projects/mirrors_controller.rb b/app/controllers/projects/mirrors_controller.rb index 78d5faf2326..53176978416 100644 --- a/app/controllers/projects/mirrors_controller.rb +++ b/app/controllers/projects/mirrors_controller.rb @@ -44,6 +44,22 @@ class Projects::MirrorsController < Projects::ApplicationController redirect_to_repository_settings(project, anchor: 'js-push-remote-settings') end + def ssh_host_keys + lookup = SshHostKey.new(project: project, url: params[:ssh_url], compare_host_keys: params[:compare_host_keys]) + + if lookup.error.present? + # Failed to read keys + render json: { message: lookup.error }, status: :bad_request + elsif lookup.known_hosts.nil? + # Still working, come back later + render body: nil, status: :no_content + else + render json: lookup + end + rescue ArgumentError => err + render json: { message: err.message }, status: :bad_request + end + private def remote_mirror diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index 3a1344651df..75e590f3f33 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -68,7 +68,7 @@ module Projects def define_variables define_runners_variables - define_secret_variables + define_ci_variables define_triggers_variables define_badges_variables define_auto_devops_variables @@ -90,7 +90,7 @@ module Projects @group_runners = ::Ci::Runner.belonging_to_parent_group_of_project(@project.id) end - def define_secret_variables + def define_ci_variables @variable = ::Ci::Variable.new(project: project) .present(current_user: current_user) @variables = project.variables.order_key_asc diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index ee438e160f2..7f4a9f5151b 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -276,7 +276,7 @@ class ProjectsController < Projects::ApplicationController # rubocop: enable CodeReuse/ActiveRecord # Render project landing depending of which features are available - # So if page is not availble in the list it renders the next page + # So if page is not available in the list it renders the next page # # pages list order: repository readme, wiki home, issues list, customize workflow def render_landing_page diff --git a/app/finders/autocomplete/users_finder.rb b/app/finders/autocomplete/users_finder.rb index e2283f3266e..45955783be9 100644 --- a/app/finders/autocomplete/users_finder.rb +++ b/app/finders/autocomplete/users_finder.rb @@ -72,7 +72,6 @@ module Autocomplete author_id.present? && current_user end - # rubocop: disable CodeReuse/ActiveRecord def find_users if project project.authorized_users.union_with_user(author_id) @@ -84,6 +83,5 @@ module Autocomplete User.none end end - # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/finders/concerns/finder_with_cross_project_access.rb b/app/finders/concerns/finder_with_cross_project_access.rb index e038636f0c4..220f62bcc7f 100644 --- a/app/finders/concerns/finder_with_cross_project_access.rb +++ b/app/finders/concerns/finder_with_cross_project_access.rb @@ -16,7 +16,6 @@ module FinderWithCrossProjectAccess end override :execute - # rubocop: disable CodeReuse/ActiveRecord def execute(*args) check = Gitlab::CrossProjectAccess.find_check(self) original = super @@ -30,7 +29,6 @@ module FinderWithCrossProjectAccess original end end - # rubocop: enable CodeReuse/ActiveRecord # We can skip the cross project check for finding indivitual records. # this would be handled by the `can?(:read_*, result)` call in `FinderMethods` diff --git a/app/finders/group_descendants_finder.rb b/app/finders/group_descendants_finder.rb index 9d57d2d3bc9..c96979619fd 100644 --- a/app/finders/group_descendants_finder.rb +++ b/app/finders/group_descendants_finder.rb @@ -131,7 +131,6 @@ class GroupDescendantsFinder .with_selects_for_list(archived: params[:archived]) end - # rubocop: disable CodeReuse/ActiveRecord def subgroups return Group.none unless Group.supports_nested_groups? @@ -145,7 +144,6 @@ class GroupDescendantsFinder groups.with_selects_for_list(archived: params[:archived]).order_by(sort) end - # rubocop: enable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/Finder def direct_child_projects diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb index a35a3ed6142..ea954f98220 100644 --- a/app/finders/groups_finder.rb +++ b/app/finders/groups_finder.rb @@ -40,7 +40,6 @@ class GroupsFinder < UnionFinder attr_reader :current_user, :params - # rubocop: disable CodeReuse/ActiveRecord def all_groups return [owned_groups] if params[:owned] return [groups_with_min_access_level] if min_access_level? @@ -52,7 +51,6 @@ class GroupsFinder < UnionFinder groups << Group.none if groups.empty? groups end - # rubocop: enable CodeReuse/ActiveRecord def groups_for_ancestors current_user.authorized_groups @@ -82,11 +80,9 @@ class GroupsFinder < UnionFinder end # rubocop: enable CodeReuse/ActiveRecord - # rubocop: disable CodeReuse/ActiveRecord def owned_groups current_user&.owned_groups || Group.none end - # rubocop: enable CodeReuse/ActiveRecord def include_public_groups? current_user.nil? || all_available? diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 8abfe0c4c17..93bef592c65 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -14,7 +14,7 @@ # project_id: integer # milestone_title: string # author_id: integer -# assignee_id: integer +# assignee_id: integer or 'None' or 'Any' # search: string # label_name: string # sort: string @@ -34,6 +34,11 @@ class IssuableFinder requires_cross_project_access unless: -> { project? } + # This is used as a common filter for None / Any + FILTER_NONE = 'none'.freeze + FILTER_ANY = 'any'.freeze + + # This is accepted as a deprecated filter and is also used in unassigning users NONE = '0'.freeze attr_accessor :current_user, :params @@ -187,11 +192,6 @@ class IssuableFinder params[:milestone_title].present? end - def filter_by_no_milestone? - milestones? && params[:milestone_title] == Milestone::None.title - end - - # rubocop: disable CodeReuse/ActiveRecord def milestones return @milestones if defined?(@milestones) @@ -212,7 +212,6 @@ class IssuableFinder Milestone.none end end - # rubocop: enable CodeReuse/ActiveRecord def labels? params[:label_name].present? @@ -222,7 +221,6 @@ class IssuableFinder labels? && params[:label_name].include?(Label::None.title) end - # rubocop: disable CodeReuse/ActiveRecord def labels return @labels if defined?(@labels) @@ -233,19 +231,13 @@ class IssuableFinder Label.none end end - # rubocop: enable CodeReuse/ActiveRecord def assignee_id? - params[:assignee_id].present? && params[:assignee_id].to_s != NONE + params[:assignee_id].present? end def assignee_username? - params[:assignee_username].present? && params[:assignee_username].to_s != NONE - end - - def no_assignee? - # Assignee_id takes precedence over assignee_username - params[:assignee_id].to_s == NONE || params[:assignee_username].to_s == NONE + params[:assignee_username].present? end # rubocop: disable CodeReuse/ActiveRecord @@ -399,18 +391,29 @@ class IssuableFinder # rubocop: disable CodeReuse/ActiveRecord def by_assignee(items) - if assignee - items = items.where(assignee_id: assignee.id) - elsif no_assignee? - items = items.where(assignee_id: nil) + if filter_by_no_assignee? + items.where(assignee_id: nil) + elsif filter_by_any_assignee? + items.where('assignee_id IS NOT NULL') + elsif assignee + items.where(assignee_id: assignee.id) elsif assignee_id? || assignee_username? # assignee not found - items = items.none + items.none + else + items end - - items end # rubocop: enable CodeReuse/ActiveRecord + def filter_by_no_assignee? + # Assignee_id takes precedence over assignee_username + [NONE, FILTER_NONE].include?(params[:assignee_id].to_s.downcase) || params[:assignee_username].to_s == NONE + end + + def filter_by_any_assignee? + params[:assignee_id].to_s.downcase == FILTER_ANY + end + # rubocop: disable CodeReuse/ActiveRecord def by_author(items) if author @@ -425,18 +428,6 @@ class IssuableFinder end # rubocop: enable CodeReuse/ActiveRecord - def filter_by_upcoming_milestone? - params[:milestone_title] == Milestone::Upcoming.name - end - - def filter_by_any_milestone? - params[:milestone_title] == Milestone::Any.title - end - - def filter_by_started_milestone? - params[:milestone_title] == Milestone::Started.name - end - # rubocop: disable CodeReuse/ActiveRecord def by_milestone(items) if milestones? @@ -458,6 +449,24 @@ class IssuableFinder end # rubocop: enable CodeReuse/ActiveRecord + def filter_by_no_milestone? + # Accepts `No Milestone` for compatibility + params[:milestone_title].to_s.downcase == FILTER_NONE || params[:milestone_title] == Milestone::None.title + end + + def filter_by_any_milestone? + # Accepts `Any Milestone` for compatibility + params[:milestone_title].to_s.downcase == FILTER_ANY || params[:milestone_title] == Milestone::Any.title + end + + def filter_by_upcoming_milestone? + params[:milestone_title] == Milestone::Upcoming.name + end + + def filter_by_started_milestone? + params[:milestone_title] == Milestone::Started.name + end + def by_label(items) return items unless labels? @@ -473,12 +482,27 @@ class IssuableFinder def by_my_reaction_emoji(items) if params[:my_reaction_emoji].present? && current_user - items = items.awarded(current_user, params[:my_reaction_emoji]) + items = + if filter_by_no_reaction? + items.not_awarded(current_user) + elsif filter_by_any_reaction? + items.awarded(current_user) + else + items.awarded(current_user, params[:my_reaction_emoji]) + end end items end + def filter_by_no_reaction? + params[:my_reaction_emoji].to_s.downcase == FILTER_NONE + end + + def filter_by_any_reaction? + params[:my_reaction_emoji].to_s.downcase == FILTER_ANY + end + def label_names if labels? params[:label_name].is_a?(String) ? params[:label_name].split(',') : params[:label_name] diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index abdc47b9866..45e494725d7 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -135,17 +135,17 @@ class IssuesFinder < IssuableFinder current_user.blank? end - # rubocop: disable CodeReuse/ActiveRecord def by_assignee(items) - if assignee - items.assigned_to(assignee) - elsif no_assignee? + if filter_by_no_assignee? items.unassigned + elsif filter_by_any_assignee? + items.assigned + elsif assignee + items.assigned_to(assignee) elsif assignee_id? || assignee_username? # assignee not found items.none else items end end - # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index d000af21be3..e523942ea4c 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -12,7 +12,6 @@ class LabelsFinder < UnionFinder @params = params end - # rubocop: disable CodeReuse/ActiveRecord def execute(skip_authorization: false) @skip_authorization = skip_authorization items = find_union(label_ids, Label) || Label.none @@ -21,7 +20,6 @@ class LabelsFinder < UnionFinder items = by_search(items) sort(items) end - # rubocop: enable CodeReuse/ActiveRecord private diff --git a/app/finders/milestones_finder.rb b/app/finders/milestones_finder.rb index 47231ea80c7..9c477978f60 100644 --- a/app/finders/milestones_finder.rb +++ b/app/finders/milestones_finder.rb @@ -20,7 +20,6 @@ class MilestonesFinder @params = params end - # rubocop: disable CodeReuse/ActiveRecord def execute return Milestone.none if project_ids.empty? && group_ids.empty? @@ -31,7 +30,6 @@ class MilestonesFinder order(items) end - # rubocop: enable CodeReuse/ActiveRecord private diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb index 5beea92689f..81fd3b7a547 100644 --- a/app/finders/personal_access_tokens_finder.rb +++ b/app/finders/personal_access_tokens_finder.rb @@ -3,7 +3,7 @@ class PersonalAccessTokensFinder attr_accessor :params - delegate :build, :find, :find_by, to: :execute + delegate :build, :find, :find_by, :find_by_token, to: :execute def initialize(params = {}) @params = params diff --git a/app/finders/pipelines_finder.rb b/app/finders/pipelines_finder.rb index 3d0d3219a94..35d0e1acce5 100644 --- a/app/finders/pipelines_finder.rb +++ b/app/finders/pipelines_finder.rb @@ -12,7 +12,6 @@ class PipelinesFinder @params = params end - # rubocop: disable CodeReuse/ActiveRecord def execute unless Ability.allowed?(current_user, :read_pipeline, project) return Ci::Pipeline.none @@ -28,7 +27,6 @@ class PipelinesFinder items = by_yaml_errors(items) sort_items(items) end - # rubocop: enable CodeReuse/ActiveRecord private diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index 6ececcd4152..93d3c991846 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -88,7 +88,6 @@ class ProjectsFinder < UnionFinder # rubocop: enable CodeReuse/ActiveRecord # Builds a collection for an anonymous user. - # rubocop: disable CodeReuse/ActiveRecord def collection_without_user if private_only? || owned_projects? || min_access_level? Project.none @@ -96,7 +95,6 @@ class ProjectsFinder < UnionFinder Project.public_to_user end end - # rubocop: enable CodeReuse/ActiveRecord def owned_projects? params[:owned].present? diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index 3528e4228b2..f90971bb9f6 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -43,7 +43,6 @@ class SnippetsFinder < UnionFinder end end - # rubocop: disable CodeReuse/ActiveRecord def authorized_snippets_from_project if can?(current_user, :read_project_snippet, project) if project.team.member?(current_user) @@ -55,7 +54,6 @@ class SnippetsFinder < UnionFinder Snippet.none end end - # rubocop: enable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord def authorized_snippets diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index ff7f1e3a9aa..638744a1426 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true module BlobHelper - def highlight(blob_name, blob_content, repository: nil, plain: false) - plain ||= blob_content.length > Blob::MAXIMUM_TEXT_HIGHLIGHT_SIZE - highlighted = Gitlab::Highlight.highlight(blob_name, blob_content, plain: plain, repository: repository) + def highlight(file_name, file_content, language: nil, plain: false) + highlighted = Gitlab::Highlight.highlight(file_name, file_content, plain: plain, language: language) raw %(<pre class="code highlight"><code>#{highlighted}</code></pre>) end diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb index 9ece8b0bc5b..57e397f6ca0 100644 --- a/app/helpers/compare_helper.rb +++ b/app/helpers/compare_helper.rb @@ -13,8 +13,8 @@ module CompareHelper def create_mr_path(from = params[:from], to = params[:to], project = @project) project_new_merge_request_path( project, + merge_request_source_branch: to, merge_request: { - source_branch: to, target_branch: from } ) diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index f573fd399a5..0c313e9e6d3 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -1,6 +1,15 @@ # frozen_string_literal: true module GroupsHelper + def group_overview_nav_link_paths + %w[ + groups#show + groups#activity + groups#subgroups + analytics#show + ] + end + def group_nav_link_paths %w[groups#projects groups#edit badges#index ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index] end diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 037004327b9..910c9e9446f 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -153,6 +153,6 @@ module IconsHelper private def known_sprites - @known_sprites ||= JSON.parse(File.read(Rails.root.join('node_modules/@gitlab-org/gitlab-svgs/dist/icons.json')))['icons'] + @known_sprites ||= JSON.parse(File.read(Rails.root.join('node_modules/@gitlab/svgs/dist/icons.json')))['icons'] end end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 23d7aa427bb..8f549bfce73 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -11,10 +11,10 @@ module MergeRequestsHelper def new_mr_from_push_event(event, target_project) { + merge_request_source_branch: event.branch_name, merge_request: { source_project_id: event.project.id, target_project_id: target_project.id, - source_branch: event.branch_name, target_branch: target_project.repository.root_ref } } @@ -51,10 +51,10 @@ module MergeRequestsHelper def mr_change_branches_path(merge_request) project_new_merge_request_path( @project, + merge_request_source_branch: merge_request.source_branch, merge_request: { source_project_id: merge_request.source_project_id, target_project_id: merge_request.target_project_id, - source_branch: merge_request.source_branch, target_branch: merge_request.target_branch }, change_branches: true diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index b33c074d1af..5038dcf9746 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -10,7 +10,7 @@ module PageLayoutHelper @breadcrumb_title = @page_title.last end - # Segments are seperated by middot + # Segments are separated by middot @page_title.join(" · ") end diff --git a/app/models/blob.rb b/app/models/blob.rb index 31a839274b5..4f310e70f4f 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true -# Blob is a Rails-specific wrapper around Gitlab::Git::Blob objects +# Blob is a Rails-specific wrapper around Gitlab::Git::Blob, SnippetBlob and Ci::ArtifactBlob class Blob < SimpleDelegator + include Presentable + include BlobLanguageFromGitAttributes + CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour - MAXIMUM_TEXT_HIGHLIGHT_SIZE = 1.megabyte - # Finding a viewer for a blob happens based only on extension and whether the # blob is binary or text, which means 1 blob should only be matched by 1 viewer, # and the order of these viewers doesn't really matter. @@ -121,10 +122,6 @@ class Blob < SimpleDelegator end end - def no_highlighting? - raw_size && raw_size > MAXIMUM_TEXT_HIGHLIGHT_SIZE - end - def empty? raw_size == 0 end diff --git a/app/models/board_group_recent_visit.rb b/app/models/board_group_recent_visit.rb new file mode 100644 index 00000000000..92abbb67222 --- /dev/null +++ b/app/models/board_group_recent_visit.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Tracks which boards in a specific group a user has visited +class BoardGroupRecentVisit < ActiveRecord::Base + belongs_to :user + belongs_to :group + belongs_to :board + + validates :user, presence: true + validates :group, presence: true + validates :board, presence: true + + scope :by_user_group, -> (user, group) { where(user: user, group: group).order(:updated_at) } + + def self.visited!(user, board) + visit = find_or_create_by(user: user, group: board.group, board: board) + visit.touch if visit.updated_at < Time.now + rescue ActiveRecord::RecordNotUnique + retry + end + + def self.latest(user, group) + by_user_group(user, group).last + end +end diff --git a/app/models/board_project_recent_visit.rb b/app/models/board_project_recent_visit.rb new file mode 100644 index 00000000000..7cffff906d8 --- /dev/null +++ b/app/models/board_project_recent_visit.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Tracks which boards in a specific project a user has visited +class BoardProjectRecentVisit < ActiveRecord::Base + belongs_to :user + belongs_to :project + belongs_to :board + + validates :user, presence: true + validates :project, presence: true + validates :board, presence: true + + scope :by_user_project, -> (user, project) { where(user: user, project: project).order(:updated_at) } + + def self.visited!(user, board) + visit = find_or_create_by(user: user, project: board.project, board: board) + visit.touch if visit.updated_at < Time.now + rescue ActiveRecord::RecordNotUnique + retry + end + + def self.latest(user, project) + by_user_project(user, project).last + end +end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index cdfe8175a42..d73c02ba5d7 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -593,11 +593,11 @@ module Ci def secret_group_variables return [] unless project.group - project.group.secret_variables_for(ref, project) + project.group.ci_variables_for(ref, project) end def secret_project_variables(environment: persisted_environment) - project.secret_variables_for(ref: ref, environment: environment) + project.ci_variables_for(ref: ref, environment: environment) end def steps diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 9909ff355e5..1939df9f86e 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -21,6 +21,12 @@ module Clusters has_many :cluster_projects, class_name: 'Clusters::Project' has_many :projects, through: :cluster_projects, class_name: '::Project' + has_many :cluster_groups, class_name: 'Clusters::Group' + has_many :groups, through: :cluster_groups, class_name: '::Group' + + has_one :cluster_group, -> { order(id: :desc) }, class_name: 'Clusters::Group' + has_one :group, through: :cluster_group, class_name: '::Group' + # we force autosave to happen when we save `Cluster` model has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true @@ -40,8 +46,12 @@ module Clusters accepts_nested_attributes_for :platform_kubernetes, update_only: true validates :name, cluster_name: true + validates :cluster_type, presence: true validate :restrict_modification, on: :update + validate :no_groups, unless: :group_type? + validate :no_projects, unless: :project_type? + delegate :status, to: :provider, allow_nil: true delegate :status_reason, to: :provider, allow_nil: true delegate :on_creation?, to: :provider, allow_nil: true @@ -52,6 +62,12 @@ module Clusters delegate :available?, to: :application_ingress, prefix: true, allow_nil: true delegate :available?, to: :application_prometheus, prefix: true, allow_nil: true + enum cluster_type: { + instance_type: 1, + group_type: 2, + project_type: 3 + } + enum platform_type: { kubernetes: 1 } @@ -125,5 +141,17 @@ module Clusters true end + + def no_groups + if groups.any? + errors.add(:cluster, 'cannot have groups assigned') + end + end + + def no_projects + if projects.any? + errors.add(:cluster, 'cannot have projects assigned') + end + end end end diff --git a/app/models/clusters/group.rb b/app/models/clusters/group.rb new file mode 100644 index 00000000000..2b08a9e47f0 --- /dev/null +++ b/app/models/clusters/group.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Clusters + class Group < ActiveRecord::Base + self.table_name = 'cluster_groups' + + belongs_to :cluster, class_name: 'Clusters::Cluster' + belongs_to :group, class_name: '::Group' + end +end diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 263b7d9d01e..d961130d251 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -108,7 +108,7 @@ module Clusters end def kubeclient - @kubeclient ||= build_kube_client!(api_groups: ['api', 'apis/rbac.authorization.k8s.io']) + @kubeclient ||= build_kube_client! end private @@ -137,7 +137,7 @@ module Clusters Gitlab::NamespaceSanitizer.sanitize(slug) end - def build_kube_client!(api_groups: ['api'], api_version: 'v1') + def build_kube_client! raise "Incomplete settings" unless api_url && actual_namespace unless (username && password) || token @@ -146,8 +146,6 @@ module Clusters Gitlab::Kubernetes::KubeClient.new( api_url, - api_groups, - api_version, auth_options: kubeclient_auth_options, ssl_options: kubeclient_ssl_options, http_proxy_uri: ENV['http_proxy'] diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 06507345fe8..95c88e11a6e 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -50,7 +50,8 @@ class CommitStatus < ActiveRecord::Base runner_system_failure: 4, missing_dependency_failure: 5, runner_unsupported: 6, - stale_schedule: 7 + stale_schedule: 7, + job_execution_timeout: 8 } ## @@ -109,7 +110,7 @@ class CommitStatus < ActiveRecord::Base before_transition any => :failed do |commit_status, transition| failure_reason = transition.args.first - commit_status.failure_reason = failure_reason + commit_status.failure_reason = CommitStatus.failure_reasons[failure_reason] end after_transition do |commit_status, transition| @@ -166,12 +167,12 @@ class CommitStatus < ActiveRecord::Base false end - # To be overriden when inherrited from + # To be overridden when inherrited from def retryable? false end - # To be overriden when inherrited from + # To be overridden when inherrited from def cancelable? false end diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb index 6f29c92d176..60b7ec2815c 100644 --- a/app/models/concerns/awardable.rb +++ b/app/models/concerns/awardable.rb @@ -13,13 +13,13 @@ module Awardable end class_methods do - def awarded(user, name) + def awarded(user, name = nil) sql = <<~EOL EXISTS ( SELECT TRUE FROM award_emoji WHERE user_id = :user_id AND - name = :name AND + #{"name = :name AND" if name.present?} awardable_type = :awardable_type AND awardable_id = #{self.arel_table.name}.id ) @@ -28,6 +28,20 @@ module Awardable where(sql, user_id: user.id, name: name, awardable_type: self.name) end + def not_awarded(user) + sql = <<~EOL + NOT EXISTS ( + SELECT TRUE + FROM award_emoji + WHERE user_id = :user_id AND + awardable_type = :awardable_type AND + awardable_id = #{self.arel_table.name}.id + ) + EOL + + where(sql, user_id: user.id, awardable_type: self.name) + end + def order_upvotes_desc order_votes_desc(AwardEmoji::UPVOTE_NAME) end diff --git a/app/models/concerns/blob_language_from_git_attributes.rb b/app/models/concerns/blob_language_from_git_attributes.rb new file mode 100644 index 00000000000..70213d22147 --- /dev/null +++ b/app/models/concerns/blob_language_from_git_attributes.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# Applicable for blob classes with project attribute +module BlobLanguageFromGitAttributes + extend ActiveSupport::Concern + + def language_from_gitattributes + return nil unless project + + repository = project.repository + repository.gitattribute(path, 'gitlab-language') + end +end diff --git a/app/models/concerns/cacheable_attributes.rb b/app/models/concerns/cacheable_attributes.rb index f8034be8376..75592bb63e2 100644 --- a/app/models/concerns/cacheable_attributes.rb +++ b/app/models/concerns/cacheable_attributes.rb @@ -12,12 +12,12 @@ module CacheableAttributes "#{name}:#{Gitlab::VERSION}:#{Rails.version}".freeze end - # Can be overriden + # Can be overridden def current_without_cache last end - # Can be overriden + # Can be overridden def defaults {} end diff --git a/app/models/concerns/deployment_platform.rb b/app/models/concerns/deployment_platform.rb index 91052013592..e57a3383544 100644 --- a/app/models/concerns/deployment_platform.rb +++ b/app/models/concerns/deployment_platform.rb @@ -42,6 +42,7 @@ module DeploymentPlatform { name: 'kubernetes-template', projects: [self], + cluster_type: :project_type, provider_type: :user, platform_type: :kubernetes, platform_kubernetes_attributes: platform_kubernetes_attributes_from_service_template diff --git a/app/models/concerns/fast_destroy_all.rb b/app/models/concerns/fast_destroy_all.rb index c342d01243e..2bfa7da6c1c 100644 --- a/app/models/concerns/fast_destroy_all.rb +++ b/app/models/concerns/fast_destroy_all.rb @@ -7,7 +7,7 @@ # `delete_all` is efficient as it deletes all rows with a single `DELETE` query. # # It's better to use `delete_all` as our best practice, however, -# if external data (e.g. ObjectStorage, FileStorage or Redis) are assosiated with database records, +# if external data (e.g. ObjectStorage, FileStorage or Redis) are associated with database records, # it is difficult to accomplish it. # # This module defines a format to use `delete_all` and delete associated external data. diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 2aa52bbaeea..69c5affe142 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -9,6 +9,7 @@ module Issuable extend ActiveSupport::Concern include Gitlab::SQL::Pattern + include Redactable include CacheMarkdownField include Participable include Mentionable @@ -32,6 +33,8 @@ module Issuable cache_markdown_field :title, pipeline: :single_line cache_markdown_field :description, issuable_state_filter_enabled: true + redact_field :description + belongs_to :author, class_name: "User" belongs_to :updated_by, class_name: "User" belongs_to :last_edited_by, class_name: 'User' @@ -360,7 +363,7 @@ module Issuable end ## - # Overriden in MergeRequest + # Overridden in MergeRequest # def wipless_title_changed(old_title) old_title != title diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb index 098eed137ba..eb315058c3a 100644 --- a/app/models/concerns/noteable.rb +++ b/app/models/concerns/noteable.rb @@ -23,7 +23,7 @@ module Noteable end def supports_discussions? - DiscussionNote::NOTEABLE_TYPES.include?(base_class_name) + DiscussionNote.noteable_types.include?(base_class_name) end def discussions_rendered_on_frontend? diff --git a/app/models/concerns/redactable.rb b/app/models/concerns/redactable.rb new file mode 100644 index 00000000000..5ad96d6cc46 --- /dev/null +++ b/app/models/concerns/redactable.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +# This module searches and redacts sensitive information in +# redactable fields. Currently only unsubscribe link is redacted. +# Add following lines into your model: +# +# include Redactable +# redact_field :foo +# +module Redactable + extend ActiveSupport::Concern + + UNSUBSCRIBE_PATTERN = %r{/sent_notifications/\h{32}/unsubscribe} + + class_methods do + def redact_field(field) + before_validation do + redact_field!(field) if attribute_changed?(field) + end + end + end + + private + + def redact_field!(field) + text = public_send(field) # rubocop:disable GitlabSecurity/PublicSend + return unless text.present? + + redacted = text.gsub(UNSUBSCRIBE_PATTERN, '/sent_notifications/REDACTED/unsubscribe') + + public_send("#{field}=", redacted) # rubocop:disable GitlabSecurity/PublicSend + end +end diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb index 7723c07279d..af699eeebce 100644 --- a/app/models/concerns/storage/legacy_namespace.rb +++ b/app/models/concerns/storage/legacy_namespace.rb @@ -94,7 +94,7 @@ module Storage if gitlab_shell.mv_namespace(repository_storage, full_path, new_path) Gitlab::AppLogger.info %Q(Namespace directory "#{full_path}" moved to "#{new_path}") - # Remove namespace directroy async with delay so + # Remove namespace directory async with delay so # GitLab has time to remove all projects first run_after_commit do GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage, new_path) diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb index 522b65e4205..66db4bd92de 100644 --- a/app/models/concerns/token_authenticatable.rb +++ b/app/models/concerns/token_authenticatable.rb @@ -5,57 +5,50 @@ module TokenAuthenticatable private - def write_new_token(token_field) - new_token = generate_available_token(token_field) - write_attribute(token_field, new_token) - end - - def generate_available_token(token_field) - loop do - token = generate_token(token_field) - break token unless self.class.unscoped.find_by(token_field => token) - end - end - - def generate_token(token_field) - Devise.friendly_token - end - class_methods do - def authentication_token_fields - @token_fields || [] - end - private # rubocop:disable Lint/UselessAccessModifier - def add_authentication_token_field(token_field) + def add_authentication_token_field(token_field, options = {}) @token_fields = [] unless @token_fields + + if @token_fields.include?(token_field) + raise ArgumentError.new("#{token_field} already configured via add_authentication_token_field") + end + @token_fields << token_field + attr_accessor :cleartext_tokens + + strategy = if options[:digest] + TokenAuthenticatableStrategies::Digest.new(self, token_field, options) + else + TokenAuthenticatableStrategies::Insecure.new(self, token_field, options) + end + define_singleton_method("find_by_#{token_field}") do |token| - find_by(token_field => token) if token + strategy.find_token_authenticatable(token) end - define_method("ensure_#{token_field}") do - current_token = read_attribute(token_field) - current_token.blank? ? write_new_token(token_field) : current_token + define_method(token_field) do + strategy.get_token(self) end define_method("set_#{token_field}") do |token| - write_attribute(token_field, token) if token + strategy.set_token(self, token) + end + + define_method("ensure_#{token_field}") do + strategy.ensure_token(self) end # Returns a token, but only saves when the database is in read & write mode define_method("ensure_#{token_field}!") do - send("reset_#{token_field}!") if read_attribute(token_field).blank? # rubocop:disable GitlabSecurity/PublicSend - - read_attribute(token_field) + strategy.ensure_token!(self) end # Resets the token, but only saves when the database is in read & write mode define_method("reset_#{token_field}!") do - write_new_token(token_field) - save! if Gitlab::Database.read_write? + strategy.reset_token!(self) end end end diff --git a/app/models/concerns/token_authenticatable_strategies/base.rb b/app/models/concerns/token_authenticatable_strategies/base.rb new file mode 100644 index 00000000000..f0f7107d627 --- /dev/null +++ b/app/models/concerns/token_authenticatable_strategies/base.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +module TokenAuthenticatableStrategies + class Base + def initialize(klass, token_field, options) + @klass = klass + @token_field = token_field + @options = options + end + + def find_token_authenticatable(instance, unscoped = false) + raise NotImplementedError + end + + def get_token(instance) + raise NotImplementedError + end + + def set_token(instance) + raise NotImplementedError + end + + def ensure_token(instance) + write_new_token(instance) unless token_set?(instance) + end + + # Returns a token, but only saves when the database is in read & write mode + def ensure_token!(instance) + reset_token!(instance) unless token_set?(instance) + get_token(instance) + end + + # Resets the token, but only saves when the database is in read & write mode + def reset_token!(instance) + write_new_token(instance) + instance.save! if Gitlab::Database.read_write? + end + + protected + + def write_new_token(instance) + new_token = generate_available_token + set_token(instance, new_token) + end + + def generate_available_token + loop do + token = generate_token + break token unless find_token_authenticatable(token, true) + end + end + + def generate_token + @options[:token_generator] ? @options[:token_generator].call : Devise.friendly_token + end + + def relation(unscoped) + unscoped ? @klass.unscoped : @klass + end + + def token_set?(instance) + raise NotImplementedError + end + + def token_field_name + @token_field + end + end +end diff --git a/app/models/concerns/token_authenticatable_strategies/digest.rb b/app/models/concerns/token_authenticatable_strategies/digest.rb new file mode 100644 index 00000000000..9926662ed66 --- /dev/null +++ b/app/models/concerns/token_authenticatable_strategies/digest.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module TokenAuthenticatableStrategies + class Digest < Base + def find_token_authenticatable(token, unscoped = false) + return unless token + + token_authenticatable = relation(unscoped).find_by(token_field_name => Gitlab::CryptoHelper.sha256(token)) + + if @options[:fallback] + token_authenticatable ||= fallback_strategy.find_token_authenticatable(token) + end + + token_authenticatable + end + + def get_token(instance) + token = instance.cleartext_tokens&.[](@token_field) + token ||= fallback_strategy.get_token(instance) if @options[:fallback] + + token + end + + def set_token(instance, token) + return unless token + + instance.cleartext_tokens ||= {} + instance.cleartext_tokens[@token_field] = token + instance[token_field_name] = Gitlab::CryptoHelper.sha256(token) + instance[@token_field] = nil if @options[:fallback] + end + + protected + + def fallback_strategy + @fallback_strategy ||= TokenAuthenticatableStrategies::Insecure.new(@klass, @token_field, @options) + end + + def token_set?(instance) + token_digest = instance.read_attribute(token_field_name) + token_digest ||= instance.read_attribute(@token_field) if @options[:fallback] + + token_digest.present? + end + + def token_field_name + "#{@token_field}_digest" + end + end +end diff --git a/app/models/concerns/token_authenticatable_strategies/insecure.rb b/app/models/concerns/token_authenticatable_strategies/insecure.rb new file mode 100644 index 00000000000..5f915259521 --- /dev/null +++ b/app/models/concerns/token_authenticatable_strategies/insecure.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module TokenAuthenticatableStrategies + class Insecure < Base + def find_token_authenticatable(token, unscoped = false) + relation(unscoped).find_by(@token_field => token) if token + end + + def get_token(instance) + instance.read_attribute(@token_field) + end + + def set_token(instance, token) + instance[@token_field] = token if token + end + + protected + + def token_set?(instance) + instance.read_attribute(@token_field).present? + end + end +end diff --git a/app/models/concerns/with_uploads.rb b/app/models/concerns/with_uploads.rb index e231af5368d..2bdef2a40e4 100644 --- a/app/models/concerns/with_uploads.rb +++ b/app/models/concerns/with_uploads.rb @@ -2,7 +2,7 @@ # Mounted uploaders are destroyed by carrierwave's after_commit # hook. This hook fetches upload location (local vs remote) from -# Upload model. So it's neccessary to make sure that during that +# Upload model. So it's necessary to make sure that during that # after_commit hook model's associated uploads are not deleted yet. # IOW we can not use dependent: :destroy : # has_many :uploads, as: :model, dependent: :destroy diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 62dc0f2cbeb..ee5b96e7454 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -127,6 +127,10 @@ class Deployment < ActiveRecord::Base metrics&.merge(deployment_time: created_at.to_i) || {} end + def status + 'success' + end + private def prometheus_adapter diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index 95694377fe3..5f59e4832db 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -8,12 +8,14 @@ class DiffNote < Note include DiffPositionableNote include Gitlab::Utils::StrongMemoize - NOTEABLE_TYPES = %w(MergeRequest Commit).freeze + def self.noteable_types + %w(MergeRequest Commit) + end validates :original_position, presence: true validates :position, presence: true validates :line_code, presence: true, line_code: true, if: :on_text? - validates :noteable_type, inclusion: { in: NOTEABLE_TYPES } + validates :noteable_type, inclusion: { in: noteable_types } validate :positions_complete validate :verify_supported validate :diff_refs_match_commit, if: :for_commit? diff --git a/app/models/discussion_note.rb b/app/models/discussion_note.rb index 89d86aaed66..142cbdcdfa6 100644 --- a/app/models/discussion_note.rb +++ b/app/models/discussion_note.rb @@ -5,9 +5,11 @@ # A note of this type can be resolvable. class DiscussionNote < Note # Names of all implementers of `Noteable` that support discussions. - NOTEABLE_TYPES = %w(MergeRequest Issue Commit Snippet).freeze + def self.noteable_types + %w(MergeRequest Issue Commit Snippet) + end - validates :noteable_type, inclusion: { in: NOTEABLE_TYPES } + validates :noteable_type, inclusion: { in: noteable_types } def discussion_class(*) Discussion diff --git a/app/models/environment.rb b/app/models/environment.rb index 0816c395185..1c31c01eb9f 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -149,7 +149,7 @@ class Environment < ActiveRecord::Base end def has_metrics? - prometheus_adapter&.can_query? && available? && last_deployment.present? + prometheus_adapter&.can_query? && available? end def metrics diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb index 5ff3acc0e58..a84871f7253 100644 --- a/app/models/environment_status.rb +++ b/app/models/environment_status.rb @@ -3,21 +3,33 @@ class EnvironmentStatus include Gitlab::Utils::StrongMemoize - attr_reader :environment, :merge_request + attr_reader :environment, :merge_request, :sha delegate :id, to: :environment delegate :name, to: :environment delegate :project, to: :environment delegate :deployed_at, to: :deployment, allow_nil: true + delegate :status, to: :deployment - def initialize(environment, merge_request) + def self.for_merge_request(mr, user) + build_environments_status(mr, user, mr.head_pipeline) + end + + def self.after_merge_request(mr, user) + return [] unless mr.merged? + + build_environments_status(mr, user, mr.merge_pipeline) + end + + def initialize(environment, merge_request, sha) @environment = environment @merge_request = merge_request + @sha = sha end def deployment strong_memoize(:deployment) do - environment.first_deployment_for(merge_request.diff_head_sha) + environment.first_deployment_for(sha) end end @@ -26,10 +38,9 @@ class EnvironmentStatus end def changes - sha = merge_request.diff_head_sha return [] if project.route_map_for(sha).nil? - changed_files.map { |file| build_change(file, sha) }.compact + changed_files.map { |file| build_change(file) }.compact end def changed_files @@ -41,7 +52,7 @@ class EnvironmentStatus PAGE_EXTENSIONS = /\A\.(s?html?|php|asp|cgi|pl)\z/i.freeze - def build_change(file, sha) + def build_change(file) public_path = project.public_path_for_source_path(file.new_path, sha) return if public_path.nil? @@ -53,4 +64,22 @@ class EnvironmentStatus external_url: environment.external_url_for(file.new_path, sha) } end + + def self.build_environments_status(mr, user, pipeline) + return [] unless pipeline.present? + + find_environments(user, pipeline).map do |environment| + EnvironmentStatus.new(environment, mr, pipeline.sha) + end + end + private_class_method :build_environments_status + + def self.find_environments(user, pipeline) + env_ids = Deployment.where(deployable: pipeline.builds).select(:environment_id) + + Environment.available.where(id: env_ids).select do |environment| + Ability.allowed?(user, :read_environment, environment) + end + end + private_class_method :find_environments end diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index a6cebabe089..085ffd16c6a 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -34,50 +34,6 @@ class GlobalMilestone new(title, child_milestones) end - def self.states_count(projects, group = nil) - legacy_group_milestones_count = legacy_group_milestone_states_count(projects) - group_milestones_count = group_milestones_states_count(group) - - legacy_group_milestones_count.merge(group_milestones_count) do |k, legacy_group_milestones_count, group_milestones_count| - legacy_group_milestones_count + group_milestones_count - end - end - - def self.group_milestones_states_count(group) - return STATE_COUNT_HASH unless group - - params = { group_ids: [group.id], state: 'all' } - - relation = MilestonesFinder.new(params).execute # rubocop: disable CodeReuse/Finder - grouped_by_state = relation.reorder(nil).group(:state).count - - { - opened: grouped_by_state['active'] || 0, - closed: grouped_by_state['closed'] || 0, - all: grouped_by_state.values.sum - } - end - - # Counts the legacy group milestones which must be grouped by title - def self.legacy_group_milestone_states_count(projects) - return STATE_COUNT_HASH unless projects - - params = { project_ids: projects.map(&:id), state: 'all' } - - relation = MilestonesFinder.new(params).execute # rubocop: disable CodeReuse/Finder - project_milestones_by_state_and_title = relation.reorder(nil).group(:state, :title).count - - opened = count_by_state(project_milestones_by_state_and_title, 'active') - closed = count_by_state(project_milestones_by_state_and_title, 'closed') - all = project_milestones_by_state_and_title.map { |(_, title), _| title }.uniq.count - - { - opened: opened, - closed: closed, - all: all - } - end - def self.count_by_state(milestones_by_state_and_title, state) milestones_by_state_and_title.count do |(milestone_state, _), _| milestone_state == state diff --git a/app/models/group.rb b/app/models/group.rb index 612c546ca57..adb9169cfcd 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -41,6 +41,9 @@ class Group < Namespace has_many :boards has_many :badges, class_name: 'GroupBadge' + has_many :cluster_groups, class_name: 'Clusters::Group' + has_many :clusters, through: :cluster_groups, class_name: 'Clusters::Cluster' + has_many :todos accepts_nested_attributes_for :variables, allow_destroy: true @@ -366,7 +369,7 @@ class Group < Namespace } end - def secret_variables_for(ref, project) + def ci_variables_for(ref, project) list_of_ids = [self] + ancestors variables = Ci::GroupVariable.where(group: list_of_ids) variables = variables.unprotected unless project.protected_for?(ref) diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb index 97bf5d611c2..69c563545bb 100644 --- a/app/models/lfs_object.rb +++ b/app/models/lfs_object.rb @@ -7,7 +7,7 @@ class LfsObject < ActiveRecord::Base has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :projects, through: :lfs_objects_projects - scope :with_files_stored_locally, -> { where(file_store: [nil, LfsObjectUploader::Store::LOCAL]) } + scope :with_files_stored_locally, -> { where(file_store: LfsObjectUploader::Store::LOCAL) } validates :oid, presence: true, uniqueness: true @@ -26,7 +26,7 @@ class LfsObject < ActiveRecord::Base end def local_store? - [nil, LfsObjectUploader::Store::LOCAL].include?(self.file_store) + file_store == LfsObjectUploader::Store::LOCAL end # rubocop: disable DestroyAll diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 6559f94a696..7eef08aa6a3 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -204,6 +204,12 @@ class MergeRequest < ActiveRecord::Base head_pipeline&.sha == diff_head_sha ? head_pipeline : nil end + def merge_pipeline + return unless merged? + + target_project.pipeline_for(target_branch, merge_commit_sha) + end + # Pattern used to extract `!123` merge request references from text # # This pattern supports cross-project references. diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 892a680f221..3cc8e2c44bb 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -145,7 +145,7 @@ class Milestone < ActiveRecord::Base end def participants - User.joins(assigned_issues: :milestone).where("milestones.id = ?", id).uniq + User.joins(assigned_issues: :milestone).where("milestones.id = ?", id).distinct end def self.sort_by_attribute(method) @@ -170,6 +170,22 @@ class Milestone < ActiveRecord::Base sorted.with_order_id_desc end + def self.states_count(projects, groups = nil) + return STATE_COUNT_HASH unless projects || groups + + counts = Milestone + .for_projects_and_groups(projects&.map(&:id), groups&.map(&:id)) + .reorder(nil) + .group(:state) + .count + + { + opened: counts['active'] || 0, + closed: counts['closed'] || 0, + all: counts.values.sum + } + end + ## # Returns the String necessary to reference this Milestone in Markdown. Group # milestones only support name references, and do not support cross-project diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 599bedde27d..74d48d0a9af 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -83,7 +83,7 @@ class Namespace < ActiveRecord::Base find_by('lower(path) = :value', value: path.downcase) end - # Case insensetive search for namespace by path or name + # Case insensitive search for namespace by path or name def find_by_path_or_name(path) find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase) end diff --git a/app/models/note.rb b/app/models/note.rb index e1bd943e8e4..990689a95f5 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -10,6 +10,7 @@ class Note < ActiveRecord::Base include Awardable include Importable include FasterCacheKeys + include Redactable include CacheMarkdownField include AfterCommitQueue include ResolvableNote @@ -33,6 +34,8 @@ class Note < ActiveRecord::Base cache_markdown_field :note, pipeline: :note, issuable_state_filter_enabled: true + redact_field :note + # Aliases to make application_helper#edited_time_ago_with_tooltip helper work properly with notes. # See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10392/diffs#note_28719102 alias_attribute :last_edited_at, :updated_at diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb index 207146479c0..73a58f2420e 100644 --- a/app/models/personal_access_token.rb +++ b/app/models/personal_access_token.rb @@ -3,7 +3,7 @@ class PersonalAccessToken < ActiveRecord::Base include Expirable include TokenAuthenticatable - add_authentication_token_field :token + add_authentication_token_field :token, digest: true, fallback: true REDIS_EXPIRY_TIME = 3.minutes @@ -33,16 +33,22 @@ class PersonalAccessToken < ActiveRecord::Base def self.redis_getdel(user_id) Gitlab::Redis::SharedState.with do |redis| - token = redis.get(redis_shared_state_key(user_id)) + encrypted_token = redis.get(redis_shared_state_key(user_id)) redis.del(redis_shared_state_key(user_id)) - token + begin + Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_token) + rescue => ex + logger.warn "Failed to decrypt PersonalAccessToken value stored in Redis for User ##{user_id}: #{ex.class}" + encrypted_token + end end end def self.redis_store!(user_id, token) + encrypted_token = Gitlab::CryptoHelper.aes256_gcm_encrypt(token) + Gitlab::Redis::SharedState.with do |redis| - redis.set(redis_shared_state_key(user_id), token, ex: REDIS_EXPIRY_TIME) - token + redis.set(redis_shared_state_key(user_id), encrypted_token, ex: REDIS_EXPIRY_TIME) end end diff --git a/app/models/project.rb b/app/models/project.rb index 382fb4f463a..e2e309e8496 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -181,7 +181,7 @@ class Project < ActiveRecord::Base has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent # Merge Requests for target project should be removed with it - has_many :merge_requests, foreign_key: 'target_project_id' + has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest' has_many :issues has_many :labels, class_name: 'ProjectLabel' @@ -665,7 +665,7 @@ class Project < ActiveRecord::Base remove_import_data end - # This method is overriden in EE::Project model + # This method is overridden in EE::Project model def remove_import_data import_data&.destroy end @@ -1811,7 +1811,7 @@ class Project < ActiveRecord::Base .first end - def secret_variables_for(ref:, environment: nil) + def ci_variables_for(ref:, environment: nil) # EE would use the environment if protected_for?(ref) variables diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index e1d342be188..7cff0e30e8d 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -9,7 +9,7 @@ class IssueTrackerService < Service # Override this method on services that uses different patterns # This pattern does not support cross-project references # The other code assumes that this pattern is a superset of all - # overriden patterns. See ReferenceRegexes::EXTERNAL_PATTERN + # overridden patterns. See ReferenceRegexes::EXTERNAL_PATTERN def self.reference_pattern(only_long: false) if only_long /(\b[A-Z][A-Z0-9_]+-)(?<issue>\d+)/ diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index f119555f16b..798944d0c06 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -144,7 +144,7 @@ class KubernetesService < DeploymentService end def kubeclient - @kubeclient ||= build_kube_client!(api_groups: ['api', 'apis/rbac.authorization.k8s.io']) + @kubeclient ||= build_kube_client! end def deprecated? @@ -182,13 +182,11 @@ class KubernetesService < DeploymentService slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '') end - def build_kube_client!(api_groups: ['api'], api_version: 'v1') + def build_kube_client! raise "Incomplete settings" unless api_url && actual_namespace && token Gitlab::Kubernetes::KubeClient.new( api_url, - api_groups, - api_version, auth_options: kubeclient_auth_options, ssl_options: kubeclient_ssl_options, http_proxy_uri: ENV['http_proxy'] diff --git a/app/models/snippet.rb b/app/models/snippet.rb index e9533ee7c77..1c5846b4023 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -2,6 +2,7 @@ class Snippet < ActiveRecord::Base include Gitlab::VisibilityLevel + include Redactable include CacheMarkdownField include Noteable include Participable @@ -18,6 +19,8 @@ class Snippet < ActiveRecord::Base cache_markdown_field :description cache_markdown_field :content + redact_field :description + # Aliases to make application_helper#edited_time_ago_with_tooltip helper work properly with snippets. # See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10392/diffs#note_28719102 alias_attribute :last_edited_at, :updated_at diff --git a/app/models/ssh_host_key.rb b/app/models/ssh_host_key.rb new file mode 100644 index 00000000000..b6844dbe870 --- /dev/null +++ b/app/models/ssh_host_key.rb @@ -0,0 +1,130 @@ +# frozen_string_literal: true + +# Detected SSH host keys are transiently stored in Redis +class SshHostKey + class Fingerprint < Gitlab::SSHPublicKey + attr_reader :index + + def initialize(key, index: nil) + super(key) + + @index = index + end + + def as_json(*) + { bits: bits, fingerprint: fingerprint, type: type, index: index } + end + end + + include ReactiveCaching + + self.reactive_cache_key = ->(key) { [key.class.to_s, key.id] } + + # Do not refresh the data in the background - it is not expected to change. + # This is achieved by making the lifetime shorter than the refresh interval. + self.reactive_cache_refresh_interval = 15.minutes + self.reactive_cache_lifetime = 10.minutes + + def self.find_by(opts = {}) + return nil unless opts.key?(:id) + + project_id, url = opts[:id].split(':', 2) + project = Project.find_by(id: project_id) + + project.presence && new(project: project, url: url) + end + + def self.fingerprint_host_keys(data) + return [] unless data.is_a?(String) + + data + .each_line + .each_with_index + .map { |line, index| Fingerprint.new(line, index: index) } + .select(&:valid?) + end + + attr_reader :project, :url, :compare_host_keys + + def initialize(project:, url:, compare_host_keys: nil) + @project = project + @url = normalize_url(url) + @compare_host_keys = compare_host_keys + end + + def id + [project.id, url].join(':') + end + + def as_json(*) + { + host_keys_changed: host_keys_changed?, + fingerprints: fingerprints, + known_hosts: known_hosts + } + end + + def known_hosts + with_reactive_cache { |data| data[:known_hosts] } + end + + def fingerprints + @fingerprints ||= self.class.fingerprint_host_keys(known_hosts) + end + + # Returns true if the known_hosts data differs from the version passed in at + # initialization as `compare_host_keys`. Comments, ordering, etc, is ignored + def host_keys_changed? + cleanup(known_hosts) != cleanup(compare_host_keys) + end + + def error + with_reactive_cache { |data| data[:error] } + end + + def calculate_reactive_cache + known_hosts, errors, status = + Open3.popen3({}, *%W[ssh-keyscan -T 5 -p #{url.port} -f-]) do |stdin, stdout, stderr, wait_thr| + stdin.puts(url.host) + stdin.close + + [ + cleanup(stdout.read), + cleanup(stderr.read), + wait_thr.value + ] + end + + # ssh-keyscan returns an exit code 0 in several error conditions, such as an + # unknown hostname, so check both STDERR and the exit code + if status.success? && !errors.present? + { known_hosts: known_hosts } + else + Rails.logger.debug("Failed to detect SSH host keys for #{id}: #{errors}") + + { error: 'Failed to detect SSH host keys' } + end + end + + private + + # Remove comments and duplicate entries + def cleanup(data) + data + .to_s + .each_line + .reject { |line| line.start_with?('#') || line.chomp.empty? } + .uniq + .sort + .join + end + + def normalize_url(url) + full_url = ::Addressable::URI.parse(url) + raise ArgumentError.new("Invalid URL") unless full_url&.scheme == 'ssh' + + Addressable::URI.parse("ssh://#{full_url.host}:#{full_url.inferred_port}") + rescue Addressable::URI::InvalidURIError + raise ArgumentError.new("Invalid URL") + end +end diff --git a/app/models/user.rb b/app/models/user.rb index ca7fc3b058f..cc2cd1b7723 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -28,7 +28,7 @@ class User < ActiveRecord::Base ignore_column :email_provider ignore_column :authentication_token - add_authentication_token_field :incoming_email_token + add_authentication_token_field :incoming_email_token, token_generator: -> { SecureRandom.hex.to_i(16).to_s(36) } add_authentication_token_field :feed_token default_value_for :admin, false @@ -463,7 +463,7 @@ class User < ActiveRecord::Base def find_by_personal_access_token(token_string) return unless token_string - PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user # rubocop: disable CodeReuse/Finder + PersonalAccessTokensFinder.new(state: 'active').find_by_token(token_string)&.user # rubocop: disable CodeReuse/Finder end # Returns a user for the given SSH key. @@ -1138,7 +1138,7 @@ class User < ActiveRecord::Base events = Event.select(:project_id) .contributions.where(author_id: self) .where("created_at > ?", Time.now - 1.year) - .uniq + .distinct .reorder(nil) Project.where(id: events) @@ -1464,15 +1464,6 @@ class User < ActiveRecord::Base end end - def generate_token(token_field) - if token_field == :incoming_email_token - # Needs to be all lowercase and alphanumeric because it's gonna be used in an email address. - SecureRandom.hex.to_i(16).to_s(36) - else - super - end - end - def self.unique_internal(scope, username, email_pattern, &block) scope.first || create_unique_internal(scope, username, email_pattern, &block) end diff --git a/app/presenters/blob_presenter.rb b/app/presenters/blob_presenter.rb new file mode 100644 index 00000000000..6323c1b3389 --- /dev/null +++ b/app/presenters/blob_presenter.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class BlobPresenter < Gitlab::View::Presenter::Simple + presents :blob + + def highlight(plain: nil) + blob.load_all_data! if blob.respond_to?(:load_all_data!) + + Gitlab::Highlight.highlight( + blob.path, + blob.data, + language: blob.language_from_gitattributes, + plain: plain + ) + end +end diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb index 29eaad759bb..a866e76df5a 100644 --- a/app/presenters/commit_status_presenter.rb +++ b/app/presenters/commit_status_presenter.rb @@ -9,7 +9,8 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated runner_system_failure: 'There has been a runner system failure, please try again', missing_dependency_failure: 'There has been a missing dependency failure', runner_unsupported: 'Your runner is outdated, please upgrade your runner', - stale_schedule: 'Delayed job could not be executed by some reason, please try again' + stale_schedule: 'Delayed job could not be executed by some reason, please try again', + job_execution_timeout: 'The script exceeded the maximum execution time set for the job' }.freeze private_constant :CALLOUT_FAILURE_MESSAGES diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb index 3f565b826dd..1db6c9eff36 100644 --- a/app/presenters/merge_request_presenter.rb +++ b/app/presenters/merge_request_presenter.rb @@ -108,16 +108,10 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated namespace = source_project_namespace branch = source_branch - if source_branch_exists? - namespace = link_to(namespace, project_path(source_project)) - branch = link_to(branch, project_tree_path(source_project, source_branch)) - end + namespace_link = source_branch_exists? ? link_to(namespace, project_path(source_project)) : ERB::Util.html_escape(namespace) + branch_link = source_branch_exists? ? link_to(branch, project_tree_path(source_project, source_branch)) : ERB::Util.html_escape(branch) - if for_fork? - namespace + ":" + branch - else - branch - end + for_fork? ? "#{namespace_link}:#{branch_link}" : branch_link end def closing_issues_links diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb index 79cd3606aec..d61124fa787 100644 --- a/app/presenters/project_presenter.rb +++ b/app/presenters/project_presenter.rb @@ -176,7 +176,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated AnchorData.new(false, _('New file'), project_new_blob_path(project, default_branch || 'master'), - 'new') + 'success') end end diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb index 066a5b1885c..9ddce0d2c80 100644 --- a/app/serializers/build_details_entity.rb +++ b/app/serializers/build_details_entity.rb @@ -5,6 +5,7 @@ class BuildDetailsEntity < JobEntity expose :tag_list, as: :tags expose :has_trace?, as: :has_trace expose :stage + expose :stuck?, as: :stuck expose :user, using: UserEntity expose :runner, using: RunnerEntity expose :pipeline, using: PipelineEntity diff --git a/app/serializers/environment_status_entity.rb b/app/serializers/environment_status_entity.rb index 3dfa4f204c9..f87cc894d2f 100644 --- a/app/serializers/environment_status_entity.rb +++ b/app/serializers/environment_status_entity.rb @@ -5,6 +5,7 @@ class EnvironmentStatusEntity < Grape::Entity expose :id expose :name + expose :status expose :url do |es| project_environment_path(es.project, es.environment) diff --git a/app/serializers/job_entity.rb b/app/serializers/job_entity.rb index 0b19cb16955..a0a66511b7b 100644 --- a/app/serializers/job_entity.rb +++ b/app/serializers/job_entity.rb @@ -9,7 +9,7 @@ class JobEntity < Grape::Entity expose :started?, as: :started expose :build_path do |build| - build.target_url || path_to(:namespace_project_job, build) + build_path(build) end expose :retry_path, if: -> (*) { retryable? } do |build| @@ -17,7 +17,11 @@ class JobEntity < Grape::Entity end expose :cancel_path, if: -> (*) { cancelable? } do |build| - path_to(:cancel_namespace_project_job, build) + path_to( + :cancel_namespace_project_job, + build, + { continue: { to: build_path(build) } } + ) end expose :play_path, if: -> (*) { playable? } do |build| @@ -60,8 +64,12 @@ class JobEntity < Grape::Entity build.detailed_status(request.current_user) end - def path_to(route, build) - send("#{route}_path", build.project.namespace, build.project, build) # rubocop:disable GitlabSecurity/PublicSend + def path_to(route, build, params = {}) + send("#{route}_path", build.project.namespace, build.project, build, params) # rubocop:disable GitlabSecurity/PublicSend + end + + def build_path(build) + build.target_url || path_to(:namespace_project_job, build) end def failed? diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb index 9ec24f799ef..f33a1654d5e 100644 --- a/app/serializers/merge_request_widget_entity.rb +++ b/app/serializers/merge_request_widget_entity.rb @@ -55,6 +55,7 @@ class MergeRequestWidgetEntity < IssuableEntity expose :merge_commit_message expose :actual_head_pipeline, with: PipelineDetailsEntity, as: :pipeline + expose :merge_pipeline, with: PipelineDetailsEntity, if: ->(mr, _) { mr.merged? && can?(request.current_user, :read_pipeline, mr.target_project)} # Booleans expose :merge_ongoing?, as: :merge_ongoing diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 893b37b831a..f764536e762 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -99,7 +99,7 @@ module Auth ## # Because we do not have two way communication with registry yet, # we create a container repository image resource when push to the - # registry is successfuly authorized. + # registry is successfully authorized. # def ensure_container_repository!(path, actions) return if path.has_repository? diff --git a/app/services/boards/visits/create_service.rb b/app/services/boards/visits/create_service.rb new file mode 100644 index 00000000000..e2adf755511 --- /dev/null +++ b/app/services/boards/visits/create_service.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Boards + module Visits + class CreateService < Boards::BaseService + def execute(board) + return unless current_user && Gitlab::Database.read_write? + + if parent.is_a?(Group) + BoardGroupRecentVisit.visited!(current_user, board) + else + BoardProjectRecentVisit.visited!(current_user, board) + end + end + end + end +end diff --git a/app/services/boards/visits/latest_service.rb b/app/services/boards/visits/latest_service.rb new file mode 100644 index 00000000000..9e4c77a6317 --- /dev/null +++ b/app/services/boards/visits/latest_service.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Boards + module Visits + class LatestService < Boards::BaseService + def execute + return nil unless current_user + + if parent.is_a?(Group) + BoardGroupRecentVisit.latest(current_user, parent) + else + BoardProjectRecentVisit.latest(current_user, parent) + end + end + end + end +end diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb index c6e955800af..cd843b8ffa8 100644 --- a/app/services/clusters/create_service.rb +++ b/app/services/clusters/create_service.rb @@ -9,9 +9,9 @@ module Clusters end def execute(project:, access_token: nil) - raise ArgumentError.new(_('Instance does not support multiple Kubernetes clusters')) unless can_create_cluster?(project) + raise ArgumentError, _('Instance does not support multiple Kubernetes clusters') unless can_create_cluster?(project) - cluster_params = params.merge(user: current_user, projects: [project]) + cluster_params = params.merge(user: current_user, cluster_type: :project_type, projects: [project]) cluster_params[:provider_gcp_attributes].try do |provider| provider[:access_token] = access_token end diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb index 3ae0a4a19d0..6ee63db8eb9 100644 --- a/app/services/clusters/gcp/finalize_creation_service.rb +++ b/app/services/clusters/gcp/finalize_creation_service.rb @@ -60,18 +60,15 @@ module Clusters 'https://' + gke_cluster.endpoint, Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate), gke_cluster.master_auth.username, - gke_cluster.master_auth.password, - api_groups: ['api', 'apis/rbac.authorization.k8s.io'] + gke_cluster.master_auth.password ) end - def build_kube_client!(api_url, ca_pem, username, password, api_groups: ['api'], api_version: 'v1') + def build_kube_client!(api_url, ca_pem, username, password) raise "Incomplete settings" unless api_url && username && password Gitlab::Kubernetes::KubeClient.new( api_url, - api_groups, - api_version, auth_options: { username: username, password: password }, ssl_options: kubeclient_ssl_options(ca_pem), http_proxy_uri: ENV['http_proxy'] diff --git a/app/services/delete_merged_branches_service.rb b/app/services/delete_merged_branches_service.rb index ced87a1c37a..80de897e94b 100644 --- a/app/services/delete_merged_branches_service.rb +++ b/app/services/delete_merged_branches_service.rb @@ -24,8 +24,8 @@ class DeleteMergedBranchesService < BaseService # rubocop: disable CodeReuse/ActiveRecord def merge_request_branch_names # reorder(nil) is necessary for SELECT DISTINCT because default scope adds an ORDER BY - source_names = project.origin_merge_requests.opened.reorder(nil).uniq.pluck(:source_branch) - target_names = project.merge_requests.opened.reorder(nil).uniq.pluck(:target_branch) + source_names = project.origin_merge_requests.opened.reorder(nil).distinct.pluck(:source_branch) + target_names = project.merge_requests.opened.reorder(nil).distinct.pluck(:target_branch) (source_names + target_names).uniq end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/services/keys/destroy_service.rb b/app/services/keys/destroy_service.rb index e2ae4047941..159455f80f3 100644 --- a/app/services/keys/destroy_service.rb +++ b/app/services/keys/destroy_service.rb @@ -6,7 +6,7 @@ module Keys key.destroy if destroy_possible?(key) end - # overriden in EE::Keys::DestroyService + # overridden in EE::Keys::DestroyService def destroy_possible?(key) true end diff --git a/app/services/labels/transfer_service.rb b/app/services/labels/transfer_service.rb index 52360f775dc..9cbc9fef529 100644 --- a/app/services/labels/transfer_service.rb +++ b/app/services/labels/transfer_service.rb @@ -40,7 +40,7 @@ module Labels group_labels_applied_to_merge_requests ]) .reorder(nil) - .uniq + .distinct end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/services/members/base_service.rb b/app/services/members/base_service.rb index 8248f1441d7..d734571f835 100644 --- a/app/services/members/base_service.rb +++ b/app/services/members/base_service.rb @@ -10,7 +10,7 @@ module Members end def after_execute(args) - # overriden in EE::Members modules + # overridden in EE::Members modules end private diff --git a/app/services/merge_requests/get_urls_service.rb b/app/services/merge_requests/get_urls_service.rb index 7c88c9abb41..35a22449e34 100644 --- a/app/services/merge_requests/get_urls_service.rb +++ b/app/services/merge_requests/get_urls_service.rb @@ -50,8 +50,8 @@ module MergeRequests end def url_for_new_merge_request(branch_name) - merge_request_params = { source_branch: branch_name } - url = Gitlab::Routing.url_helpers.project_new_merge_request_url(project, merge_request: merge_request_params) + url = Gitlab::Routing.url_helpers.project_new_merge_request_url(project, branch_name) + { branch_name: branch_name, url: url, new_merge_request: true } end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index fb44f809c41..70a67baa01c 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -49,6 +49,11 @@ module MergeRequests end end + # Overridden in EE. + def hooks_validation_pass?(_merge_request) + true + end + private def error_check! diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index b03d14fa3cc..f01872b205e 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -85,7 +85,7 @@ module MergeRequests .where.not(target_project: @project).to_a filter_merge_requests(merge_requests).each do |merge_request| - if merge_request.source_branch == @push.branch_name || @push.force_push? + if branch_and_project_match?(merge_request) || @push.force_push? merge_request.reload_diff(current_user) else mr_commit_ids = merge_request.commit_shas @@ -104,6 +104,11 @@ module MergeRequests end # rubocop: enable CodeReuse/ActiveRecord + def branch_and_project_match?(merge_request) + merge_request.source_project == @project && + merge_request.source_branch == @push.branch_name + end + def reset_merge_when_pipeline_succeeds merge_requests_for_source_branch.each(&:reset_merge_when_pipeline_succeeds) end diff --git a/app/services/projects/move_project_authorizations_service.rb b/app/services/projects/move_project_authorizations_service.rb index 2060a263751..2985ba89014 100644 --- a/app/services/projects/move_project_authorizations_service.rb +++ b/app/services/projects/move_project_authorizations_service.rb @@ -3,7 +3,7 @@ # NOTE: This service cannot be used directly because it is part of a # a bigger process. Instead, use the service MoveAccessService which moves # project memberships, project group links, authorizations and refreshes -# the authorizations if neccessary +# the authorizations if necessary module Projects class MoveProjectAuthorizationsService < BaseMoveRelationsService def execute(source_project, remove_remaining_elements: true) diff --git a/app/services/projects/move_project_group_links_service.rb b/app/services/projects/move_project_group_links_service.rb index fb395ecb9a1..36afcd0c503 100644 --- a/app/services/projects/move_project_group_links_service.rb +++ b/app/services/projects/move_project_group_links_service.rb @@ -3,7 +3,7 @@ # NOTE: This service cannot be used directly because it is part of a # a bigger process. Instead, use the service MoveAccessService which moves # project memberships, project group links, authorizations and refreshes -# the authorizations if neccessary +# the authorizations if necessary module Projects class MoveProjectGroupLinksService < BaseMoveRelationsService def execute(source_project, remove_remaining_elements: true) diff --git a/app/services/projects/move_project_members_service.rb b/app/services/projects/move_project_members_service.rb index f28f44adc03..faf389241d2 100644 --- a/app/services/projects/move_project_members_service.rb +++ b/app/services/projects/move_project_members_service.rb @@ -3,7 +3,7 @@ # NOTE: This service cannot be used directly because it is part of a # a bigger process. Instead, use the service MoveAccessService which moves # project memberships, project group links, authorizations and refreshes -# the authorizations if neccessary +# the authorizations if necessary module Projects class MoveProjectMembersService < BaseMoveRelationsService def execute(source_project, remove_remaining_elements: true) diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index 751aae2696d..eb431c36807 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -433,14 +433,14 @@ module QuickActions end end - desc 'Add or substract spent time' + desc 'Add or subtract spent time' explanation do |time_spent, time_spent_date| if time_spent if time_spent > 0 verb = 'Adds' value = time_spent else - verb = 'Substracts' + verb = 'Subtracts' value = -time_spent end diff --git a/app/services/resource_events/merge_into_notes_service.rb b/app/services/resource_events/merge_into_notes_service.rb index 596c0105ea0..7504773a002 100644 --- a/app/services/resource_events/merge_into_notes_service.rb +++ b/app/services/resource_events/merge_into_notes_service.rb @@ -34,7 +34,7 @@ module ResourceEvents def label_events_by_discussion_id return [] unless resource.respond_to?(:resource_label_events) - events = resource.resource_label_events.includes(:label, :user) + events = resource.resource_label_events.includes(:label, user: :status) events = since_fetch_at(events) events.group_by { |event| event.discussion_id } diff --git a/app/services/search/group_service.rb b/app/services/search/group_service.rb index 00372887985..34803d005e3 100644 --- a/app/services/search/group_service.rb +++ b/app/services/search/group_service.rb @@ -11,13 +11,11 @@ module Search @group = group end - # rubocop: disable CodeReuse/ActiveRecord def projects return Project.none unless group return @projects if defined? @projects @projects = super.inside_path(group.full_path) end - # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml index e402801a776..f34305e94fa 100644 --- a/app/views/ci/variables/_index.html.haml +++ b/app/views/ci/variables/_index.html.haml @@ -9,8 +9,8 @@ = render 'ci/variables/variable_row', form_field: 'variables', variable: variable = render 'ci/variables/variable_row', form_field: 'variables' .prepend-top-20 - %button.btn.btn-success.js-secret-variables-save-button{ type: 'button' } - %span.hide.js-secret-variables-save-loading-icon + %button.btn.btn-success.js-ci-variables-save-button{ type: 'button' } + %span.hide.js-ci-variables-save-loading-icon = icon('spinner spin') = _('Save variables') %button.btn.btn-info.btn-inverted.prepend-left-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: "#{@variables.size == 0}" } } diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 684b51b8552..0904e44a658 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -1,12 +1,12 @@ - @hide_breadcrumbs = true - @hide_top_links = true -- page_title 'New Group' -- header_title "Groups", dashboard_groups_path +- page_title _('New Group') +- header_title _("Groups"), dashboard_groups_path +.page-title-holder + %h1.page-title= _('New group') .row.prepend-top-default .col-lg-3.profile-settings-sidebar - %h4.prepend-top-0 - = _('New group') %p - group_docs_path = help_page_path('user/group/index') - group_docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: group_docs_path } @@ -15,24 +15,29 @@ - subgroup_docs_path = help_page_path('user/group/subgroups/index') - subgroup_docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: subgroup_docs_path } = s_('Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}.').html_safe % { subgroup_docs_link_start: subgroup_docs_link_start, subgroup_docs_link_end: '</a>'.html_safe } + %p + = _('Projects that belong to a group are prefixed with the group namespace. Existing projects may be moved into a group.') .col-lg-9 = form_for @group, html: { class: 'group-form gl-show-field-errors' } do |f| = form_errors(@group) = render 'shared/group_form', f: f, autofocus: true - .form-group.row.group-description-holder - = f.label :avatar, "Group avatar", class: 'col-form-label col-sm-2' - .col-sm-10 - = render 'shared/choose_group_avatar_button', f: f - - = render 'shared/old_visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group, with_label: false + .row + .form-group.group-description-holder.col-sm-12 + = f.label :avatar, _("Group avatar"), class: 'label-bold' + %div + = render 'shared/choose_group_avatar_button', f: f - = render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled + .form-group.col-sm-12 + %label.label-bold + = _('Visibility level') + %p + = _('Who will be able to see this group?') + = link_to _('View the documentation'), help_page_path("public_access/public_access"), target: '_blank' + = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group, with_label: false - .form-group.row - .offset-sm-2.col-sm-10 - = render 'shared/group_tips' + = render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled .form-actions = f.submit 'Create group', class: "btn btn-success" diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml index 647948c7dff..a5e6abdba52 100644 --- a/app/views/groups/settings/ci_cd/show.html.haml +++ b/app/views/groups/settings/ci_cd/show.html.haml @@ -3,7 +3,7 @@ - expanded = Rails.env.test? -%section.settings#secret-variables.no-animate{ class: ('expanded' if expanded) } +%section.settings#ci-variables.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 = _('Variables') diff --git a/app/views/instance_statistics/conversational_development_index/_disabled.html.haml b/app/views/instance_statistics/conversational_development_index/_disabled.html.haml index 0a5717f75e1..b854e15d36f 100644 --- a/app/views/instance_statistics/conversational_development_index/_disabled.html.haml +++ b/app/views/instance_statistics/conversational_development_index/_disabled.html.haml @@ -11,4 +11,4 @@ %p = _('Enable usage ping to get an overview of how you are using GitLab from a feature perspective.') - if current_user.admin? - = link_to _('Enable usage ping'), admin_application_settings_path(anchor: 'usage-statistics'), class: 'btn btn-primary' + = link_to _('Enable usage ping'), metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), class: 'btn btn-primary' diff --git a/app/views/layouts/_flash.html.haml b/app/views/layouts/_flash.html.haml index 8bd5708d490..2cdaa85bdaa 100644 --- a/app/views/layouts/_flash.html.haml +++ b/app/views/layouts/_flash.html.haml @@ -6,5 +6,5 @@ -# Don't show a flash message if the message is nil - if value %div{ class: "flash-#{key}" } - %div{ class: "#{container_class} #{extra_flash_class}" } + %div{ class: "#{(container_class unless fluid_layout)} #{(extra_flash_class unless @no_container)} #{@content_class}" } %span= value diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 1420b0a4973..1b2a4cd6780 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -6,12 +6,12 @@ .mobile-overlay .alert-wrapper = render "layouts/broadcast" - = render 'layouts/header/read_only_banner' + = render "layouts/header/read_only_banner" = yield :flash_message = render "shared/ping_consent" - unless @hide_breadcrumbs = render "layouts/nav/breadcrumbs" - = render "layouts/flash" + = render "layouts/flash", extra_flash_class: 'limit-container-width' .d-flex %div{ class: "#{(container_class unless @no_container)} #{@content_class}" } .content{ id: "content-body" } diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 596fc3985b3..b7d69539eb7 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -5,7 +5,7 @@ - else - search_path_url = search_path -%header.navbar.navbar-gitlab.qa-navbar.navbar-expand-sm +%header.navbar.navbar-gitlab.qa-navbar.navbar-expand-sm.js-navbar %a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content .container-fluid .header-content diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index 4aa22138498..163556f4509 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -12,7 +12,7 @@ = @group.name %ul.sidebar-top-level-items.qa-group-sidebar - if group_sidebar_link?(:overview) - = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups', 'analytics#show'], html_options: { class: 'home' }) do + = nav_link(path: group_overview_nav_link_paths, html_options: { class: 'home' }) do = link_to group_path(@group) do .nav-icon-container = sprite_icon('home') @@ -36,6 +36,16 @@ %span = _('Activity') + = render_if_exists 'groups/sidebar/security_dashboard' + + - if group_sidebar_link?(:contribution_analytics) + = nav_link(path: 'analytics#show') do + = link_to group_analytics_path(@group), title: 'Contribution Analytics', data: {placement: 'right'} do + %span + Contribution Analytics + + = render_if_exists "layouts/nav/ee/epic_link", group: @group + - if group_sidebar_link?(:issues) = nav_link(path: issues_sub_menu_items) do = link_to issues_group_path(@group) do @@ -132,4 +142,6 @@ %span = _('CI / CD') + = render_if_exists "groups/ee/settings_nav" + = render 'shared/sidebar_toggle_button' diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index 156c0d05b02..7c378633667 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -46,7 +46,7 @@ Layout width = f.select :layout, layout_choices, {}, class: 'form-control' .form-text.text-muted - Choose between fixed (max. 1200px) and fluid (100%) application layout. + Choose between fixed (max. 1280px) and fluid (100%) application layout. .form-group = f.label :dashboard, class: 'label-bold' do Default dashboard @@ -56,6 +56,6 @@ Project overview content = f.select :project_view, project_view_choices, {}, class: 'form-control' .form-text.text-muted - Choose what content you want to see on a project’s overview page + Choose what content you want to see on a project’s overview page. .form-group = f.submit 'Save changes', class: 'btn btn-success' diff --git a/app/views/projects/blob/viewers/_highlight_embed.html.haml b/app/views/projects/blob/viewers/_highlight_embed.html.haml index 9bd4ef6ad0b..6a631fef1a9 100644 --- a/app/views/projects/blob/viewers/_highlight_embed.html.haml +++ b/app/views/projects/blob/viewers/_highlight_embed.html.haml @@ -4,4 +4,6 @@ - blob.data.each_line.each_with_index do |_, index| %span.diff-line-num= index + 1 .blob-content{ data: { blob_id: blob.id } } - = highlight(blob.path, blob.data, repository: nil, plain: blob.no_highlighting?) + %pre.code.highlight + %code + = blob.present.highlight diff --git a/app/views/projects/blob/viewers/_text.html.haml b/app/views/projects/blob/viewers/_text.html.haml index a91df321ca0..26ad23da436 100644 --- a/app/views/projects/blob/viewers/_text.html.haml +++ b/app/views/projects/blob/viewers/_text.html.haml @@ -1 +1 @@ -= render 'shared/file_highlight', blob: viewer.blob, repository: @repository += render 'shared/file_highlight', blob: viewer.blob diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index f5685d3b50d..0b10c66777a 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -105,10 +105,10 @@ = icon('remove', class: 'cred') - elsif job.scheduled? .btn-group - .btn.btn-default.has-tooltip{ disabled: true, - title: job.scheduled_at } + .btn.btn-default{ disabled: true } = sprite_icon('planning') - = duration_in_numbers(job.execute_in) + %time.js-remaining-time{ datetime: job.scheduled_at.utc.iso8601 } + = duration_in_numbers(job.execute_in) - confirmation_message = s_("DelayedJobs|Are you sure you want to run %{job_name} immediately? This job will run automatically after it's timer finishes.") % { job_name: job.name } = link_to play_project_job_path(job.project, job, return_to: request.original_url), method: :post, diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 07fc9e1c682..3aff5538813 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -148,7 +148,7 @@ = link_to 'Archive project', archive_project_path(@project), data: { confirm: "Are you sure that you want to archive this project?" }, method: :post, class: "btn btn-warning" - .sub-section.rename-respository + .sub-section.rename-repository %h4.warning-title Rename repository = render 'projects/errors' diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 75f35360e5e..936900a0087 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -36,7 +36,7 @@ .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller .fade-left= icon('angle-left') .fade-right= icon('angle-right') - .nav-links.scrolling-tabs + .nav-links.scrolling-tabs.quick-links = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index efc2d88172e..5d1bbb077af 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -21,6 +21,7 @@ window.gl.mrWidgetData = #{serialize_issuable(@merge_request, serializer: 'widget')} window.gl.mrWidgetData.squash_before_merge_help_path = '#{help_page_path("user/project/merge_requests/squash_and_merge")}'; + window.gl.mrWidgetData.troubleshooting_docs_path = '#{help_page_path('user/project/merge_requests', anchor: 'troubleshooting')}'; #js-vue-mr-widget.mr-widget diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index c63ff070f70..95bba47802c 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -19,30 +19,23 @@ #js-pipeline-graph-vue #js-tab-builds.tab-pane - - if pipeline.yaml_errors.present? - .bs-callout.bs-callout-danger - %h4 Found errors in your .gitlab-ci.yml: - %ul - - pipeline.yaml_errors.split(",").each do |error| - %li= error - You can also test your .gitlab-ci.yml in the #{link_to "Lint", project_ci_lint_path(@project)} + - if pipeline.legacy_stages.present? + .table-holder.pipeline-holder + %table.table.ci-table.pipeline + %thead + %tr + %th Status + %th Job ID + %th Name + %th + %th Coverage + %th + = render partial: "projects/stage/stage", collection: pipeline.legacy_stages, as: :stage - - if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file + - elsif pipeline.project.builds_enabled? && !pipeline.ci_yaml_file .bs-callout.bs-callout-warning \.gitlab-ci.yml not found in this commit - .table-holder.pipeline-holder - %table.table.ci-table.pipeline - %thead - %tr - %th Status - %th Job ID - %th Name - %th - %th Coverage - %th - = render partial: "projects/stage/stage", collection: pipeline.legacy_stages, as: :stage - - if @pipeline.failed_builds.present? #js-tab-failures.build-failures.tab-pane.build-page %table.table.responsive-table.ci-table.responsive-table-sm-rounded diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml index ff0ed3ed30d..193d437dad1 100644 --- a/app/views/projects/pipelines/show.html.haml +++ b/app/views/projects/pipelines/show.html.haml @@ -9,6 +9,14 @@ - if @pipeline.commit.present? = render "projects/pipelines/info", commit: @pipeline.commit - = render "projects/pipelines/with_tabs", pipeline: @pipeline + - if @pipeline.builds.empty? && @pipeline.yaml_errors.present? + .bs-callout.bs-callout-danger + %h4 Found errors in your .gitlab-ci.yml: + %ul + - @pipeline.yaml_errors.split(",").each do |error| + %li= error + You can test your .gitlab-ci.yml in #{link_to "CI Lint", project_ci_lint_path(@project)}. + - else + = render "projects/pipelines/with_tabs", pipeline: @pipeline .js-pipeline-details-vue{ data: { endpoint: project_pipeline_path(@project, @pipeline, format: :json) } } diff --git a/app/views/projects/settings/ci_cd/_badge.html.haml b/app/views/projects/settings/ci_cd/_badge.html.haml index d14360913a4..82c8ec088e5 100644 --- a/app/views/projects/settings/ci_cd/_badge.html.haml +++ b/app/views/projects/settings/ci_cd/_badge.html.haml @@ -15,14 +15,14 @@ .col-md-2.text-center Markdown .col-md-10.code.js-syntax-highlight - = highlight('.md', badge.to_markdown) + = highlight('.md', badge.to_markdown, language: 'markdown') .row %hr .row .col-md-2.text-center HTML .col-md-10.code.js-syntax-highlight - = highlight('.html', badge.to_html) + = highlight('.html', badge.to_html, language: 'html') .row %hr .row diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 283031b06da..f29ce4f5c06 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -22,7 +22,7 @@ .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller .fade-left= icon('angle-left') .fade-right= icon('angle-right') - .nav-links.scrolling-tabs + .nav-links.scrolling-tabs.quick-links = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout) = render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout) diff --git a/app/views/projects/tree/_tree_commit_column.html.haml b/app/views/projects/tree/_tree_commit_column.html.haml index 406dccb74fb..e37fd7624be 100644 --- a/app/views/projects/tree/_tree_commit_column.html.haml +++ b/app/views/projects/tree/_tree_commit_column.html.haml @@ -1,2 +1,2 @@ %span.str-truncated - = link_to_html commit.redacted_full_title_html, project_commit_path(@project, commit.id), class: 'tree-commit-link' + = link_to_html commit.redacted_full_title_html, project_commit_path(@project, commit.id), title: commit.redacted_full_title_html, class: 'tree-commit-link' diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml index 8b95bdf9747..b4ecd7bbae9 100644 --- a/app/views/search/results/_snippet_blob.html.haml +++ b/app/views/search/results/_snippet_blob.html.haml @@ -39,7 +39,7 @@ .blob-content - snippet_chunks.each do |chunk| - unless chunk[:data].empty? - = highlight(snippet.file_name, chunk[:data], repository: nil, plain: snippet.blob.no_highlighting?) + = highlight(snippet.file_name, chunk[:data]) - else .file-content.code .nothing-here-block Empty file diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml index 8d64cb5d698..5073e6ad48f 100644 --- a/app/views/shared/_file_highlight.html.haml +++ b/app/views/shared/_file_highlight.html.haml @@ -1,5 +1,3 @@ -- repository = nil unless local_assigns.key?(:repository) - .file-content.code.js-syntax-highlight .line-numbers - if blob.data.present? @@ -13,4 +11,6 @@ = link_icon = i .blob-content{ data: { blob_id: blob.id } } - = highlight(blob.path, blob.data, repository: repository, plain: blob.no_highlighting?) + %pre.code.highlight + %code + = blob.present.highlight diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml index dbed4b94d61..973c756f496 100644 --- a/app/views/shared/_group_form.html.haml +++ b/app/views/shared/_group_form.html.haml @@ -2,10 +2,19 @@ - group_path = root_url - group_path << parent.full_path + '/' if parent -.form-group.row - = f.label :path, class: 'col-form-label col-sm-2' do - Group path - .col-sm-10 +.row + .form-group.group-name-holder.col-sm-12 + = f.label :name, class: 'label-bold' do + = _("Group name") + = f.text_field :name, placeholder: 'My Awesome Group', class: 'form-control input-lg', + required: true, + title: _('Please fill in a descriptive name for your group.'), + autofocus: true + +.row + .form-group.col-xs-12.col-sm-8 + = f.label :path, class: 'label-bold' do + = _("Group URL") .input-group.gl-field-error-anchor .group-root-path.input-group-prepend.has-tooltip{ title: group_path, :'data-placement' => 'bottom' } .input-group-text @@ -13,10 +22,10 @@ - if parent %strong= parent.full_path + '/' = f.hidden_field :parent_id - = f.text_field :path, placeholder: 'open-source', class: 'form-control', + = f.text_field :path, placeholder: 'my-awesome-group', class: 'form-control', autofocus: local_assigns[:autofocus] || false, required: true, pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, - title: 'Please choose a group path with no special characters.', + title: _('Please choose a group URL with no special characters.'), "data-bind-in" => "#{'create_chat_team' if Gitlab.config.mattermost.enabled}" - if @group.persisted? @@ -25,23 +34,17 @@ = succeed '.' do = link_to 'Learn more', help_page_path('user/group/index', anchor: 'changing-a-groups-path'), target: '_blank' -.form-group.row.group-name-holder - = f.label :name, class: 'col-form-label col-sm-2' do - Group name - .col-sm-10 - = f.text_field :name, class: 'form-control', - required: true, - title: 'You can choose a descriptive name different from the path.' - - if @group.persisted? - .form-group.row.group-name-holder - = f.label :id, class: 'col-form-label col-sm-2' do - = _("Group ID") - .col-sm-10 + .row + .form-group.group-name-holder.col-sm-8 + = f.label :id, class: 'label-bold' do + = _("Group ID") = f.text_field :id, class: 'form-control', readonly: true -.form-group.row.group-description-holder - = f.label :description, class: 'col-form-label col-sm-2' - .col-sm-10 +.row + .form-group.group-description-holder.col-sm-8 + = f.label :description, class: 'label-bold' do + = _("Group description") + %span (optional) = f.text_area :description, maxlength: 250, class: 'form-control js-gfm-input', rows: 4 diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml index e26f5260e5b..c6c5cadc3f5 100644 --- a/app/views/shared/boards/components/_board.html.haml +++ b/app/views/shared/boards/components/_board.html.haml @@ -39,14 +39,13 @@ {{ list.issuesSize }} = render_if_exists "shared/boards/components/list_weight" - - if can?(current_user, :admin_list, current_board_parent) - %button.issue-count-badge-add-button.btn.btn-sm.btn-default.ml-1.has-tooltip.js-no-trigger-collapse{ type: "button", - "@click" => "showNewIssueForm", - "v-if" => 'list.type !== "closed"', - "aria-label" => _("New issue"), - "title" => _("New issue"), - data: { placement: "top", container: "body" } } - = icon("plus", class: "js-no-trigger-collapse") + %button.issue-count-badge-add-button.btn.btn-sm.btn-default.ml-1.has-tooltip.js-no-trigger-collapse{ type: "button", + "@click" => "showNewIssueForm", + "v-if" => "isNewIssueShown", + "aria-label" => _("New issue"), + "title" => _("New issue"), + data: { placement: "top", container: "body" } } + = icon("plus", class: "js-no-trigger-collapse") %board-list{ "v-if" => 'list.type !== "blank" && list.type !== "promotion"', ":list" => "list", diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index cb45928d9a5..d27f79dc404 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -61,7 +61,10 @@ %ul{ data: { dropdown: true } } %li.filter-dropdown-item{ data: { value: 'none' } } %button.btn.btn-link{ type: 'button' } - = _('No Assignee') + = _('None') + %li.filter-dropdown-item{ data: { value: 'any' } } + %button.btn.btn-link{ type: 'button' } + = _('Any') %li.divider.droplab-item-ignore - if current_user = render 'shared/issuable/user_dropdown_item', @@ -81,7 +84,7 @@ %li.filter-dropdown-item{ data: { value: 'upcoming' } } %button.btn.btn-link{ type: 'button' } = _('Upcoming') - %li.filter-dropdown-item{ 'data-value' => 'started' } + %li.filter-dropdown-item{ data: { value: 'started' } } %button.btn.btn-link{ type: 'button' } = _('Started') %li.divider.droplab-item-ignore @@ -102,6 +105,14 @@ %span.label-title.js-data-value {{title}} #js-dropdown-my-reaction.filtered-search-input-dropdown-menu.dropdown-menu + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'none' } } + %button.btn.btn-link{ type: 'button' } + = _('None') + %li.filter-dropdown-item{ data: { value: 'any' } } + %button.btn.btn-link{ type: 'button' } + = _('Any') + %li.divider.droplab-item-ignore %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item %button.btn.btn-link{ type: 'button' } diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 10ffe8dd37f..5295e656ab0 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -24,7 +24,7 @@ .block.milestone .sidebar-collapsed-icon.has-tooltip{ title: milestone_tooltip_title(issuable.milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } } = icon('clock-o', 'aria-hidden': 'true') - %span.milestone-title + %span.milestone-title.collapse-truncated-title - if issuable.milestone = issuable.milestone.title - else diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml index 09ddf732ada..f6c7ca70ebd 100644 --- a/app/views/shared/notifications/_button.html.haml +++ b/app/views/shared/notifications/_button.html.haml @@ -9,11 +9,11 @@ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } } = icon("bell", class: "js-notification-loading") = notification_title(notification_setting.level) - %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } } + %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } } = icon('caret-down') .sr-only Toggle dropdown - else - %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), display: 'static' } } + %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } } = icon("bell", class: "js-notification-loading") = notification_title(notification_setting.level) = icon("caret-down") diff --git a/app/views/shared/projects/_search_form.html.haml b/app/views/shared/projects/_search_form.html.haml index b89194bcc67..3b5c13ed93a 100644 --- a/app/views/shared/projects/_search_form.html.haml +++ b/app/views/shared/projects/_search_form.html.haml @@ -21,3 +21,5 @@ - if params[:visibility_level].present? = hidden_field_tag :visibility_level, params[:visibility_level] + + = render_if_exists 'shared/projects/search_fields' diff --git a/app/views/sherlock/queries/_general.html.haml b/app/views/sherlock/queries/_general.html.haml index ddc089b0bd7..52c7bc47ca7 100644 --- a/app/views/sherlock/queries/_general.html.haml +++ b/app/views/sherlock/queries/_general.html.haml @@ -36,7 +36,7 @@ %li .code.js-syntax-highlight.sherlock-code :preserve - #{highlight("#{@query.id}.sql", @query.formatted_query)} + #{highlight("#{@query.id}.sql", @query.formatted_query, language: 'sql')} .card .card-header diff --git a/app/views/sherlock/transactions/_queries.html.haml b/app/views/sherlock/transactions/_queries.html.haml index c1ec4b91bb6..5e224f3aa0e 100644 --- a/app/views/sherlock/transactions/_queries.html.haml +++ b/app/views/sherlock/transactions/_queries.html.haml @@ -17,7 +17,7 @@ = t('sherlock.milliseconds') %td .code.js-syntax-highlight.sherlock-code - = highlight("#{query.id}.sql", query.formatted_query) + = highlight("#{query.id}.sql", query.formatted_query, language: 'sql') %td = link_to(t('sherlock.view'), sherlock_transaction_query_path(@transaction, query), diff --git a/app/workers/gitlab/github_import/advance_stage_worker.rb b/app/workers/gitlab/github_import/advance_stage_worker.rb index cd2ceb8dcdf..2b49860025a 100644 --- a/app/workers/gitlab/github_import/advance_stage_worker.rb +++ b/app/workers/gitlab/github_import/advance_stage_worker.rb @@ -14,7 +14,7 @@ module Gitlab INTERVAL = 30.seconds.to_i # The number of seconds to wait (while blocking the thread) before - # continueing to the next waiter. + # continuing to the next waiter. BLOCKING_WAIT_TIME = 5 # The known importer stages and their corresponding Sidekiq workers. diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index 09a594cdb4e..72a1733a2a8 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -29,15 +29,14 @@ class PostReceive def process_project_changes(post_received) changes = [] refs = Set.new + @user = post_received.identify - post_received.changes_refs do |oldrev, newrev, ref| - @user ||= post_received.identify(newrev) - - unless @user - log("Triggered hook for non-existing user \"#{post_received.identifier}\"") - return false # rubocop:disable Cop/AvoidReturnFromBlocks - end + unless @user + log("Triggered hook for non-existing user \"#{post_received.identifier}\"") + return false + end + post_received.changes_refs do |oldrev, newrev, ref| if Gitlab::Git.tag_ref?(ref) GitTagPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute elsif Gitlab::Git.branch_ref?(ref) |