diff options
Diffstat (limited to 'app')
78 files changed, 1461 insertions, 1264 deletions
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue index f248f53fa51..f7ce5128964 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.vue +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -1,5 +1,6 @@ <script> import $ from 'jquery'; +import { Button } from '@gitlab-org/gitlab-ui'; import eventHub from '../eventhub'; import ProjectSelect from './project_select.vue'; import ListIssue from '../models/issue'; @@ -10,6 +11,7 @@ export default { name: 'BoardNewIssue', components: { ProjectSelect, + 'gl-button': Button, }, props: { groupId: { @@ -123,21 +125,23 @@ export default { :group-id="groupId" /> <div class="clearfix prepend-top-10"> - <button + <gl-button ref="submit-button" :disabled="disabled" - class="btn btn-success float-left" + class="float-left" + variant="success" type="submit" > Submit issue - </button> - <button - class="btn btn-default float-right" + </gl-button> + <gl-button + class="float-right" type="button" + variant="default" @click="cancel" > Cancel - </button> + </gl-button> </div> </form> </div> diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index fc41ee4b777..e60c53338fe 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -5,22 +5,22 @@ import { __ } from '~/locale'; import createFlash from '~/flash'; import eventHub from '../../notes/event_hub'; import CompareVersions from './compare_versions.vue'; -import ChangedFiles from './changed_files.vue'; import DiffFile from './diff_file.vue'; import NoChanges from './no_changes.vue'; import HiddenFilesWarning from './hidden_files_warning.vue'; import CommitWidget from './commit_widget.vue'; +import TreeList from './tree_list.vue'; export default { name: 'DiffsApp', components: { Icon, CompareVersions, - ChangedFiles, DiffFile, NoChanges, HiddenFilesWarning, CommitWidget, + TreeList, }, props: { endpoint: { @@ -58,6 +58,7 @@ export default { plainDiffPath: state => state.diffs.plainDiffPath, emailPatchPath: state => state.diffs.emailPatchPath, }), + ...mapState('diffs', ['showTreeList']), ...mapGetters('diffs', ['isParallelView']), ...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']), targetBranch() { @@ -88,6 +89,9 @@ export default { canCurrentUserFork() { return this.currentUser.canFork === true && this.currentUser.canCreateMergeRequest; }, + showCompareVersions() { + return this.mergeRequestDiffs && this.mergeRequestDiff; + }, }, watch: { diffViewType() { @@ -102,6 +106,8 @@ export default { this.adjustView(); }, + isLoading: 'adjustView', + showTreeList: 'adjustView', }, mounted() { this.setBaseConfig({ endpoint: this.endpoint, projectPath: this.projectPath }); @@ -152,10 +158,11 @@ export default { } }, adjustView() { - if (this.shouldShow && this.isParallelView) { - window.mrTabs.expandViewContainer(); - } else { - window.mrTabs.resetViewContainer(); + if (this.shouldShow) { + this.$nextTick(() => { + window.mrTabs.resetViewContainer(); + window.mrTabs.expandViewContainer(this.showTreeList); + }); } }, }, @@ -177,7 +184,7 @@ export default { class="diffs tab-pane" > <compare-versions - v-if="!commit && mergeRequestDiffs.length > 1" + v-if="showCompareVersions" :merge-request-diffs="mergeRequestDiffs" :merge-request-diff="mergeRequestDiff" :start-version="startVersion" @@ -215,22 +222,26 @@ export default { :commit="commit" /> - <changed-files - :diff-files="diffFiles" - /> - - <div - v-if="diffFiles.length > 0" - class="files" - > - <diff-file - v-for="file in diffFiles" - :key="file.newPath" - :file="file" - :can-current-user-fork="canCurrentUserFork" - /> + <div class="files d-flex prepend-top-default"> + <div + v-show="showTreeList" + class="diff-tree-list" + > + <tree-list /> + </div> + <div + v-if="diffFiles.length > 0" + class="diff-files-holder" + > + <diff-file + v-for="file in diffFiles" + :key="file.newPath" + :file="file" + :can-current-user-fork="canCurrentUserFork" + /> + </div> + <no-changes v-else /> </div> - <no-changes v-else /> </div> </div> </template> diff --git a/app/assets/javascripts/diffs/components/changed_files.vue b/app/assets/javascripts/diffs/components/changed_files.vue deleted file mode 100644 index 97751db1254..00000000000 --- a/app/assets/javascripts/diffs/components/changed_files.vue +++ /dev/null @@ -1,171 +0,0 @@ -<script> -import { mapGetters, mapActions } from 'vuex'; -import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; -import Icon from '~/vue_shared/components/icon.vue'; -import { pluralize } from '~/lib/utils/text_utility'; -import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility'; -import { contentTop } from '~/lib/utils/common_utils'; -import { __ } from '~/locale'; -import ChangedFilesDropdown from './changed_files_dropdown.vue'; -import changedFilesMixin from '../mixins/changed_files'; - -export default { - components: { - Icon, - ChangedFilesDropdown, - ClipboardButton, - }, - mixins: [changedFilesMixin], - data() { - return { - isStuck: false, - maxWidth: 'auto', - offsetTop: 0, - }; - }, - computed: { - ...mapGetters('diffs', ['isInlineView', 'isParallelView', 'areAllFilesCollapsed']), - sumAddedLines() { - return this.sumValues('addedLines'); - }, - sumRemovedLines() { - return this.sumValues('removedLines'); - }, - whitespaceVisible() { - return !getParameterValues('w')[0]; - }, - toggleWhitespaceText() { - if (this.whitespaceVisible) { - return __('Hide whitespace changes'); - } - return __('Show whitespace changes'); - }, - toggleWhitespacePath() { - if (this.whitespaceVisible) { - return mergeUrlParams({ w: 1 }, window.location.href); - } - - return mergeUrlParams({ w: 0 }, window.location.href); - }, - top() { - return `${this.offsetTop}px`; - }, - }, - created() { - document.addEventListener('scroll', this.handleScroll); - this.offsetTop = contentTop(); - }, - beforeDestroy() { - document.removeEventListener('scroll', this.handleScroll); - }, - methods: { - ...mapActions('diffs', ['setInlineDiffViewType', 'setParallelDiffViewType', 'expandAllFiles']), - pluralize, - handleScroll() { - if (!this.updating) { - this.$nextTick(this.updateIsStuck); - this.updating = true; - } - }, - updateIsStuck() { - if (!this.$refs.wrapper) { - return; - } - - const scrollPosition = window.scrollY; - - this.isStuck = scrollPosition + this.offsetTop >= this.$refs.placeholder.offsetTop; - this.updating = false; - }, - sumValues(key) { - return this.diffFiles.reduce((total, file) => total + file[key], 0); - }, - }, -}; -</script> - -<template> - <span> - <div ref="placeholder"></div> - <div - ref="wrapper" - :style="{ top }" - :class="{'is-stuck': isStuck}" - class="content-block oneline-block diff-files-changed diff-files-changed-merge-request - files-changed js-diff-files-changed" - > - <div class="files-changed-inner"> - <div - class="inline-parallel-buttons d-none d-md-block" - > - <a - v-if="areAllFilesCollapsed" - class="btn btn-default" - @click="expandAllFiles" - > - {{ __('Expand all') }} - </a> - <a - :href="toggleWhitespacePath" - class="btn btn-default" - > - {{ toggleWhitespaceText }} - </a> - <div class="btn-group"> - <button - id="inline-diff-btn" - :class="{ active: isInlineView }" - type="button" - class="btn js-inline-diff-button" - data-view-type="inline" - @click="setInlineDiffViewType" - > - {{ __('Inline') }} - </button> - <button - id="parallel-diff-btn" - :class="{ active: isParallelView }" - type="button" - class="btn js-parallel-diff-button" - data-view-type="parallel" - @click="setParallelDiffViewType" - > - {{ __('Side-by-side') }} - </button> - </div> - </div> - - <div class="commit-stat-summary dropdown"> - <changed-files-dropdown - :diff-files="diffFiles" - /> - - <span - class="js-diff-stats-additions-deletions-expanded - diff-stats-additions-deletions-expanded" - > - with - <strong class="cgreen"> - {{ pluralize(`${sumAddedLines} addition`, sumAddedLines) }} - </strong> - and - <strong class="cred"> - {{ pluralize(`${sumRemovedLines} deletion`, sumRemovedLines) }} - </strong> - </span> - <div - class="js-diff-stats-additions-deletions-collapsed - diff-stats-additions-deletions-collapsed float-right d-sm-none" - > - <strong class="cgreen"> - +{{ sumAddedLines }} - </strong> - <strong class="cred"> - -{{ sumRemovedLines }} - </strong> - </div> - </div> - </div> - </div> - </span> -</template> diff --git a/app/assets/javascripts/diffs/components/changed_files_dropdown.vue b/app/assets/javascripts/diffs/components/changed_files_dropdown.vue deleted file mode 100644 index 0ec6b8b7f21..00000000000 --- a/app/assets/javascripts/diffs/components/changed_files_dropdown.vue +++ /dev/null @@ -1,126 +0,0 @@ -<script> -import Icon from '~/vue_shared/components/icon.vue'; -import changedFilesMixin from '../mixins/changed_files'; - -export default { - components: { - Icon, - }, - mixins: [changedFilesMixin], - data() { - return { - searchText: '', - }; - }, - computed: { - filteredDiffFiles() { - return this.diffFiles.filter(file => - file.filePath.toLowerCase().includes(this.searchText.toLowerCase()), - ); - }, - }, - methods: { - clearSearch() { - this.searchText = ''; - }, - }, -}; -</script> - -<template> - <span> - Showing - <button - class="diff-stats-summary-toggler" - data-toggle="dropdown" - type="button" - aria-expanded="false" - > - <span> - {{ n__('%d changed file', '%d changed files', diffFiles.length) }} - </span> - <icon - class="caret-icon" - name="chevron-down" - /> - </button> - <div class="dropdown-menu diff-file-changes"> - <div class="dropdown-input"> - <input - v-model="searchText" - type="search" - class="dropdown-input-field" - placeholder="Search files" - autocomplete="off" - /> - <i - v-if="searchText.length === 0" - aria-hidden="true" - data-hidden="true" - class="fa fa-search dropdown-input-search"> - </i> - <i - v-else - role="button" - class="fa fa-times dropdown-input-search" - @click.stop.prevent="clearSearch" - ></i> - </div> - <div class="dropdown-content"> - <ul> - <li - v-for="diffFile in filteredDiffFiles" - :key="diffFile.name" - > - <a - :href="`#${diffFile.fileHash}`" - :title="diffFile.newPath" - class="diff-changed-file" - > - <icon - :name="fileChangedIcon(diffFile)" - :size="16" - :class="fileChangedClass(diffFile)" - class="diff-file-changed-icon append-right-8" - /> - <span class="diff-changed-file-content append-right-8"> - <strong - v-if="diffFile.blob && diffFile.blob.name" - class="diff-changed-file-name" - > - {{ diffFile.blob.name }} - </strong> - <strong - v-else - class="diff-changed-blank-file-name" - > - {{ s__('Diffs|No file name available') }} - </strong> - <span class="diff-changed-file-path prepend-top-5"> - {{ truncatedDiffPath(diffFile.blob.path) }} - </span> - </span> - <span class="diff-changed-stats"> - <span class="cgreen"> - +{{ diffFile.addedLines }} - </span> - <span class="cred"> - -{{ diffFile.removedLines }} - </span> - </span> - </a> - </li> - - <li - v-show="filteredDiffFiles.length === 0" - class="dropdown-menu-empty-item" - > - <a> - {{ __('No files found') }} - </a> - </li> - </ul> - </div> - </div> - </span> -</template> diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue index 1c9ad8e77f1..9bbf62c0eb6 100644 --- a/app/assets/javascripts/diffs/components/compare_versions.vue +++ b/app/assets/javascripts/diffs/components/compare_versions.vue @@ -1,9 +1,18 @@ <script> +import { mapActions, mapGetters, mapState } from 'vuex'; +import Tooltip from '@gitlab-org/gitlab-ui/dist/directives/tooltip'; +import { __ } from '~/locale'; +import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility'; +import Icon from '~/vue_shared/components/icon.vue'; import CompareVersionsDropdown from './compare_versions_dropdown.vue'; export default { components: { CompareVersionsDropdown, + Icon, + }, + directives: { + Tooltip, }, props: { mergeRequestDiffs: { @@ -26,30 +35,119 @@ export default { }, }, computed: { + ...mapState('diffs', ['commit', 'showTreeList']), + ...mapGetters('diffs', ['isInlineView', 'isParallelView', 'areAllFilesCollapsed']), comparableDiffs() { return this.mergeRequestDiffs.slice(1); }, + isWhitespaceVisible() { + return !getParameterValues('w')[0]; + }, + toggleWhitespaceText() { + if (this.isWhitespaceVisible) { + return __('Hide whitespace changes'); + } + return __('Show whitespace changes'); + }, + toggleWhitespacePath() { + if (this.isWhitespaceVisible) { + return mergeUrlParams({ w: 1 }, window.location.href); + } + + return mergeUrlParams({ w: 0 }, window.location.href); + }, + showDropdowns() { + return !this.commit && this.mergeRequestDiffs.length; + }, + }, + methods: { + ...mapActions('diffs', [ + 'setInlineDiffViewType', + 'setParallelDiffViewType', + 'expandAllFiles', + 'toggleShowTreeList', + ]), }, }; </script> <template> <div class="mr-version-controls"> - <div class="mr-version-menus-container content-block"> - Changes between - <compare-versions-dropdown - :other-versions="mergeRequestDiffs" - :merge-request-version="mergeRequestDiff" - :show-commit-count="true" - class="mr-version-dropdown" - /> - and - <compare-versions-dropdown - :other-versions="comparableDiffs" - :start-version="startVersion" - :target-branch="targetBranch" - class="mr-version-compare-dropdown" - /> + <div + class="mr-version-menus-container content-block" + > + <button + v-tooltip.hover + type="button" + class="btn btn-default append-right-8 js-toggle-tree-list" + :class="{ + active: showTreeList + }" + :title="__('Toggle file browser')" + @click="toggleShowTreeList" + > + <icon + name="hamburger" + /> + </button> + <div + v-if="showDropdowns" + class="d-flex align-items-center compare-versions-container" + > + Changes between + <compare-versions-dropdown + :other-versions="mergeRequestDiffs" + :merge-request-version="mergeRequestDiff" + :show-commit-count="true" + class="mr-version-dropdown" + /> + and + <compare-versions-dropdown + :other-versions="comparableDiffs" + :start-version="startVersion" + :target-branch="targetBranch" + class="mr-version-compare-dropdown" + /> + </div> + <div + class="inline-parallel-buttons d-none d-md-flex ml-auto" + > + <a + v-if="areAllFilesCollapsed" + class="btn btn-default" + @click="expandAllFiles" + > + {{ __('Expand all') }} + </a> + <a + :href="toggleWhitespacePath" + class="btn btn-default" + > + {{ toggleWhitespaceText }} + </a> + <div class="btn-group prepend-left-8"> + <button + id="inline-diff-btn" + :class="{ active: isInlineView }" + type="button" + class="btn js-inline-diff-button" + data-view-type="inline" + @click="setInlineDiffViewType" + > + {{ __('Inline') }} + </button> + <button + id="parallel-diff-btn" + :class="{ active: isParallelView }" + type="button" + class="btn js-parallel-diff-button" + data-view-type="parallel" + @click="setParallelDiffViewType" + > + {{ __('Side-by-side') }} + </button> + </div> + </div> </div> </div> </template> diff --git a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue index 96cccb49378..c3acc352d5e 100644 --- a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue +++ b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue @@ -108,7 +108,7 @@ export default { <template> <span class="dropdown inline"> <a - class="dropdown-toggle btn btn-default" + class="dropdown-menu-toggle btn btn-default w-100" data-toggle="dropdown" aria-expanded="false" > @@ -118,6 +118,7 @@ export default { <Icon :size="12" name="angle-down" + class="position-absolute" /> </a> <div class="dropdown-menu dropdown-select dropdown-menu-selectable"> @@ -163,3 +164,10 @@ export default { </div> </span> </template> + +<style> +.dropdown { + min-width: 0; + max-height: 170px; +} +</style> diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index bcbe374a90c..4e04e50c52a 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -1,5 +1,5 @@ <script> -import { mapActions, mapGetters } from 'vuex'; +import { mapActions, mapGetters, mapState } from 'vuex'; import _ from 'underscore'; import { __, sprintf } from '~/locale'; import createFlash from '~/flash'; @@ -28,6 +28,7 @@ export default { }; }, computed: { + ...mapState('diffs', ['currentDiffFileId']), ...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']), isCollapsed() { return this.file.collapsed || false; @@ -101,6 +102,9 @@ export default { <template> <div :id="file.fileHash" + :class="{ + 'is-active': currentDiffFileId === file.fileHash + }" class="diff-file file-holder" > <diff-file-header @@ -168,3 +172,20 @@ export default { </div> </div> </template> + +<style> +@keyframes shadow-fade { + from { + box-shadow: 0 0 4px #919191; + } + + to { + box-shadow: 0 0 0 #dfdfdf; + } +} + +.diff-file.is-active { + box-shadow: 0 0 0 #dfdfdf; + animation: shadow-fade 1.2s 0.1s 1; +} +</style> diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index 517fbf400e8..15b37243030 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -166,18 +166,16 @@ export default { :title="diffFile.oldPath" class="file-title-name" data-container="body" - > - {{ diffFile.oldPath }} - </strong> + v-html="diffFile.oldPathHtml" + ></strong> → <strong v-tooltip :title="diffFile.newPath" class="file-title-name" data-container="body" - > - {{ diffFile.newPath }} - </strong> + v-html="diffFile.newPathHtml" + ></strong> </span> <strong diff --git a/app/assets/javascripts/diffs/components/file_row_stats.vue b/app/assets/javascripts/diffs/components/file_row_stats.vue new file mode 100644 index 00000000000..105f7ebdbed --- /dev/null +++ b/app/assets/javascripts/diffs/components/file_row_stats.vue @@ -0,0 +1,30 @@ +<script> +export default { + props: { + file: { + type: Object, + required: true, + }, + }, +}; +</script> + +<template> + <span + v-once + class="file-row-stats" + > + <span class="cgreen"> + +{{ file.addedLines }} + </span> + <span class="cred"> + -{{ file.removedLines }} + </span> + </span> +</template> + +<style> +.file-row-stats { + font-size: 12px; +} +</style> diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue new file mode 100644 index 00000000000..cfe4273742f --- /dev/null +++ b/app/assets/javascripts/diffs/components/tree_list.vue @@ -0,0 +1,101 @@ +<script> +import { mapActions, mapGetters, mapState } from 'vuex'; +import Icon from '~/vue_shared/components/icon.vue'; +import FileRow from '~/vue_shared/components/file_row.vue'; +import FileRowStats from './file_row_stats.vue'; + +export default { + components: { + Icon, + FileRow, + }, + data() { + return { + search: '', + }; + }, + computed: { + ...mapState('diffs', ['tree', 'addedLines', 'removedLines']), + ...mapGetters('diffs', ['allBlobs', 'diffFilesLength']), + filteredTreeList() { + const search = this.search.toLowerCase().trim(); + + if (search === '') return this.tree; + + return this.allBlobs.filter(f => f.name.toLowerCase().indexOf(search) >= 0); + }, + }, + methods: { + ...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']), + clearSearch() { + this.search = ''; + }, + }, + FileRowStats, +}; +</script> + +<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" + > + <icon + name="close" + /> + </button> + </div> + <div + class="tree-list-scroll" + > + <template v-if="filteredTreeList.length"> + <file-row + v-for="file in filteredTreeList" + :key="file.key" + :file="file" + :level="0" + :hide-extra-on-tree="true" + :extra-component="$options.FileRowStats" + :show-changed-icon="true" + @toggleTreeOpen="toggleTreeOpen" + @clickFile="scrollToFile" + /> + </template> + <p + v-else + class="prepend-top-20 append-bottom-20 text-center" + > + {{ s__('MergeRequest|No files found') }} + </p> + </div> + <div + v-once + class="pt-3 pb-3 text-center" + > + {{ n__('%d changed file', '%d changed files', diffFilesLength) }} + <div> + <span class="cgreen"> + {{ n__('%d addition', '%d additions', addedLines) }} + </span> + <span class="cred"> + {{ n__('%d deleted', '%d deletions', removedLines) }} + </span> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js index 2795dddfc48..6a50d2c1426 100644 --- a/app/assets/javascripts/diffs/constants.js +++ b/app/assets/javascripts/diffs/constants.js @@ -29,3 +29,5 @@ export const LENGTH_OF_AVATAR_TOOLTIP = 17; export const LINES_TO_BE_RENDERED_DIRECTLY = 100; export const MAX_LINES_TO_BE_RENDERED = 2000; + +export const MR_TREE_SHOW_KEY = 'mr_tree_show'; diff --git a/app/assets/javascripts/diffs/mixins/changed_files.js b/app/assets/javascripts/diffs/mixins/changed_files.js deleted file mode 100644 index da1339f0ffa..00000000000 --- a/app/assets/javascripts/diffs/mixins/changed_files.js +++ /dev/null @@ -1,38 +0,0 @@ -export default { - props: { - diffFiles: { - type: Array, - required: true, - }, - }, - methods: { - fileChangedIcon(diffFile) { - if (diffFile.deletedFile) { - return 'file-deletion'; - } else if (diffFile.newFile) { - return 'file-addition'; - } - return 'file-modified'; - }, - fileChangedClass(diffFile) { - if (diffFile.deletedFile) { - return 'cred'; - } else if (diffFile.newFile) { - return 'cgreen'; - } - - return ''; - }, - truncatedDiffPath(path) { - const maxLength = 60; - - if (path.length > maxLength) { - const start = path.length - maxLength; - const end = start + maxLength; - return `...${path.slice(start, end)}`; - } - - return path; - }, - }, -}; diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 98d8d5943f9..1e0b27b538d 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -12,6 +12,7 @@ import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME, + MR_TREE_SHOW_KEY, } from '../constants'; export const setBaseConfig = ({ commit }, options) => { @@ -195,5 +196,23 @@ export const saveDiffDiscussion = ({ dispatch }, { note, formData }) => { .catch(() => createFlash(s__('MergeRequests|Saving the comment failed'))); }; +export const toggleTreeOpen = ({ commit }, path) => { + commit(types.TOGGLE_FOLDER_OPEN, path); +}; + +export const scrollToFile = ({ state, commit }, path) => { + const { fileHash } = state.treeEntries[path]; + document.location.hash = fileHash; + + commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash); + + setTimeout(() => commit(types.UPDATE_CURRENT_DIFF_FILE_ID, ''), 1000); +}; + +export const toggleShowTreeList = ({ commit, state }) => { + commit(types.TOGGLE_SHOW_TREE_LIST); + localStorage.setItem(MR_TREE_SHOW_KEY, state.showTreeList); +}; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js index 968ba3c5e13..d4c205882ff 100644 --- a/app/assets/javascripts/diffs/store/getters.js +++ b/app/assets/javascripts/diffs/store/getters.js @@ -110,5 +110,9 @@ export const shouldRenderInlineCommentRow = state => line => { export const getDiffFileByHash = state => fileHash => state.diffFiles.find(file => file.fileHash === fileHash); +export const allBlobs = state => Object.values(state.treeEntries).filter(f => f.type === 'blob'); + +export const diffFilesLength = state => state.diffFiles.length; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js index eb596b251c1..ae8930c8968 100644 --- a/app/assets/javascripts/diffs/store/modules/diff_state.js +++ b/app/assets/javascripts/diffs/store/modules/diff_state.js @@ -1,10 +1,11 @@ import Cookies from 'js-cookie'; import { getParameterValues } from '~/lib/utils/url_utility'; -import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME } from '../../constants'; +import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME, MR_TREE_SHOW_KEY } from '../../constants'; const viewTypeFromQueryString = getParameterValues('view')[0]; const viewTypeFromCookie = Cookies.get(DIFF_VIEW_COOKIE_NAME); const defaultViewType = INLINE_DIFF_VIEW_TYPE; +const storedTreeShow = localStorage.getItem(MR_TREE_SHOW_KEY); export default () => ({ isLoading: true, @@ -17,4 +18,8 @@ export default () => ({ mergeRequestDiff: null, diffLineCommentForms: {}, diffViewType: viewTypeFromQueryString || viewTypeFromCookie || defaultViewType, + tree: [], + treeEntries: {}, + showTreeList: storedTreeShow === null ? true : storedTreeShow === 'true', + currentDiffFileId: '', }); diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js index f61efbe6e1e..6474ee628e2 100644 --- a/app/assets/javascripts/diffs/store/mutation_types.js +++ b/app/assets/javascripts/diffs/store/mutation_types.js @@ -11,3 +11,6 @@ export const EXPAND_ALL_FILES = 'EXPAND_ALL_FILES'; export const RENDER_FILE = 'RENDER_FILE'; export const SET_LINE_DISCUSSIONS_FOR_FILE = 'SET_LINE_DISCUSSIONS_FOR_FILE'; export const REMOVE_LINE_DISCUSSIONS_FOR_FILE = 'REMOVE_LINE_DISCUSSIONS_FOR_FILE'; +export const TOGGLE_FOLDER_OPEN = 'TOGGLE_FOLDER_OPEN'; +export const TOGGLE_SHOW_TREE_LIST = 'TOGGLE_SHOW_TREE_LIST'; +export const UPDATE_CURRENT_DIFF_FILE_ID = 'UPDATE_CURRENT_DIFF_FILE_ID'; diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index 59a2c09e54f..0b4485ecdb5 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import { sortTree } from '~/ide/stores/utils'; import { findDiffFile, addLineReferences, @@ -7,6 +8,7 @@ import { addContextLines, prepareDiffData, isDiscussionApplicableToLine, + generateTreeList, } from './utils'; import * as types from './mutation_types'; @@ -23,9 +25,12 @@ export default { [types.SET_DIFF_DATA](state, data) { const diffData = convertObjectPropsToCamelCase(data, { deep: true }); prepareDiffData(diffData); + const { tree, treeEntries } = generateTreeList(diffData.diffFiles); Object.assign(state, { ...diffData, + tree: sortTree(tree), + treeEntries, }); }, @@ -163,4 +168,13 @@ export default { } } }, + [types.TOGGLE_FOLDER_OPEN](state, path) { + state.treeEntries[path].opened = !state.treeEntries[path].opened; + }, + [types.TOGGLE_SHOW_TREE_LIST](state) { + state.showTreeList = !state.showTreeList; + }, + [types.UPDATE_CURRENT_DIFF_FILE_ID](state, fileId) { + state.currentDiffFileId = fileId; + }, }; diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js index 631e3de311e..4ae588042e4 100644 --- a/app/assets/javascripts/diffs/store/utils.js +++ b/app/assets/javascripts/diffs/store/utils.js @@ -267,3 +267,49 @@ export function isDiscussionApplicableToLine({ discussion, diffPosition, latestD return latestDiff && discussion.active && lineCode === discussion.line_code; } + +export const generateTreeList = files => + files.reduce( + (acc, file) => { + const { fileHash, addedLines, removedLines, newFile, deletedFile, newPath } = file; + const split = newPath.split('/'); + + split.forEach((name, i) => { + const parent = acc.treeEntries[split.slice(0, i).join('/')]; + const path = `${parent ? `${parent.path}/` : ''}${name}`; + + if (!acc.treeEntries[path]) { + const type = path === newPath ? 'blob' : 'tree'; + acc.treeEntries[path] = { + key: path, + path, + name, + type, + tree: [], + }; + + const entry = acc.treeEntries[path]; + + if (type === 'blob') { + Object.assign(entry, { + changed: true, + tempFile: newFile, + deleted: deletedFile, + fileHash, + addedLines, + removedLines, + }); + } else { + Object.assign(entry, { + opened: true, + }); + } + + (parent ? parent.tree : acc.tree).push(entry); + } + }); + + return acc; + }, + { treeEntries: {}, tree: [] }, + ); diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue index ccc8419ca6d..a0797b594cb 100644 --- a/app/assets/javascripts/environments/components/environment_monitoring.vue +++ b/app/assets/javascripts/environments/components/environment_monitoring.vue @@ -2,12 +2,14 @@ /** * Renders the Monitoring (Metrics) link in environments table. */ +import { Button } 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, }, directives: { tooltip, @@ -26,15 +28,16 @@ export default { }; </script> <template> - <a + <gl-button v-tooltip :href="monitoringUrl" :title="title" :aria-label="title" - class="btn monitoring-url d-none d-sm-none d-md-block" + class="monitoring-url d-none d-sm-none d-md-block" data-container="body" rel="noopener noreferrer nofollow" + variant="default" > <icon name="chart" /> - </a> + </gl-button> </template> diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js index 8aecf9725e6..c568f4e4ebf 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js @@ -51,7 +51,11 @@ export default class DropdownHint extends FilteredSearchDropdown { FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' ')); } - FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''), '', false, this.container); + const key = token.replace(':', ''); + const { uppercaseTokenName } = this.tokenKeys.searchByKey(key); + FilteredSearchDropdownManager.addWordToInput(key, '', false, { + uppercaseTokenName, + }); } this.dismissDropdown(); this.dispatchInputEvent(); diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js index 27fff488603..6da6ca10008 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js @@ -143,7 +143,9 @@ export default class DropdownUtils { const dataValue = selected.getAttribute('data-value'); if (dataValue) { - FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true); + FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true, { + capitalizeTokenValue: selected.hasAttribute('data-capitalize'), + }); } // Return boolean based on whether it was set 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 207616b9de2..cd3d532c958 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -91,6 +91,11 @@ export default class FilteredSearchDropdownManager { gl: DropdownEmoji, element: this.container.querySelector('#js-dropdown-my-reaction'), }, + wip: { + reference: null, + gl: DropdownNonUser, + element: this.container.querySelector('#js-dropdown-wip'), + }, status: { reference: null, gl: NullDropdown, @@ -136,10 +141,16 @@ export default class FilteredSearchDropdownManager { return endpoint; } - static addWordToInput(tokenName, tokenValue = '', clicked = false) { + static addWordToInput(tokenName, tokenValue = '', clicked = false, options = {}) { + const { + uppercaseTokenName = false, + capitalizeTokenValue = false, + } = options; const input = FilteredSearchContainer.container.querySelector('.filtered-search'); - - FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue); + FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue, { + uppercaseTokenName, + capitalizeTokenValue, + }); input.value = ''; if (clicked) { diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index d25f6f95b22..54533ebb70d 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -405,7 +405,10 @@ export default class FilteredSearchManager { if (isLastVisualTokenValid) { tokens.forEach((t) => { input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, ''); - FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`); + FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`, { + uppercaseTokenName: this.filteredSearchTokenKeys.shouldUppercaseTokenName(t.key), + capitalizeTokenValue: this.filteredSearchTokenKeys.shouldCapitalizeTokenValue(t.key), + }); }); const fragments = searchToken.split(':'); @@ -421,7 +424,10 @@ export default class FilteredSearchManager { FilteredSearchVisualTokens.addSearchVisualToken(searchTerms); } - FilteredSearchVisualTokens.addFilterVisualToken(tokenKey); + FilteredSearchVisualTokens.addFilterVisualToken(tokenKey, null, { + uppercaseTokenName: this.filteredSearchTokenKeys.shouldUppercaseTokenName(tokenKey), + capitalizeTokenValue: this.filteredSearchTokenKeys.shouldCapitalizeTokenValue(tokenKey), + }); input.value = input.value.replace(`${tokenKey}:`, ''); } } else { @@ -429,7 +435,10 @@ export default class FilteredSearchManager { const valueCompletedRegex = /([~%@]{0,1}".+")|([~%@]{0,1}'.+')|^((?![~%@]')(?![~%@]")(?!')(?!")).*/g; if (searchToken.match(valueCompletedRegex) && input.value[input.value.length - 1] === ' ') { - FilteredSearchVisualTokens.addFilterVisualToken(searchToken); + const tokenKey = FilteredSearchVisualTokens.getLastTokenPartial(); + FilteredSearchVisualTokens.addFilterVisualToken(searchToken, null, { + capitalizeTokenValue: this.filteredSearchTokenKeys.shouldCapitalizeTokenValue(tokenKey), + }); // Trim the last space as seen in the if statement above input.value = input.value.replace(searchToken, '').trim(); @@ -480,7 +489,7 @@ export default class FilteredSearchManager { FilteredSearchVisualTokens.addFilterVisualToken( condition.tokenKey, condition.value, - canEdit, + { canEdit }, ); } else { // Sanitize value since URL converts spaces into + @@ -506,10 +515,15 @@ export default class FilteredSearchManager { hasFilteredSearch = true; const canEdit = this.canEdit && this.canEdit(sanitizedKey, sanitizedValue); + const { uppercaseTokenName, capitalizeTokenValue } = match; FilteredSearchVisualTokens.addFilterVisualToken( sanitizedKey, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`, - canEdit, + { + canEdit, + uppercaseTokenName, + capitalizeTokenValue, + }, ); } else if (!match && keyParam === 'assignee_id') { const id = parseInt(value, 10); @@ -517,7 +531,7 @@ 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); @@ -525,7 +539,7 @@ 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; @@ -561,15 +575,17 @@ export default class FilteredSearchManager { this.saveCurrentSearchQuery(); - const { tokens, searchToken } - = this.tokenizer.processTokens(searchQuery, this.filteredSearchTokenKeys.getKeys()); + const tokenKeys = this.filteredSearchTokenKeys.getKeys(); + const { tokens, searchToken } = this.tokenizer.processTokens(searchQuery, tokenKeys); const currentState = state || getParameterByName('state') || 'opened'; paths.push(`state=${currentState}`); tokens.forEach((token) => { const condition = this.filteredSearchTokenKeys .searchByConditionKeyValue(token.key, token.value.toLowerCase()); - const { param } = this.filteredSearchTokenKeys.searchByKey(token.key) || {}; + const tokenConfig = this.filteredSearchTokenKeys.searchByKey(token.key) || {}; + const { param } = tokenConfig; + // Replace hyphen with underscore to use as request parameter // e.g. 'my-reaction' => 'my_reaction' const underscoredKey = token.key.replace('-', '_'); @@ -581,6 +597,10 @@ export default class FilteredSearchManager { } else { let tokenValue = token.value; + if (tokenConfig.lowercaseValueOnSubmit) { + tokenValue = tokenValue.toLowerCase(); + } + if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') || (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) { tokenValue = tokenValue.slice(1, tokenValue.length - 1); diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js index 5d131b396a0..a09ad3e4758 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js @@ -23,6 +23,16 @@ export default class FilteredSearchTokenKeys { return this.conditions; } + shouldUppercaseTokenName(tokenKey) { + const token = this.searchByKey(tokenKey.toLowerCase()); + return token && token.uppercaseTokenName; + } + + shouldCapitalizeTokenValue(tokenKey) { + const token = this.searchByKey(tokenKey.toLowerCase()); + return token && token.capitalizeTokenValue; + } + searchByKey(key) { return this.tokenKeys.find(tokenKey => tokenKey.key === key) || null; } @@ -55,4 +65,21 @@ export default class FilteredSearchTokenKeys { return this.conditions .find(condition => condition.tokenKey === key && condition.value === value) || null; } + + addExtraTokensForMergeRequests() { + const wipToken = { + key: 'wip', + type: 'string', + param: '', + symbol: '', + icon: 'admin', + tag: 'Yes or No', + lowercaseValueOnSubmit: true, + uppercaseTokenName: true, + capitalizeTokenValue: true, + }; + + this.tokenKeys.push(wipToken); + this.tokenKeysWithAlternative.push(wipToken); + } } diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index 56fe1ab4e90..0854c1822fb 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -55,12 +55,18 @@ export default class FilteredSearchVisualTokens { } } - static createVisualTokenElementHTML(canEdit = true) { + static createVisualTokenElementHTML(options = {}) { + const { + canEdit = true, + uppercaseTokenName = false, + capitalizeTokenValue = false, + } = options; + return ` <div class="${canEdit ? 'selectable' : 'hidden'}" role="button"> - <div class="name"></div> + <div class="${uppercaseTokenName ? 'text-uppercase' : ''} name"></div> <div class="value-container"> - <div class="value"></div> + <div class="${capitalizeTokenValue ? 'text-capitalize' : ''} value"></div> <div class="remove-token" role="button"> <i class="fa fa-close"></i> </div> @@ -182,16 +188,26 @@ export default class FilteredSearchVisualTokens { } } - static addVisualTokenElement(name, value, isSearchTerm, canEdit) { + static addVisualTokenElement(name, value, options = {}) { + const { + isSearchTerm = false, + canEdit, + uppercaseTokenName, + capitalizeTokenValue, + } = options; const li = document.createElement('li'); li.classList.add('js-visual-token'); li.classList.add(isSearchTerm ? 'filtered-search-term' : 'filtered-search-token'); if (value) { - li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML(canEdit); + li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML({ + canEdit, + uppercaseTokenName, + capitalizeTokenValue, + }); FilteredSearchVisualTokens.renderVisualTokenValue(li, name, value); } else { - li.innerHTML = '<div class="name"></div>'; + li.innerHTML = `<div class="${uppercaseTokenName ? 'text-uppercase' : ''} name"></div>`; } li.querySelector('.name').innerText = name; @@ -212,20 +228,32 @@ export default class FilteredSearchVisualTokens { } } - static addFilterVisualToken(tokenName, tokenValue, canEdit) { + static addFilterVisualToken(tokenName, tokenValue, { + canEdit, + uppercaseTokenName = false, + capitalizeTokenValue = false, + } = {}) { const { lastVisualToken, isLastVisualTokenValid } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); const { addVisualTokenElement } = FilteredSearchVisualTokens; if (isLastVisualTokenValid) { - addVisualTokenElement(tokenName, tokenValue, false, canEdit); + addVisualTokenElement(tokenName, tokenValue, { + canEdit, + uppercaseTokenName, + capitalizeTokenValue, + }); } else { const previousTokenName = lastVisualToken.querySelector('.name').innerText; const tokensContainer = FilteredSearchContainer.container.querySelector('.tokens-container'); tokensContainer.removeChild(lastVisualToken); const value = tokenValue || tokenName; - addVisualTokenElement(previousTokenName, value, false, canEdit); + addVisualTokenElement(previousTokenName, value, { + canEdit, + uppercaseTokenName, + capitalizeTokenValue, + }); } } @@ -235,7 +263,9 @@ export default class FilteredSearchVisualTokens { if (lastVisualToken && lastVisualToken.classList.contains('filtered-search-term')) { lastVisualToken.querySelector('.name').innerText += ` ${searchTerm}`; } else { - FilteredSearchVisualTokens.addVisualTokenElement(searchTerm, null, true); + FilteredSearchVisualTokens.addVisualTokenElement(searchTerm, null, { + isSearchTerm: true, + }); } } @@ -306,7 +336,9 @@ export default class FilteredSearchVisualTokens { let value; if (token.classList.contains('filtered-search-token')) { - FilteredSearchVisualTokens.addFilterVisualToken(nameElement.innerText); + FilteredSearchVisualTokens.addFilterVisualToken(nameElement.innerText, null, { + uppercaseTokenName: nameElement.classList.contains('text-uppercase'), + }); const valueContainerElement = token.querySelector('.value-container'); value = valueContainerElement.dataset.originalValue; diff --git a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue index 3aca38399fb..b0e60edcbe5 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue @@ -3,7 +3,7 @@ import $ from 'jquery'; import { mapActions } from 'vuex'; import { __ } from '~/locale'; import FileIcon from '~/vue_shared/components/file_icon.vue'; -import ChangedFileIcon from '../changed_file_icon.vue'; +import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue'; export default { components: { diff --git a/app/assets/javascripts/ide/components/file_finder/item.vue b/app/assets/javascripts/ide/components/file_finder/item.vue index a612739d641..72ce37be63a 100644 --- a/app/assets/javascripts/ide/components/file_finder/item.vue +++ b/app/assets/javascripts/ide/components/file_finder/item.vue @@ -1,7 +1,7 @@ <script> import fuzzaldrinPlus from 'fuzzaldrin-plus'; import FileIcon from '../../../vue_shared/components/file_icon.vue'; -import ChangedFileIcon from '../changed_file_icon.vue'; +import ChangedFileIcon from '../../../vue_shared/components/changed_file_icon.vue'; const MAX_PATH_LENGTH = 60; diff --git a/app/assets/javascripts/ide/components/file_row_extra.vue b/app/assets/javascripts/ide/components/file_row_extra.vue index 44a360ab909..2ad14b88410 100644 --- a/app/assets/javascripts/ide/components/file_row_extra.vue +++ b/app/assets/javascripts/ide/components/file_row_extra.vue @@ -3,8 +3,8 @@ import { mapGetters } from 'vuex'; import { n__, __, sprintf } from '~/locale'; import tooltip from '~/vue_shared/directives/tooltip'; import Icon from '~/vue_shared/components/icon.vue'; +import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue'; import NewDropdown from './new_dropdown/index.vue'; -import ChangedFileIcon from './changed_file_icon.vue'; import MrFileIcon from './mr_file_icon.vue'; export default { diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue index db47b75ec5c..d621653d6fd 100644 --- a/app/assets/javascripts/ide/components/repo_tab.vue +++ b/app/assets/javascripts/ide/components/repo_tab.vue @@ -3,8 +3,8 @@ import { mapActions } from 'vuex'; import FileIcon from '~/vue_shared/components/file_icon.vue'; import Icon from '~/vue_shared/components/icon.vue'; +import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue'; import FileStatusIcon from './repo_file_status_icon.vue'; -import ChangedFileIcon from './changed_file_icon.vue'; export default { components: { diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js index 0e71e705c13..854445bd2a4 100644 --- a/app/assets/javascripts/job.js +++ b/app/assets/javascripts/job.js @@ -24,7 +24,6 @@ export default class Job extends LogOutputBehaviours { this.$document = $(document); this.$window = $(window); this.logBytes = 0; - this.updateDropdown = this.updateDropdown.bind(this); this.$buildTrace = $('#build-trace'); this.$buildRefreshAnimation = $('.js-build-refresh'); @@ -35,18 +34,12 @@ export default class Job extends LogOutputBehaviours { clearTimeout(this.timeout); this.initSidebar(); - this.populateJobs(this.buildStage); - this.updateStageDropdownText(this.buildStage); this.sidebarOnResize(); this.$document .off('click', '.js-sidebar-build-toggle') .on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this)); - this.$document - .off('click', '.stage-item') - .on('click', '.stage-item', this.updateDropdown); - this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100); this.$window @@ -194,20 +187,4 @@ export default class Job extends LogOutputBehaviours { if (this.shouldHideSidebarForViewport()) this.toggleSidebar(); } - // eslint-disable-next-line class-methods-use-this - populateJobs(stage) { - $('.build-job').hide(); - $(`.build-job[data-stage="${stage}"]`).show(); - } - // eslint-disable-next-line class-methods-use-this - updateStageDropdownText(stage) { - $('.stage-selection').text(stage); - } - - updateDropdown(e) { - e.preventDefault(); - const stage = e.currentTarget.text; - this.updateStageDropdownText(stage); - this.populateJobs(stage); - } } diff --git a/app/assets/javascripts/jobs/components/environments_block.vue b/app/assets/javascripts/jobs/components/environments_block.vue index ca6386595c7..e6e1d418194 100644 --- a/app/assets/javascripts/jobs/components/environments_block.vue +++ b/app/assets/javascripts/jobs/components/environments_block.vue @@ -12,12 +12,16 @@ type: Object, required: true, }, + iconStatus: { + type: Object, + required: true, + }, }, computed: { environment() { let environmentText; switch (this.deploymentStatus.status) { - case 'latest': + case 'last': environmentText = sprintf( __('This job is the most recent deployment to %{link}.'), { link: this.environmentLink }, @@ -32,7 +36,7 @@ ), { environmentLink: this.environmentLink, - deploymentLink: this.deploymentLink, + deploymentLink: this.deploymentLink(`#${this.lastDeployment.iid}`), }, false, ); @@ -56,11 +60,11 @@ if (this.hasLastDeployment) { environmentText = sprintf( __( - 'This job is creating a deployment to %{environmentLink} and will overwrite the last %{deploymentLink}.', + 'This job is creating a deployment to %{environmentLink} and will overwrite the %{deploymentLink}.', ), { environmentLink: this.environmentLink, - deploymentLink: this.deploymentLink, + deploymentLink: this.deploymentLink(__('latest deployment')), }, false, ); @@ -78,41 +82,57 @@ return environmentText; }, environmentLink() { - return sprintf( - '%{startLink}%{name}%{endLink}', - { - startLink: `<a href="${this.deploymentStatus.environment.path}">`, - name: _.escape(this.deploymentStatus.environment.name), - endLink: '</a>', - }, - false, - ); + if (this.hasEnvironment) { + return sprintf( + '%{startLink}%{name}%{endLink}', + { + startLink: `<a href="${ + this.deploymentStatus.environment.environment_path + }" class="js-environment-link">`, + name: _.escape(this.deploymentStatus.environment.name), + endLink: '</a>', + }, + false, + ); + } + return ''; }, - deploymentLink() { + hasLastDeployment() { + return this.hasEnvironment && this.deploymentStatus.environment.last_deployment; + }, + lastDeployment() { + return this.hasLastDeployment ? this.deploymentStatus.environment.last_deployment : {}; + }, + hasEnvironment() { + return !_.isEmpty(this.deploymentStatus.environment); + }, + lastDeploymentPath() { + return !_.isEmpty(this.lastDeployment.deployable) ? this.lastDeployment.deployable.build_path : ''; + }, + }, + methods: { + deploymentLink(name) { return sprintf( '%{startLink}%{name}%{endLink}', { - startLink: `<a href="${this.lastDeployment.path}">`, - name: _.escape(this.lastDeployment.name), + startLink: `<a href="${this.lastDeploymentPath}" class="js-job-deployment-link">`, + name, endLink: '</a>', }, false, ); }, - hasLastDeployment() { - return this.deploymentStatus.environment.last_deployment; - }, - lastDeployment() { - return this.deploymentStatus.environment.last_deployment; - }, }, }; </script> <template> <div class="prepend-top-default js-environment-container"> <div class="environment-information"> - <ci-icon :status="deploymentStatus.icon" /> - <p v-html="environment"></p> + <ci-icon :status="iconStatus"/> + <p + class="inline append-bottom-0" + v-html="environment" + ></p> </div> </div> </template> diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue deleted file mode 100644 index 63324e68d68..00000000000 --- a/app/assets/javascripts/jobs/components/header.vue +++ /dev/null @@ -1,95 +0,0 @@ -<script> -import ciHeader from '../../vue_shared/components/header_ci_component.vue'; -import callout from '../../vue_shared/components/callout.vue'; - -export default { - name: 'JobHeaderSection', - components: { - ciHeader, - callout, - }, - props: { - job: { - type: Object, - required: true, - }, - isLoading: { - type: Boolean, - required: true, - }, - }, - data() { - return { - actions: this.getActions(), - }; - }, - computed: { - status() { - return this.job && this.job.status; - }, - shouldRenderContent() { - return !this.isLoading && Object.keys(this.job).length; - }, - shouldRenderReason() { - return !!(this.job.status && this.job.callout_message); - }, - /** - * When job has not started the key will be `false` - * When job started the key will be a string with a date. - */ - jobStarted() { - return !this.job.started === false; - }, - headerTime() { - return this.jobStarted ? this.job.started : this.job.created_at; - }, - }, - watch: { - job() { - this.actions = this.getActions(); - }, - }, - methods: { - getActions() { - const actions = []; - - if (this.job.new_issue_path) { - actions.push({ - label: 'New issue', - path: this.job.new_issue_path, - cssClass: 'js-new-issue btn btn-success btn-inverted d-none d-md-block d-lg-block d-xl-block', - type: 'link', - }); - } - return actions; - }, - }, -}; -</script> -<template> - <header> - <div class="js-build-header build-header top-area"> - <ci-header - v-if="shouldRenderContent" - :status="status" - :item-id="job.id" - :time="headerTime" - :user="job.user" - :actions="actions" - :has-sidebar-button="true" - :should-render-triggered-label="jobStarted" - item-name="Job" - /> - <gl-loading-icon - v-if="isLoading" - :size="2" - class="prepend-top-default append-bottom-default" - /> - </div> - - <callout - v-if="shouldRenderReason" - :message="job.callout_message" - /> - </header> -</template> diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue new file mode 100644 index 00000000000..bac8bd71d64 --- /dev/null +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -0,0 +1,99 @@ +<script> + import { mapGetters, mapState } from 'vuex'; + import CiHeader from '~/vue_shared/components/header_ci_component.vue'; + import Callout from '~/vue_shared/components/callout.vue'; + import EnvironmentsBlock from './environments_block.vue'; + import ErasedBlock from './erased_block.vue'; + import StuckBlock from './stuck_block.vue'; + + export default { + name: 'JobPageApp', + components: { + CiHeader, + Callout, + EnvironmentsBlock, + ErasedBlock, + StuckBlock, + }, + props: { + runnerHelpUrl: { + type: String, + required: false, + default: null, + }, + }, + computed: { + ...mapState(['isLoading', 'job']), + ...mapGetters([ + 'headerActions', + 'headerTime', + 'shouldRenderCalloutMessage', + 'jobHasStarted', + 'hasEnvironment', + 'isJobStuck', + ]), + }, + }; +</script> +<template> + <div> + <gl-loading-icon + v-if="isLoading" + :size="2" + class="prepend-top-20" + /> + + <template v-else> + <!-- Header Section --> + <header> + <div class="js-build-header build-header top-area"> + <ci-header + :status="job.status" + :item-id="job.id" + :time="headerTime" + :user="job.user" + :actions="headerActions" + :has-sidebar-button="true" + :should-render-triggered-label="jobHasStarted" + :item-name="__('Job')" + /> + </div> + + <callout + v-if="shouldRenderCalloutMessage" + :message="job.callout_message" + /> + </header> + <!-- EO Header Section --> + + <!-- Body Section --> + <stuck-block + v-if="isJobStuck" + class="js-job-stuck" + :has-no-runners-for-project="job.runners.available" + :tags="job.tags" + :runners-path="runnerHelpUrl" + /> + + <environments-block + v-if="hasEnvironment" + :deployment-status="job.deployment_status" + :icon-status="job.status" + /> + + <erased-block + v-if="job.erased" + :user="job.erased_by" + :erased-at="job.erased_at" + /> + + <!--job log --> + <!-- EO job log --> + + <!--empty state --> + <!-- EO empty state --> + + <!-- EO Body Section --> + </template> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/jobs_container.vue b/app/assets/javascripts/jobs/components/jobs_container.vue index 93e2292ff84..271b7790d75 100644 --- a/app/assets/javascripts/jobs/components/jobs_container.vue +++ b/app/assets/javascripts/jobs/components/jobs_container.vue @@ -1,4 +1,5 @@ <script> + import _ from 'underscore'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import Icon from '~/vue_shared/components/icon.vue'; import tooltip from '~/vue_shared/directives/tooltip'; @@ -16,26 +17,39 @@ type: Array, required: true, }, + jobId: { + type: Number, + required: true, + }, + }, + methods: { + isJobActive(currentJobId) { + return this.jobId === currentJobId; + }, + tooltipText(job) { + return `${_.escape(job.name)} - ${job.status.tooltip}`; + }, }, }; </script> <template> - <div class="builds-container"> + <div class="js-jobs-container builds-container"> <div + v-for="job in jobs" + :key="job.id" class="build-job" + :class="{ retried: job.retried, active: isJobActive(job.id) }" > <a - v-for="job in jobs" - :key="job.id" v-tooltip - :href="job.path" - :title="job.tooltip" - :class="{ active: job.active, retried: job.retried }" + :href="job.status.details_path" + :title="tooltipText(job)" + data-container="body" > <icon - v-if="job.active" + v-if="isJobActive(job.id)" name="arrow-right" - class="js-arrow-right" + class="js-arrow-right icon-arrow-right" /> <ci-icon :status="job.status" /> diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue new file mode 100644 index 00000000000..22bcd402e72 --- /dev/null +++ b/app/assets/javascripts/jobs/components/sidebar.vue @@ -0,0 +1,297 @@ +<script> + import _ from 'underscore'; + import { mapActions, mapState } from 'vuex'; + import timeagoMixin from '~/vue_shared/mixins/timeago'; + import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; + import Icon from '~/vue_shared/components/icon.vue'; + import DetailRow from './sidebar_detail_row.vue'; + import ArtifactsBlock from './artifacts_block.vue'; + import TriggerBlock from './trigger_block.vue'; + import CommitBlock from './commit_block.vue'; + import StagesDropdown from './stages_dropdown.vue'; + import JobsContainer from './jobs_container.vue'; + + export default { + name: 'JobSidebar', + components: { + ArtifactsBlock, + CommitBlock, + DetailRow, + Icon, + TriggerBlock, + StagesDropdown, + JobsContainer, + }, + mixins: [timeagoMixin], + props: { + runnerHelpUrl: { + type: String, + required: false, + default: '', + }, + terminalPath: { + type: String, + required: false, + default: null, + }, + }, + computed: { + ...mapState(['job', 'isLoading', 'stages', 'jobs']), + coverage() { + return `${this.job.coverage}%`; + }, + duration() { + return timeIntervalInWords(this.job.duration); + }, + queued() { + return timeIntervalInWords(this.job.queued); + }, + runnerId() { + return `${this.job.runner.description} (#${this.job.runner.id})`; + }, + retryButtonClass() { + let className = + 'js-retry-button float-right btn btn-retry d-none d-md-block d-lg-block d-xl-block'; + className += + this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary'; + return className; + }, + hasTimeout() { + return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null; + }, + timeout() { + if (this.job.metadata == null) { + return ''; + } + + let t = this.job.metadata.timeout_human_readable; + if (this.job.metadata.timeout_source !== '') { + t += ` (from ${this.job.metadata.timeout_source})`; + } + + return t; + }, + renderBlock() { + return ( + this.job.merge_request || + this.job.duration || + this.job.finished_data || + this.job.erased_at || + this.job.queued || + this.job.runner || + this.job.coverage || + this.job.tags.length || + this.job.cancel_path + ); + }, + hasArtifact() { + return !_.isEmpty(this.job.artifact); + }, + hasTriggers() { + return !_.isEmpty(this.job.trigger); + }, + hasStages() { + return ( + (this.job && + this.job.pipeline && + this.job.pipeline.stages && + this.job.pipeline.stages.length > 0) || + false + ); + }, + commit() { + return this.job.pipeline.commit || {}; + }, + }, + methods: { + ...mapActions(['fetchJobsForStage']), + }, + }; +</script> +<template> + <aside + class="right-sidebar right-sidebar-expanded build-sidebar" + data-offset-top="101" + data-spy="affix" + > + <div class="sidebar-container"> + <div class="blocks-container"> + <template v-if="!isLoading"> + <div class="block"> + <strong class="inline prepend-top-8"> + {{ job.name }} + </strong> + <a + v-if="job.retry_path" + :class="retryButtonClass" + :href="job.retry_path" + data-method="post" + rel="nofollow" + > + {{ __('Retry') }} + </a> + <a + v-if="terminalPath" + :href="terminalPath" + class="js-terminal-link pull-right btn btn-primary + btn-inverted visible-md-block visible-lg-block" + target="_blank" + > + {{ __('Debug') }} + <icon name="external-link" /> + </a> + <button + :aria-label="__('Toggle Sidebar')" + type="button" + class="btn btn-blank gutter-toggle + float-right d-block d-md-none js-sidebar-build-toggle" + > + <i + aria-hidden="true" + data-hidden="true" + class="fa fa-angle-double-right" + ></i> + </button> + </div> + <div + v-if="job.retry_path || job.new_issue_path" + class="block retry-link" + > + <a + v-if="job.new_issue_path" + :href="job.new_issue_path" + class="js-new-issue btn btn-success btn-inverted" + > + {{ __('New issue') }} + </a> + <a + v-if="job.retry_path" + :href="job.retry_path" + class="js-retry-job btn btn-inverted-secondary" + data-method="post" + rel="nofollow" + > + {{ __('Retry') }} + </a> + </div> + <div :class="{ block : renderBlock }"> + <p + v-if="job.merge_request" + class="build-detail-row js-job-mr" + > + <span class="build-light-text"> + {{ __('Merge Request:') }} + </span> + <a :href="job.merge_request.path"> + !{{ job.merge_request.iid }} + </a> + </p> + + <detail-row + v-if="job.duration" + :value="duration" + class="js-job-duration" + title="Duration" + /> + <detail-row + v-if="job.finished_at" + :value="timeFormated(job.finished_at)" + class="js-job-finished" + title="Finished" + /> + <detail-row + v-if="job.erased_at" + :value="timeFormated(job.erased_at)" + class="js-job-erased" + title="Erased" + /> + <detail-row + v-if="job.queued" + :value="queued" + class="js-job-queued" + title="Queued" + /> + <detail-row + v-if="hasTimeout" + :help-url="runnerHelpUrl" + :value="timeout" + class="js-job-timeout" + title="Timeout" + /> + <detail-row + v-if="job.runner" + :value="runnerId" + class="js-job-runner" + title="Runner" + /> + <detail-row + v-if="job.coverage" + :value="coverage" + class="js-job-coverage" + title="Coverage" + /> + <p + v-if="job.tags.length" + class="build-detail-row js-job-tags" + > + <span class="build-light-text"> + {{ __('Tags:') }} + </span> + <span + v-for="(tag, i) in job.tags" + :key="i" + class="label label-primary"> + {{ tag }} + </span> + </p> + + <div + v-if="job.cancel_path" + class="btn-group prepend-top-5" + role="group"> + <a + :href="job.cancel_path" + class="js-cancel-job btn btn-sm btn-default" + data-method="post" + rel="nofollow" + > + {{ __('Cancel') }} + </a> + </div> + </div> + <artifacts-block + v-if="hasArtifact" + :artifact="job.artifact" + /> + <trigger-block + v-if="hasTriggers" + :trigger="job.trigger" + /> + <commit-block + :is-last-block="hasStages" + :commit="commit" + :merge-request="job.merge_request" + /> + + <stages-dropdown + :stages="stages" + :pipeline="job.pipeline" + @requestSidebarStageDropdown="fetchJobsForStage" + /> + + </template> + <gl-loading-icon + v-else + :size="2" + class="prepend-top-10" + /> + </div> + + <jobs-container + v-if="!isLoading && jobs.length" + :jobs="jobs" + :job-id="job.id" + /> + </div> + </aside> +</template> diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue deleted file mode 100644 index a591fcfb482..00000000000 --- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue +++ /dev/null @@ -1,276 +0,0 @@ -<script> - import _ from 'underscore'; - import timeagoMixin from '~/vue_shared/mixins/timeago'; - import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; - import Icon from '~/vue_shared/components/icon.vue'; - import DetailRow from './sidebar_detail_row.vue'; - import ArtifactsBlock from './artifacts_block.vue'; - import TriggerBlock from './trigger_block.vue'; - import CommitBlock from './commit_block.vue'; - - export default { - name: 'SidebarDetailsBlock', - components: { - ArtifactsBlock, - CommitBlock, - DetailRow, - Icon, - TriggerBlock, - }, - mixins: [timeagoMixin], - props: { - job: { - type: Object, - required: true, - }, - isLoading: { - type: Boolean, - required: true, - }, - runnerHelpUrl: { - type: String, - required: false, - default: '', - }, - terminalPath: { - type: String, - required: false, - default: null, - }, - }, - computed: { - shouldRenderContent() { - return !this.isLoading && Object.keys(this.job).length > 0; - }, - coverage() { - return `${this.job.coverage}%`; - }, - duration() { - return timeIntervalInWords(this.job.duration); - }, - queued() { - return timeIntervalInWords(this.job.queued); - }, - runnerId() { - return `${this.job.runner.description} (#${this.job.runner.id})`; - }, - retryButtonClass() { - let className = - 'js-retry-button float-right btn btn-retry d-none d-md-block d-lg-block d-xl-block'; - className += - this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary'; - return className; - }, - hasTimeout() { - return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null; - }, - timeout() { - if (this.job.metadata == null) { - return ''; - } - - let t = this.job.metadata.timeout_human_readable; - if (this.job.metadata.timeout_source !== '') { - t += ` (from ${this.job.metadata.timeout_source})`; - } - - return t; - }, - renderBlock() { - return ( - this.job.merge_request || - this.job.duration || - this.job.finished_data || - this.job.erased_at || - this.job.queued || - this.job.runner || - this.job.coverage || - this.job.tags.length || - this.job.cancel_path - ); - }, - hasArtifact() { - return !_.isEmpty(this.job.artifact); - }, - hasTriggers() { - return !_.isEmpty(this.job.trigger); - }, - hasStages() { - return ( - this.job && - this.job.pipeline && - this.job.pipeline.stages && - this.job.pipeline.stages.length > 0 - ) || false; - }, - commit() { - return this.job.pipeline.commit || {}; - }, - }, - }; -</script> -<template> - <div> - <div class="block"> - <strong class="inline prepend-top-8"> - {{ job.name }} - </strong> - <a - v-if="job.retry_path" - :class="retryButtonClass" - :href="job.retry_path" - data-method="post" - rel="nofollow" - > - {{ __('Retry') }} - </a> - <a - v-if="terminalPath" - :href="terminalPath" - class="js-terminal-link pull-right btn btn-primary - btn-inverted visible-md-block visible-lg-block" - target="_blank" - > - {{ __('Debug') }} - <icon name="external-link" /> - </a> - <button - :aria-label="__('Toggle Sidebar')" - type="button" - class="btn btn-blank gutter-toggle float-right d-block d-md-none js-sidebar-build-toggle" - > - <i - aria-hidden="true" - data-hidden="true" - class="fa fa-angle-double-right" - ></i> - </button> - </div> - <template v-if="shouldRenderContent"> - <div - v-if="job.retry_path || job.new_issue_path" - class="block retry-link" - > - <a - v-if="job.new_issue_path" - :href="job.new_issue_path" - class="js-new-issue btn btn-success btn-inverted" - > - {{ __('New issue') }} - </a> - <a - v-if="job.retry_path" - :href="job.retry_path" - class="js-retry-job btn btn-inverted-secondary" - data-method="post" - rel="nofollow" - > - {{ __('Retry') }} - </a> - </div> - <div :class="{block : renderBlock }"> - <p - v-if="job.merge_request" - class="build-detail-row js-job-mr" - > - <span class="build-light-text"> - {{ __('Merge Request:') }} - </span> - <a :href="job.merge_request.path"> - !{{ job.merge_request.iid }} - </a> - </p> - - <detail-row - v-if="job.duration" - :value="duration" - class="js-job-duration" - title="Duration" - /> - <detail-row - v-if="job.finished_at" - :value="timeFormated(job.finished_at)" - class="js-job-finished" - title="Finished" - /> - <detail-row - v-if="job.erased_at" - :value="timeFormated(job.erased_at)" - class="js-job-erased" - title="Erased" - /> - <detail-row - v-if="job.queued" - :value="queued" - class="js-job-queued" - title="Queued" - /> - <detail-row - v-if="hasTimeout" - :help-url="runnerHelpUrl" - :value="timeout" - class="js-job-timeout" - title="Timeout" - /> - <detail-row - v-if="job.runner" - :value="runnerId" - class="js-job-runner" - title="Runner" - /> - <detail-row - v-if="job.coverage" - :value="coverage" - class="js-job-coverage" - title="Coverage" - /> - <p - v-if="job.tags.length" - class="build-detail-row js-job-tags" - > - <span class="build-light-text"> - {{ __('Tags:') }} - </span> - <span - v-for="(tag, i) in job.tags" - :key="i" - class="label label-primary"> - {{ tag }} - </span> - </p> - - <div - v-if="job.cancel_path" - class="btn-group prepend-top-5" - role="group"> - <a - :href="job.cancel_path" - class="js-cancel-job btn btn-sm btn-default" - data-method="post" - rel="nofollow" - > - {{ __('Cancel') }} - </a> - </div> - </div> - <artifacts-block - v-if="hasArtifact" - :artifact="job.artifact" - /> - <trigger-block - v-if="hasTriggers" - :trigger="job.trigger" - /> - <commit-block - :is-last-block="hasStages" - :commit="commit" - :merge-request="job.merge_request" - /> - </template> - <gl-loading-icon - v-if="isLoading" - :size="2" - class="prepend-top-10" - /> - </div> -</template> diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue index d6d64fa32f7..1c15af55a8b 100644 --- a/app/assets/javascripts/jobs/components/stages_dropdown.vue +++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue @@ -1,8 +1,8 @@ <script> + import _ from 'underscore'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import Icon from '~/vue_shared/components/icon.vue'; - - import { sprintf, __ } from '~/locale'; + import { __ } from '~/locale'; export default { components: { @@ -10,30 +10,14 @@ Icon, }, props: { - pipelineId: { - type: Number, - required: true, - }, - pipelinePath: { - type: String, - required: true, - }, - pipelineRef: { - type: String, - required: true, - }, - pipelineRefPath: { - type: String, + pipeline: { + type: Object, required: true, }, stages: { type: Array, required: true, }, - pipelineStatus: { - type: Object, - required: true, - }, }, data() { return { @@ -41,57 +25,73 @@ }; }, computed: { - pipelineLink() { - return sprintf(__('Pipeline %{pipelineLinkStart} #%{pipelineId} %{pipelineLinkEnd} from %{pipelineLinkRefStart} %{pipelineRef} %{pipelineLinkRefEnd}'), { - pipelineLinkStart: `<a href=${this.pipelinePath} class="js-pipeline-path link-commit">`, - pipelineId: this.pipelineId, - pipelineLinkEnd: '</a>', - pipelineLinkRefStart: `<a href=${this.pipelineRefPath} class="link-commit ref-name">`, - pipelineRef: this.pipelineRef, - pipelineLinkRefEnd: '</a>', - }, false); + hasRef() { + return !_.isEmpty(this.pipeline.ref); + }, + }, + watch: { + // When the component is initially mounted it may start with an empty stages array. + // Once the prop is updated, we set the first stage as the selected one + stages(newVal) { + if (newVal.length) { + this.selectedStage = newVal[0].name; + } }, }, methods: { onStageClick(stage) { - // todo: consider moving into store - this.selectedStage = stage.name; - - // update dropdown with jobs - // jobs container is a new component. this.$emit('requestSidebarStageDropdown', stage); + this.selectedStage = stage.name; }, }, }; </script> <template> - <div class="block-last"> - <ci-icon :status="pipelineStatus" /> + <div class="block-last dropdown"> + <ci-icon + :status="pipeline.details.status" + class="vertical-align-middle" + /> + + {{ __('Pipeline') }} + <a + :href="pipeline.path" + class="js-pipeline-path link-commit" + > + #{{ pipeline.id }} + </a> + <template v-if="hasRef"> + {{ __('from') }} + <a + :href="pipeline.ref.path" + class="link-commit ref-name" + > + {{ pipeline.ref.name }} + </a> + </template> - <p v-html="pipelineLink"></p> + <button + type="button" + data-toggle="dropdown" + class="js-selected-stage dropdown-menu-toggle prepend-top-8" + > + {{ selectedStage }} + <i class="fa fa-chevron-down" ></i> + </button> - <div class="dropdown"> - <button - type="button" - data-toggle="dropdown" + <ul class="dropdown-menu"> + <li + v-for="stage in stages" + :key="stage.name" > - {{ selectedStage }} - <icon name="chevron-down" /> - </button> - <ul class="dropdown-menu"> - <li - v-for="(stage, index) in stages" - :key="index" + <button + type="button" + class="js-stage-item stage-item" + @click="onStageClick(stage)" > - <button - type="button" - class="stage-item" - @click="onStageClick(stage)" - > - {{ stage.name }} - </button> - </li> - </ul> - </div> + {{ stage.name }} + </button> + </li> + </ul> </div> </template> diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js index 0136ec4d194..3eb75e72506 100644 --- a/app/assets/javascripts/jobs/job_details_bundle.js +++ b/app/assets/javascripts/jobs/job_details_bundle.js @@ -1,8 +1,9 @@ -import { mapState } from 'vuex'; +import _ from 'underscore'; +import { mapState, mapActions } from 'vuex'; import Vue from 'vue'; import Job from '../job'; -import JobHeader from './components/header.vue'; -import DetailsBlock from './components/sidebar_details_block.vue'; +import JobApp from './components/job_app.vue'; +import Sidebar from './components/sidebar.vue'; import createStore from './store'; export default () => { @@ -13,6 +14,7 @@ export default () => { const store = createStore(); store.dispatch('setJobEndpoint', dataset.endpoint); + store.dispatch('fetchJob'); // Header @@ -20,17 +22,18 @@ export default () => { new Vue({ el: '#js-build-header-vue', components: { - JobHeader, + JobApp, }, store, computed: { ...mapState(['job', 'isLoading']), }, render(createElement) { - return createElement('job-header', { + return createElement('job-app', { props: { isLoading: this.isLoading, job: this.job, + runnerHelpUrl: dataset.runnerHelpUrl, }, }); }, @@ -43,17 +46,25 @@ export default () => { new Vue({ el: detailsBlockElement, components: { - DetailsBlock, + Sidebar, }, - store, computed: { - ...mapState(['job', 'isLoading']), + ...mapState(['job']), + }, + watch: { + job(newVal, oldVal) { + if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) { + this.fetchStages(); + } + }, }, + methods: { + ...mapActions(['fetchStages']), + }, + store, render(createElement) { - return createElement('details-block', { + return createElement('sidebar', { props: { - isLoading: this.isLoading, - job: this.job, runnerHelpUrl: dataset.runnerHelpUrl, terminalPath: detailsBlockDataset.terminalPath, }, diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js index 7f5406d6f43..298367c9342 100644 --- a/app/assets/javascripts/jobs/store/actions.js +++ b/app/assets/javascripts/jobs/store/actions.js @@ -62,7 +62,9 @@ export const fetchJob = ({ state, dispatch }) => { }); }; -export const receiveJobSuccess = ({ commit }, data) => commit(types.RECEIVE_JOB_SUCCESS, data); +export const receiveJobSuccess = ({ commit }, data) => { + commit(types.RECEIVE_JOB_SUCCESS, data); +}; export const receiveJobError = ({ commit }) => { commit(types.RECEIVE_JOB_ERROR); flash(__('An error occurred while fetching the job.')); @@ -137,8 +139,11 @@ export const fetchStages = ({ state, dispatch }) => { dispatch('requestStages'); axios - .get(state.stagesEndpoint) - .then(({ data }) => dispatch('receiveStagesSuccess', data)) + .get(state.job.pipeline.path) + .then(({ data }) => { + dispatch('receiveStagesSuccess', data.details.stages); + dispatch('fetchJobsForStage', data.details.stages[0]); + }) .catch(() => dispatch('receiveStagesError')); }; export const receiveStagesSuccess = ({ commit }, data) => @@ -152,16 +157,23 @@ export const receiveStagesError = ({ commit }) => { * Jobs list on sidebar - depend on stages dropdown */ export const requestJobsForStage = ({ commit }) => commit(types.REQUEST_JOBS_FOR_STAGE); -export const setSelectedStage = ({ commit }, stage) => commit(types.SET_SELECTED_STAGE, stage); // On stage click, set selected stage + fetch job -export const fetchJobsForStage = ({ state, dispatch }, stage) => { - dispatch('setSelectedStage', stage); +export const fetchJobsForStage = ({ dispatch }, stage) => { dispatch('requestJobsForStage'); axios - .get(state.stageJobsEndpoint) - .then(({ data }) => dispatch('receiveJobsForStageSuccess', data)) + .get(stage.dropdown_path, { + params: { + retried: 1, + }, + }) + .then(({ data }) => { + const retriedJobs = data.retried.map(job => Object.assign({}, job, { retried: true })); + const jobs = data.latest_statuses.concat(retriedJobs); + + dispatch('receiveJobsForStageSuccess', jobs); + }) .catch(() => dispatch('receiveJobsForStageError')); }; export const receiveJobsForStageSuccess = ({ commit }, data) => diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js new file mode 100644 index 00000000000..62d154ff584 --- /dev/null +++ b/app/assets/javascripts/jobs/store/getters.js @@ -0,0 +1,42 @@ +import _ from 'underscore'; +import { __ } from '~/locale'; + +export const headerActions = state => { + if (state.job.new_issue_path) { + return [ + { + label: __('New issue'), + path: state.job.new_issue_path, + cssClass: + 'js-new-issue btn btn-success btn-inverted d-none d-md-block d-lg-block d-xl-block', + type: 'link', + }, + ]; + } + return []; +}; + +export const headerTime = state => (state.job.started ? state.job.started : state.job.created_at); + +export const shouldRenderCalloutMessage = state => + !_.isEmpty(state.job.status) && !_.isEmpty(state.job.callout_message); + +/** + * When job has not started the key will be `false` + * When job started the key will be a string with a date. + */ +export const jobHasStarted = state => !(state.job.started === false); + +export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status); + +/** + * When the job is pending and there are no available runners + * we need to render the stuck block; + * + * @returns {Boolean} + */ +export const isJobStuck = state => + state.job.status.group === 'pending' && state.job.runners && state.job.runners.available === false; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/jobs/store/index.js b/app/assets/javascripts/jobs/store/index.js index d8f6f56ce61..96e38f9a2fa 100644 --- a/app/assets/javascripts/jobs/store/index.js +++ b/app/assets/javascripts/jobs/store/index.js @@ -2,6 +2,7 @@ import Vue from 'vue'; import Vuex from 'vuex'; import state from './state'; import * as actions from './actions'; +import * as getters from './getters'; import mutations from './mutations'; Vue.use(Vuex); @@ -9,5 +10,6 @@ Vue.use(Vuex); export default () => new Vuex.Store({ actions, mutations, + getters, state: state(), }); diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 31faa11ea72..e14fff7a610 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -88,6 +88,7 @@ export const handleLocationHash = () => { const fixedDiffStats = document.querySelector('.js-diff-files-changed'); const fixedNav = document.querySelector('.navbar-gitlab'); const performanceBar = document.querySelector('#js-peek'); + const topPadding = 8; let adjustment = 0; if (fixedNav) adjustment -= fixedNav.offsetHeight; @@ -108,6 +109,10 @@ export const handleLocationHash = () => { adjustment -= performanceBar.offsetHeight; } + if (isInMRPage()) { + adjustment -= topPadding; + } + window.scrollBy(0, adjustment); }; @@ -381,8 +386,11 @@ export const objectToQueryString = (params = {}) => .map(param => `${param}=${params[param]}`) .join('&'); -export const buildUrlWithCurrentLocation = param => - (param ? `${window.location.pathname}${param}` : window.location.pathname); +export const buildUrlWithCurrentLocation = param => { + if (param) return `${window.location.pathname}${param}`; + + return window.location.pathname; +}; /** * Based on the current location and the string parameters provided diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 763429d7242..78f56ab57ff 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -194,9 +194,7 @@ export default class MergeRequestTabs { if (bp.getBreakpointSize() !== 'lg') { this.shrinkView(); } - if (this.diffViewType() === 'parallel') { - this.expandViewContainer(); - } + this.expandViewContainer(); this.destroyPipelinesView(); this.commitsTab.classList.remove('active'); } else if (action === 'pipelines') { @@ -355,7 +353,7 @@ export default class MergeRequestTabs { localTimeAgo($('.js-timeago', 'div#diffs')); syntaxHighlight($('#diffs .js-syntax-highlight')); - if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) { + if (this.isDiffAction(this.currentAction)) { this.expandViewContainer(); } this.diffsLoaded = true; @@ -408,19 +406,23 @@ export default class MergeRequestTabs { } diffViewType() { - return $('.inline-parallel-buttons a.active').data('viewType'); + return $('.inline-parallel-buttons button.active').data('viewType'); } isDiffAction(action) { return action === 'diffs' || action === 'new/diffs'; } - expandViewContainer() { + expandViewContainer(removeLimited = true) { const $wrapper = $('.content-wrapper .container-fluid').not('.breadcrumbs'); if (this.fixedLayoutPref === null) { this.fixedLayoutPref = $wrapper.hasClass('container-limited'); } - $wrapper.removeClass('container-limited'); + if (this.diffViewType() === 'parallel' || removeLimited) { + $wrapper.removeClass('container-limited'); + } else { + $wrapper.addClass('container-limited'); + } } resetViewContainer() { diff --git a/app/assets/javascripts/notes/components/diff_file_header.vue b/app/assets/javascripts/notes/components/diff_file_header.vue deleted file mode 100644 index 4fd93304a03..00000000000 --- a/app/assets/javascripts/notes/components/diff_file_header.vue +++ /dev/null @@ -1,94 +0,0 @@ -<script> -import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; -import Icon from '~/vue_shared/components/icon.vue'; - -export default { - components: { - ClipboardButton, - Icon, - }, - props: { - diffFile: { - type: Object, - required: true, - }, - }, - computed: { - titleTag() { - return this.diffFile.discussionPath ? 'a' : 'span'; - }, - }, -}; -</script> - -<template> - <div class="file-header-content"> - <div - v-if="diffFile.submodule" - > - <span> - <icon name="archive" /> - <strong - class="file-title-name" - v-html="diffFile.submoduleLink" - ></strong> - <clipboard-button - :text="diffFile.submoduleLink" - title="Copy file path to clipboard" - css-class="btn-default btn-transparent btn-clipboard" - /> - </span> - </div> - <template v-else> - <component - :is="titleTag" - ref="titleWrapper" - :href="diffFile.discussionPath" - > - <span v-html="diffFile.blobIcon"></span> - <span v-if="diffFile.renamedFile"> - <strong - :title="diffFile.oldPath" - class="file-title-name has-tooltip" - data-container="body" - > - {{ diffFile.oldPath }} - </strong> - → - <strong - :title="diffFile.newPath" - class="file-title-name has-tooltip" - data-container="body" - > - {{ diffFile.newPath }} - </strong> - </span> - - <strong - v-else - :title="diffFile.oldPath" - class="file-title-name has-tooltip" - data-container="body" - > - {{ diffFile.filePath }} - <span v-if="diffFile.deletedFile"> - deleted - </span> - </strong> - </component> - - <clipboard-button - :text="diffFile.filePath" - title="Copy file path to clipboard" - css-class="btn-default btn-transparent btn-clipboard" - /> - - <small - v-if="diffFile.modeChanged" - ref="fileMode" - > - {{ diffFile.aMode }} → {{ diffFile.bMode }} - </small> - </template> - </div> -</template> diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 6ede7562edf..e9218723149 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -191,6 +191,7 @@ export default { if (note.placeholderType === SYSTEM_NOTE) { return placeholderSystemNote; } + return placeholderNote; } @@ -201,7 +202,7 @@ export default { return noteableNote; }, componentData(note) { - return note.isPlaceholderNote ? this.discussion.notes[0] : note; + return note.isPlaceholderNote ? note.notes[0] : note; }, toggleDiscussionHandler() { this.toggleDiscussion({ discussionId: this.discussion.id }); diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js index b798a254459..339ce67438a 100644 --- a/app/assets/javascripts/pages/groups/merge_requests/index.js +++ b/app/assets/javascripts/pages/groups/merge_requests/index.js @@ -4,6 +4,8 @@ import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered import { FILTERED_SEARCH } from '~/pages/constants'; document.addEventListener('DOMContentLoaded', () => { + IssuableFilteredSearchTokenKeys.addExtraTokensForMergeRequests(); + initFilteredSearch({ page: FILTERED_SEARCH.MERGE_REQUESTS, isGroupDecendent: true, diff --git a/app/assets/javascripts/pages/projects/merge_requests/index/index.js b/app/assets/javascripts/pages/projects/merge_requests/index/index.js index 3647048a872..ec39db12e74 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/index/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js @@ -7,10 +7,13 @@ import { FILTERED_SEARCH } from '~/pages/constants'; import { ISSUABLE_INDEX } from '~/pages/projects/constants'; document.addEventListener('DOMContentLoaded', () => { + IssuableFilteredSearchTokenKeys.addExtraTokensForMergeRequests(); + initFilteredSearch({ page: FILTERED_SEARCH.MERGE_REQUESTS, filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys, }); + new IssuableIndex(ISSUABLE_INDEX.MERGE_REQUEST); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new new UsersSelect(); // eslint-disable-line no-new 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 2e1d6e9643a..8660b0546cf 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 @@ -51,10 +51,10 @@ export default { <template> <div class="block"> <issuable-time-tracker - :time_estimate="store.timeEstimate" - :time_spent="store.totalTimeSpent" - :human_time_estimate="store.humanTimeEstimate" - :human_time_spent="store.humanTotalTimeSpent" + :time-estimate="store.timeEstimate" + :time-spent="store.totalTimeSpent" + :human-time-estimate="store.humanTimeEstimate" + :human-time-spent="store.humanTotalTimeSpent" :root-path="store.rootPath" /> </div> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue index 2ee3e1f322e..ef76dc13ce9 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue @@ -19,24 +19,20 @@ export default { TimeTrackingHelpState, }, props: { - // eslint-disable-next-line vue/prop-name-casing - time_estimate: { + timeEstimate: { type: Number, required: true, }, - // eslint-disable-next-line vue/prop-name-casing - time_spent: { + timeSpent: { type: Number, required: true, }, - // eslint-disable-next-line vue/prop-name-casing - human_time_estimate: { + humanTimeEstimate: { type: String, required: false, default: '', }, - // eslint-disable-next-line vue/prop-name-casing - human_time_spent: { + humanTimeSpent: { type: String, required: false, default: '', @@ -52,18 +48,6 @@ export default { }; }, computed: { - timeSpent() { - return this.time_spent; - }, - timeEstimate() { - return this.time_estimate; - }, - timeEstimateHumanReadable() { - return this.human_time_estimate; - }, - timeSpentHumanReadable() { - return this.human_time_spent; - }, hasTimeSpent() { return !!this.timeSpent; }, @@ -94,10 +78,12 @@ export default { this.showHelp = show; }, update(data) { - this.time_estimate = data.time_estimate; - this.time_spent = data.time_spent; - this.human_time_estimate = data.human_time_estimate; - this.human_time_spent = data.human_time_spent; + const { timeEstimate, timeSpent, humanTimeEstimate, humanTimeSpent } = data; + + this.timeEstimate = timeEstimate; + this.timeSpent = timeSpent; + this.humanTimeEstimate = humanTimeEstimate; + this.humanTimeSpent = humanTimeSpent; }, }, }; @@ -114,8 +100,8 @@ export default { :show-help-state="showHelpState" :show-spent-only-state="showSpentOnlyState" :show-estimate-only-state="showEstimateOnlyState" - :time-spent-human-readable="timeSpentHumanReadable" - :time-estimate-human-readable="timeEstimateHumanReadable" + :time-spent-human-readable="humanTimeSpent" + :time-estimate-human-readable="humanTimeEstimate" /> <div class="title hide-collapsed"> {{ __('Time tracking') }} @@ -145,11 +131,11 @@ export default { <div class="time-tracking-content hide-collapsed"> <time-tracking-estimate-only-pane v-if="showEstimateOnlyState" - :time-estimate-human-readable="timeEstimateHumanReadable" + :time-estimate-human-readable="humanTimeEstimate" /> <time-tracking-spent-only-pane v-if="showSpentOnlyState" - :time-spent-human-readable="timeSpentHumanReadable" + :time-spent-human-readable="humanTimeSpent" /> <time-tracking-no-tracking-pane v-if="showNoTimeTrackingState" @@ -158,8 +144,8 @@ export default { v-if="showComparisonState" :time-estimate="timeEstimate" :time-spent="timeSpent" - :time-spent-human-readable="timeSpentHumanReadable" - :time-estimate-human-readable="timeEstimateHumanReadable" + :time-spent-human-readable="humanTimeSpent" + :time-estimate-human-readable="humanTimeEstimate" /> <transition name="help-state-toggle"> <time-tracking-help-state diff --git a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js index b15ad0e5586..87da65a1b1f 100644 --- a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js @@ -7,6 +7,8 @@ export default class SidebarMilestone { if (!el) return; + const { timeEstimate, timeSpent, humanTimeEstimate, humanTimeSpent } = el.dataset; + // eslint-disable-next-line no-new new Vue({ el, @@ -15,10 +17,10 @@ export default class SidebarMilestone { }, render: createElement => createElement('timeTracker', { props: { - time_estimate: parseInt(el.dataset.timeEstimate, 10), - time_spent: parseInt(el.dataset.timeSpent, 10), - human_time_estimate: el.dataset.humanTimeEstimate, - human_time_spent: el.dataset.humanTimeSpent, + timeEstimate: parseInt(timeEstimate, 10), + timeSpent: parseInt(timeSpent, 10), + humanTimeEstimate, + humanTimeSpent, rootPath: '/', }, }), diff --git a/app/assets/javascripts/ide/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue index 720ae11aaa6..8684005e0fb 100644 --- a/app/assets/javascripts/ide/components/changed_file_icon.vue +++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue @@ -3,7 +3,7 @@ import tooltip from '~/vue_shared/directives/tooltip'; import Icon from '~/vue_shared/components/icon.vue'; import { pluralize } from '~/lib/utils/text_utility'; import { __, sprintf } from '~/locale'; -import { getCommitIconMap } from '../utils'; +import { getCommitIconMap } from '~/ide/utils'; export default { components: { @@ -32,6 +32,11 @@ export default { required: false, default: false, }, + size: { + type: Number, + required: false, + default: 12, + }, }, computed: { changedIcon() { @@ -42,7 +47,7 @@ export default { return `${getCommitIconMap(this.file).icon}${suffix}`; }, changedIconClass() { - return `ide-${this.changedIcon} float-left`; + return `${this.changedIcon} float-left d-block`; }, tooltipTitle() { if (!this.showTooltip) return undefined; @@ -78,13 +83,30 @@ export default { :title="tooltipTitle" data-container="body" data-placement="right" - class="ide-file-changed-icon" + class="file-changed-icon ml-auto" > <icon v-if="showIcon" :name="changedIcon" - :size="12" + :size="size" :css-classes="changedIconClass" /> </span> </template> + +<style> +.file-addition, +.file-addition-solid { + color: #1aaa55; +} + +.file-modified, +.file-modified-solid { + color: #fc9403; +} + +.file-deletion, +.file-deletion-solid { + color: #db3b21; +} +</style> diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue index c797ad62a5d..36a345130c0 100644 --- a/app/assets/javascripts/vue_shared/components/file_row.vue +++ b/app/assets/javascripts/vue_shared/components/file_row.vue @@ -1,12 +1,14 @@ <script> import Icon from '~/vue_shared/components/icon.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue'; +import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue'; export default { name: 'FileRow', components: { FileIcon, Icon, + ChangedFileIcon, }, props: { file: { @@ -22,6 +24,16 @@ export default { required: false, default: null, }, + hideExtraOnTree: { + type: Boolean, + required: false, + default: false, + }, + showChangedIcon: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -65,6 +77,9 @@ export default { toggleTreeOpen(path) { this.$emit('toggleTreeOpen', path); }, + clickedFile(path) { + this.$emit('clickFile', path); + }, clickFile() { // Manual Action if a tree is selected/opened if (this.isTree && this.hasUrlAtCurrentRoute()) { @@ -72,6 +87,8 @@ export default { } if (this.$router) this.$router.push(`/project${this.file.url}`); + + if (this.isBlob) this.clickedFile(this.file.path); }, scrollIntoView(isInit = false) { const block = isInit && this.isTree ? 'center' : 'nearest'; @@ -126,17 +143,24 @@ export default { class="file-row-name str-truncated" > <file-icon + v-if="!showChangedIcon || file.type === 'tree'" :file-name="file.name" :loading="file.loading" :folder="isTree" :opened="file.opened" :size="16" /> + <changed-file-icon + v-else + :file="file" + :size="16" + class="append-right-5" + /> {{ file.name }} </span> <component :is="extraComponent" - v-if="extraComponent" + v-if="extraComponent && !(hideExtraOnTree && file.type === 'tree')" :file="file" :mouse-over="mouseOver" /> @@ -148,8 +172,11 @@ export default { :key="childFile.key" :file="childFile" :level="level + 1" + :hide-extra-on-tree="hideExtraOnTree" :extra-component="extraComponent" + :show-changed-icon="showChangedIcon" @toggleTreeOpen="toggleTreeOpen" + @clickFile="clickedFile" /> </template> </div> diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index 65f0a0d18e2..07d82e984ba 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -517,21 +517,6 @@ $ide-commit-header-height: 48px; } } -.ide-file-addition, -.ide-file-addition-solid { - color: $green-500; -} - -.ide-file-modified, -.ide-file-modified-solid { - color: $orange-500; -} - -.ide-file-deletion, -.ide-file-deletion-solid { - color: $red-500; -} - .multi-file-commit-list-collapsed { display: flex; flex-direction: column; @@ -1399,14 +1384,6 @@ $ide-commit-header-height: 48px; color: $theme-gray-700; } -.ide-file-changed-icon { - margin-left: auto; - - > svg { - display: block; - } -} - .file-row:hover, .file-row:focus { .ide-new-btn { diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 14ba8b1df83..ed877f625b5 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -328,23 +328,6 @@ } } - .build-dropdown { - margin: $gl-padding 0; - padding: 0; - - .dropdown-menu-toggle { - margin-top: #{$gl-padding / 2}; - } - - svg { - position: relative; - top: 3px; - margin-right: 3px; - width: 14px; - height: 14px; - } - } - .builds-container { background-color: $white-light; border-top: 1px solid $border-color; @@ -381,15 +364,11 @@ position: absolute; left: 15px; top: 20px; - display: none; + display: block; } &.active { font-weight: $gl-font-weight-bold; - - .icon-arrow-right { - display: block; - } } &.retried { diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 10764e0f3df..628a4ca38da 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -223,6 +223,7 @@ } } +.clipboard-group, .commit-sha-group { display: inline-flex; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 987dcd32e3a..5035714b95f 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -571,8 +571,6 @@ } .files { - margin-top: 1px; - .diff-file:last-child { margin-bottom: 0; } @@ -987,3 +985,63 @@ .discussion-body .image .frame { position: relative; } + +.diff-tree-list { + width: 320px; +} + +.diff-files-holder { + flex: 1; + min-width: 0; +} + +.compare-versions-container { + min-width: 0; +} + +.tree-list-holder { + position: sticky; + top: 100px; + max-height: calc(100vh - 100px); + padding-right: $gl-padding; + + .file-row { + margin-left: 0; + margin-right: 0; + } + + .with-performance-bar & { + top: 135px; + } +} + +.tree-list-scroll { + max-height: 100%; + padding-top: $grid-size; + padding-bottom: $grid-size; + border-top: 1px solid $border-color; + border-bottom: 1px solid $border-color; + overflow-y: scroll; + overflow-x: auto; +} + +.tree-list-search .form-control { + padding-left: 30px; +} + +.tree-list-icon { + top: 50%; + left: 10px; + transform: translateY(-50%); + + &, + svg { + fill: $gl-text-color-tertiary; + } +} + +.tree-list-clear-icon { + right: 10px; + left: auto; + line-height: 0; +} diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 97b131687d3..45382d4ea43 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -723,6 +723,17 @@ align-items: center; padding: 16px; z-index: 199; + white-space: nowrap; + + .dropdown-menu-toggle { + width: auto; + max-width: 170px; + + svg { + top: 10px; + right: 8px; + } + } } .content-block { diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 0209a1397b9..9e24154e4b6 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -128,7 +128,7 @@ class IssuableFinder labels_count = 1 if use_cte_for_search? finder.execute.reorder(nil).group(:state).count.each do |key, value| - counts[Array(key).last.to_sym] += value / labels_count + counts[count_key(key)] += value / labels_count end counts[:all] = counts.values.sum @@ -297,6 +297,10 @@ class IssuableFinder klass.all end + def count_key(value) + Array(value).last.to_sym + end + # rubocop: disable CodeReuse/ActiveRecord def by_scope(items) return items.none if current_user_related? && !current_user diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index b698a3c7b09..50c051c3aa1 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -27,13 +27,17 @@ # updated_before: datetime # class MergeRequestsFinder < IssuableFinder + def self.scalar_params + @scalar_params ||= super + [:wip] + end + def klass MergeRequest end def filter_items(_items) items = by_source_branch(super) - + items = by_wip(items) by_target_branch(items) end @@ -61,5 +65,24 @@ class MergeRequestsFinder < IssuableFinder items.where(target_branch: target_branch) end - # rubocop: enable CodeReuse/ActiveRecord + + def item_project_ids(items) + items&.reorder(nil)&.select(:target_project_id) + end + + def by_wip(items) + if params[:wip] == 'yes' + items.where(wip_match(items.arel_table)) + elsif params[:wip] == 'no' + items.where.not(wip_match(items.arel_table)) + else + items + end + end + + def wip_match(table) + table[:title].matches('WIP:%') + .or(table[:title].matches('WIP %')) + .or(table[:title].matches('[WIP]%')) + end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 0481a4a3d28..6559f94a696 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -261,7 +261,7 @@ class MergeRequest < ActiveRecord::Base end end - WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze + WIP_REGEX = /\A*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze def self.work_in_progress?(title) !!(title =~ WIP_REGEX) diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 102907a8bd3..42fd213d03b 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -58,7 +58,7 @@ class WikiPage attr_reader :page # The attributes Hash used for storing and validating - # new Page values before writing to the Gollum repository. + # new Page values before writing to the raw repository. attr_accessor :attributes def hook_attrs @@ -111,10 +111,7 @@ class WikiPage # The processed/formatted content of this page. def formatted_content - # Assuming @page exists, nil formatted_data means we didn't load it - # before hand (i.e. page was fetched by Gitaly), so we fetch it separately. - # If the page was fetched by Gollum, formatted_data would've been a String. - @attributes[:formatted_content] ||= @page&.formatted_data || @wiki.page_formatted_data(@page) + @attributes[:formatted_content] ||= @wiki.page_formatted_data(@page) end # The markup format for the page. diff --git a/app/serializers/diff_file_entity.rb b/app/serializers/diff_file_entity.rb index cbe6f200b86..c193ed10fef 100644 --- a/app/serializers/diff_file_entity.rb +++ b/app/serializers/diff_file_entity.rb @@ -84,7 +84,7 @@ class DiffFileEntity < Grape::Entity end expose :old_path_html do |diff_file| - old_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path) + old_path, _ = mark_inline_diffs(diff_file.old_path, diff_file.new_path) old_path end diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb index 719bd6ef418..cefcd3d3f5a 100644 --- a/app/uploaders/gitlab_uploader.rb +++ b/app/uploaders/gitlab_uploader.rb @@ -63,6 +63,12 @@ class GitlabUploader < CarrierWave::Uploader::Base super || file&.filename end + def relative_path + return path if pathname.relative? + + pathname.relative_path_from(Pathname.new(root)) + end + def model_valid? !!model end @@ -115,4 +121,8 @@ class GitlabUploader < CarrierWave::Uploader::Base # the cache directory. File.join(work_dir, cache_id, version_name.to_s, for_file) end + + def pathname + @pathname ||= Pathname.new(path) + end end diff --git a/app/uploaders/job_artifact_uploader.rb b/app/uploaders/job_artifact_uploader.rb index 557b13a8bd6..400f0b3dcc6 100644 --- a/app/uploaders/job_artifact_uploader.rb +++ b/app/uploaders/job_artifact_uploader.rb @@ -9,6 +9,8 @@ class JobArtifactUploader < GitlabUploader storage_options Gitlab.config.artifacts + alias_method :upload, :model + def cached_size return model.size if model.size.present? && !model.file_changed? diff --git a/app/uploaders/legacy_artifact_uploader.rb b/app/uploaders/legacy_artifact_uploader.rb index b4d0d752016..a9afc104ed1 100644 --- a/app/uploaders/legacy_artifact_uploader.rb +++ b/app/uploaders/legacy_artifact_uploader.rb @@ -8,6 +8,8 @@ class LegacyArtifactUploader < GitlabUploader storage_options Gitlab.config.artifacts + alias_method :upload, :model + def store_dir dynamic_segment end diff --git a/app/uploaders/lfs_object_uploader.rb b/app/uploaders/lfs_object_uploader.rb index f3d32e6b39d..0a966f3d44f 100644 --- a/app/uploaders/lfs_object_uploader.rb +++ b/app/uploaders/lfs_object_uploader.rb @@ -6,6 +6,8 @@ class LfsObjectUploader < GitlabUploader storage_options Gitlab.config.lfs + alias_method :upload, :model + def filename model.oid[4..-1] end diff --git a/app/views/admin/applications/show.html.haml b/app/views/admin/applications/show.html.haml index 593a6d816e3..e69143abe45 100644 --- a/app/views/admin/applications/show.html.haml +++ b/app/views/admin/applications/show.html.haml @@ -1,4 +1,5 @@ - page_title @application.name, "Applications" + %h3.page-title Application: #{@application.name} @@ -6,23 +7,29 @@ %table.table %tr %td - Application Id + = _('Application ID') %td - %code#application_id= @application.uid + .clipboard-group + .input-group + %input.label.label-monospace{ id: "application_id", type: "text", autocomplete: 'off', value: @application.uid, readonly: true } + .input-group-append + = clipboard_button(target: '#application_id', title: _("Copy ID to clipboard"), class: "btn btn btn-default") %tr %td - Secret: + = _('Secret') %td - %code#secret= @application.secret - + .clipboard-group + .input-group + %input.label.label-monospace{ id: "secret", type: "text", autocomplete: 'off', value: @application.secret, readonly: true } + .input-group-append + = clipboard_button(target: '#application_id', title: _("Copy secret to clipboard"), class: "btn btn btn-default") %tr %td - Callback url + = _('Callback URL') %td - @application.redirect_uri.split.each do |uri| %div %span.monospace= uri - %tr %td Trusted diff --git a/app/views/doorkeeper/applications/show.html.haml b/app/views/doorkeeper/applications/show.html.haml index bb76ac6d5f6..776bbc36ec2 100644 --- a/app/views/doorkeeper/applications/show.html.haml +++ b/app/views/doorkeeper/applications/show.html.haml @@ -10,18 +10,25 @@ %table.table %tr %td - = _('Application Id') + = _('Application ID') %td - %code#application_id= @application.uid + .clipboard-group + .input-group + %input.label.label-monospace{ id: "application_id", type: "text", autocomplete: 'off', value: @application.uid, readonly: true } + .input-group-append + = clipboard_button(target: '#application_id', title: _("Copy ID to clipboard"), class: "btn btn btn-default") %tr %td - = _('Secret:') + = _('Secret') %td - %code#secret= @application.secret - + .clipboard-group + .input-group + %input.label.label-monospace{ id: "secret", type: "text", autocomplete: 'off', value: @application.secret, readonly: true } + .input-group-append + = clipboard_button(target: '#application_id', title: _("Copy secret to clipboard"), class: "btn btn btn-default") %tr %td - = _('Callback url') + = _('Callback URL') %td - @application.redirect_uri.split.each do |uri| %div diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index 43170587797..4aa22138498 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -10,7 +10,7 @@ = group_icon(@group, class: "avatar s40 avatar-tile") .sidebar-context-title = @group.name - %ul.sidebar-top-level-items + %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 = link_to group_path(@group) do @@ -109,9 +109,9 @@ = link_to edit_group_path(@group) do .nav-icon-container = sprite_icon('settings') - %span.nav-item-name.qa-settings-item + %span.nav-item-name.qa-group-settings-item = _('Settings') - %ul.sidebar-sub-level-items + %ul.sidebar-sub-level-items.qa-group-sidebar-submenu = nav_link(path: %w[groups#projects groups#edit badges#index ci_cd#show], html_options: { class: "fly-out-top-item" } ) do = link_to edit_group_path(@group) do %strong.fly-out-top-item-name diff --git a/app/views/projects/_import_project_pane.html.haml b/app/views/projects/_import_project_pane.html.haml index 32da38f14b9..2b425f18389 100644 --- a/app/views/projects/_import_project_pane.html.haml +++ b/app/views/projects/_import_project_pane.html.haml @@ -1,4 +1,5 @@ - active_tab = local_assigns.fetch(:active_tab, 'blank') +- track_label = local_assigns.fetch(:track_label, 'import_project') .project-import .form-group.import-btn-container.clearfix @@ -7,60 +8,63 @@ .import-buttons - if gitlab_project_import_enabled? .import_gitlab_project.has-tooltip{ data: { container: 'body' } } - = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do + = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "gitlab_export" } do = icon('gitlab', text: 'GitLab export') - if github_import_enabled? %div - = link_to new_import_github_path, class: 'btn js-import-github' do + = link_to new_import_github_path, class: 'btn js-import-github', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "github" } do = icon('github', text: 'GitHub') - if bitbucket_import_enabled? %div - = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do + = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}", + data: { track_label: "#{track_label}", track_event: "click_button", track_property: "bitbucket_cloud" } do = icon('bitbucket', text: 'Bitbucket Cloud') - unless bitbucket_import_configured? = render 'bitbucket_import_modal' - if bitbucket_server_import_enabled? %div - = link_to status_import_bitbucket_server_path, class: "btn import_bitbucket" do + = link_to status_import_bitbucket_server_path, class: "btn import_bitbucket", + data: { track_label: "#{track_label}", track_event: "click_button", track_property: "bitbucket_server" } do = icon('bitbucket-square', text: 'Bitbucket Server') %div - if gitlab_import_enabled? %div - = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do + = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}", + data: { track_label: "#{track_label}", track_event: "click_button", track_property: "gitlab_com" } do = icon('gitlab', text: 'GitLab.com') - unless gitlab_import_configured? = render 'gitlab_import_modal' - if google_code_import_enabled? %div - = link_to new_import_google_code_path, class: 'btn import_google_code' do + = link_to new_import_google_code_path, class: 'btn import_google_code', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "google_code" } do = icon('google', text: 'Google Code') - if fogbugz_import_enabled? %div - = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do + = link_to new_import_fogbugz_path, class: 'btn import_fogbugz', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "fogbugz" } do = icon('bug', text: 'Fogbugz') - if gitea_import_enabled? %div - = link_to new_import_gitea_path, class: 'btn import_gitea' do + = link_to new_import_gitea_path, class: 'btn import_gitea', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "gitea" } do = custom_icon('go_logo') Gitea - if git_import_enabled? %div - %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } } + %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active', data: { toggle_open_class: 'active', track_label: "#{track_label}" , track_event: "click_button", track_property: "repo_url" } } } = icon('git', text: 'Repo by URL') - if manifest_import_enabled? %div - = link_to new_import_manifest_path, class: 'btn import_manifest' do + = link_to new_import_manifest_path, class: 'btn import_manifest', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "manifest_file" } do = icon('file-text-o', text: 'Manifest file') .js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') } = form_for @project, html: { class: 'new_project' } do |f| %hr = render "shared/import_form", f: f - = render 'new_project_fields', f: f, project_name_id: "import-url-name", hide_init_with_readme: true + = render 'new_project_fields', f: f, project_name_id: "import-url-name", hide_init_with_readme: true, track_label: track_label diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml index cbf89fa8f02..e1f28364e19 100644 --- a/app/views/projects/_new_project_fields.html.haml +++ b/app/views/projects/_new_project_fields.html.haml @@ -1,13 +1,14 @@ - visibility_level = params.dig(:project, :visibility_level) || default_project_visibility - ci_cd_only = local_assigns.fetch(:ci_cd_only, false) - hide_init_with_readme = local_assigns.fetch(:hide_init_with_readme, false) +- track_label = local_assigns.fetch(:track_label, 'blank_project') .row{ id: project_name_id } = f.hidden_field :ci_cd_only, value: ci_cd_only .form-group.project-name.col-sm-12 = f.label :name, class: 'label-bold' do %span= _("Project name") - = f.text_field :name, placeholder: "My awesome project", class: "form-control input-lg", autofocus: true + = f.text_field :name, placeholder: "My awesome project", class: "form-control input-lg", autofocus: true, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_name", track_value: "" } .form-group.project-path.col-sm-6 = f.label :namespace_id, class: 'label-bold' do %span= s_("Project URL") @@ -22,7 +23,7 @@ display_path: true, extra_group: namespace_id), {}, - { class: 'select2 js-select-namespace qa-project-namespace-select', tabindex: 1}) + { class: 'select2 js-select-namespace qa-project-namespace-select', tabindex: 1, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_path", track_value: "" }}) - else .input-group-prepend.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' } @@ -42,7 +43,7 @@ = f.label :description, class: 'label-bold' do Project description %span (optional) - = f.text_area :description, placeholder: 'Description format', class: "form-control", rows: 3, maxlength: 250 + = f.text_area :description, placeholder: 'Description format', class: "form-control", rows: 3, maxlength: 250, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_description", track_value: "" } = f.label :visibility_level, class: 'label-bold' do Visibility Level @@ -53,12 +54,12 @@ .form-group.row.initialize-with-readme-setting %div{ :class => "col-sm-12" } .form-check - = check_box_tag 'project[initialize_with_readme]', '1', false, class: 'form-check-input' + = check_box_tag 'project[initialize_with_readme]', '1', false, class: 'form-check-input', data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "init_with_readme" } = label_tag 'project[initialize_with_readme]', class: 'form-check-label' do .option-title %strong Initialize repository with a README .option-description Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository. -= f.submit 'Create project', class: "btn btn-success project-submit", tabindex: 4 -= link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel' += f.submit 'Create project', class: "btn btn-success project-submit", tabindex: 4, data: { track_label: "#{track_label}", track_event: "click_button", track_property: "create_project", track_value: "" } += link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "cancel" } diff --git a/app/views/projects/_project_templates.html.haml b/app/views/projects/_project_templates.html.haml index 0f6f3ad6d5e..98fdb1d7a0b 100644 --- a/app/views/projects/_project_templates.html.haml +++ b/app/views/projects/_project_templates.html.haml @@ -5,4 +5,4 @@ .project-fields-form = render 'projects/project_templates/project_fields_form' - = render 'projects/new_project_fields', f: f, project_name_id: "template-project-name", hide_init_with_readme: true + = render 'projects/new_project_fields', f: f, project_name_id: "template-project-name", hide_init_with_readme: true, track_label: "create_from_template" diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml deleted file mode 100644 index 66a3b8b8fd1..00000000000 --- a/app/views/projects/jobs/_sidebar.html.haml +++ /dev/null @@ -1,38 +0,0 @@ -%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } } - .sidebar-container - .blocks-container - #js-details-block-vue{ data: { terminal_path: can?(current_user, :create_build_terminal, @build) && @build.has_terminal? ? terminal_project_job_path(@project, @build) : nil } } - - - if @build.pipeline.stages_count > 1 - .block-last.dropdown.build-dropdown - %div - %span{ class: "ci-status-icon-#{@build.pipeline.status}" } - = ci_icon_for_status(@build.pipeline.status) - Pipeline - = link_to "##{@build.pipeline.id}", project_pipeline_path(@project, @build.pipeline), class: 'link-commit' - from - = link_to "#{@build.pipeline.ref}", project_ref_path(@project, @build.pipeline.ref), class: 'link-commit ref-name' - %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } - %span.stage-selection More - = icon('chevron-down') - %ul.dropdown-menu - - @build.pipeline.legacy_stages.each do |stage| - %li - %a.stage-item= stage.name - - .builds-container - - HasStatus::ORDERED_STATUSES.each do |build_status| - - builds.select{|build| build.status == build_status}.each do |build| - .build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } } - - tooltip = sanitize(build.tooltip_message.dup) - = link_to(project_job_path(@project, build), data: { toggle: 'tooltip', title: tooltip, container: 'body' }) do - = sprite_icon('arrow-right', size:16, css_class: 'icon-arrow-right') - %span{ class: "ci-status-icon-#{build.status}" } - = ci_icon_for_status(build.status) - %span - - if build.name - = build.name - - else - = build.id - - if build.retried? - = sprite_icon('retry', size:16, css_class: 'icon-retry') diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml index 5321bc46e73..ab7963737ca 100644 --- a/app/views/projects/jobs/show.html.haml +++ b/app/views/projects/jobs/show.html.haml @@ -9,54 +9,6 @@ %div{ class: container_class } .build-page.js-build-page #js-build-header-vue - - if @build.stuck? - - unless @build.any_runners_online? - .bs-callout.bs-callout-warning.js-build-stuck - %p - - if @project.any_runners? - This job is stuck, because the project doesn't have any runners online assigned to it. - - elsif @build.tags.any? - This job is stuck, because you don't have any active runners online with any of these tags assigned to them: - - @build.tags.each do |tag| - %span.badge.badge-primary - = tag - - else - This job is stuck, because you don't have any active runners that can run this job. - - %br - Go to - = link_to project_runners_path(@build.project, anchor: 'js-runners-settings') do - Runners page - - - if @build.starts_environment? - .prepend-top-default.js-environment-container - .environment-information - - if @build.outdated_deployment? - = ci_icon_for_status('success_with_warnings') - - else - = ci_icon_for_status(@build.status) - - - environment = environment_for_build(@build.project, @build) - - if @build.success? && @build.last_deployment.present? - - if @build.last_deployment.last? - This job is the most recent deployment to #{environment_link_for_build(@build.project, @build)}. - - else - This job is an out-of-date deployment to #{environment_link_for_build(@build.project, @build)}. - View the most recent deployment #{deployment_link(environment.last_deployment)}. - - elsif @build.complete? && !@build.success? - The deployment of this job to #{environment_link_for_build(@build.project, @build)} did not succeed. - - else - This job is creating a deployment to #{environment_link_for_build(@build.project, @build)} - - if environment.try(:last_deployment) - and will overwrite the #{deployment_link(environment.last_deployment, text: 'latest deployment')} - - - if @build.erased? - .prepend-top-default.js-build-erased - .erased.alert.alert-warning - - if @build.erased_by_user? - Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)} - - else - Job has been erased #{time_ago_with_tooltip(@build.erased_at)} - if @build.running? || @build.has_trace? .build-trace-container.prepend-top-default @@ -93,7 +45,7 @@ - else = render "empty_states" - = render "sidebar", builds: @builds + #js-details-block-vue{ data: { terminal_path: can?(current_user, :create_build_terminal, @build) && @build.has_terminal? ? terminal_project_job_path(@project, @build) : nil } } .js-build-options{ data: javascript_build_options } diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index f9b4cddf9b2..d99b809c387 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -29,15 +29,15 @@ .col-lg-9.js-toggle-container %ul.nav.nav-tabs.nav-links.gitlab-tabs{ role: 'tablist' } %li.nav-item{ role: 'presentation' } - %a.nav-link.active{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab' }, role: 'tab' } + %a.nav-link.active{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab', track_label: 'blank_project', track_event: "click_tab" }, role: 'tab' } %span.d-none.d-sm-block Blank project %span.d-block.d-sm-none Blank %li.nav-item{ role: 'presentation' } - %a.nav-link{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab' }, role: 'tab' } + %a.nav-link{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab', track_label: 'create_from_template', track_event: "click_tab" }, role: 'tab' } %span.d-none.d-sm-block Create from template %span.d-block.d-sm-none Template %li.nav-item{ role: 'presentation' } - %a.nav-link{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab' }, role: 'tab' } + %a.nav-link{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab', track_label: 'import_project', track_event: "click_tab" }, role: 'tab' } %span.d-none.d-sm-block Import project %span.d-block.d-sm-none Import diff --git a/app/views/projects/project_templates/_built_in_templates.html.haml b/app/views/projects/project_templates/_built_in_templates.html.haml index e7636099be6..233c3adba0e 100644 --- a/app/views/projects/project_templates/_built_in_templates.html.haml +++ b/app/views/projects/project_templates/_built_in_templates.html.haml @@ -10,8 +10,8 @@ = template.description .controls.d-flex.align-items-center %label.btn.btn-success.template-button.choose-template.append-right-10.append-bottom-0{ for: template.name } - %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name } + %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name, data: { track_label: "create_from_template", track_property: "template_use", track_event: "click_button" } } %span = _("Use template") - %a.btn.btn-default{ href: template.preview, rel: 'noopener noreferrer', target: '_blank' } + %a.btn.btn-default{ href: template.preview, rel: 'noopener noreferrer', target: '_blank', data: { track_label: "create_from_template", track_property: "template_preview", track_event: "click_button", track_value: template.name } } = _("Preview") diff --git a/app/views/shared/_visibility_radios.html.haml b/app/views/shared/_visibility_radios.html.haml index dd6b9cce58e..9fc46afe177 100644 --- a/app/views/shared/_visibility_radios.html.haml +++ b/app/views/shared/_visibility_radios.html.haml @@ -3,7 +3,7 @@ - restricted = restricted_visibility_levels.include?(level) - disabled = disallowed || restricted .form-check{ class: [('disabled' if disabled), ('restricted' if restricted)] } - = form.radio_button model_method, level, checked: (selected_level == level), disabled: disabled, class: 'form-check-input' + = form.radio_button model_method, level, checked: (selected_level == level), disabled: disabled, class: 'form-check-input', data: { track_label: "blank_project", track_event: "activate_form_input", track_property: "#{model_method}", track_value: "#{level}" } = form.label "#{model_method}_#{level}", class: 'form-check-label' do = visibility_level_icon(level) .option-title diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 659e03fd67d..c4d177361e7 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -33,13 +33,13 @@ #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown %ul{ data: { dropdown: true } } %li.filter-dropdown-item{ data: { action: 'submit' } } - %button.btn.btn-link + %button.btn.btn-link{ type: 'button' } = sprite_icon('search') %span Press Enter or click to search %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item - %button.btn.btn-link + %button.btn.btn-link{ type: 'button' } -# Encapsulate static class name `{{icon}}` inside #{} to bypass -# haml lint's ClassAttributeWithStaticValue %svg @@ -60,7 +60,7 @@ #js-dropdown-assignee.filtered-search-input-dropdown-menu.dropdown-menu %ul{ data: { dropdown: true } } %li.filter-dropdown-item{ data: { value: 'none' } } - %button.btn.btn-link + %button.btn.btn-link{ type: 'button' } No Assignee %li.divider.droplab-item-ignore - if current_user @@ -73,38 +73,46 @@ #js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu %ul{ data: { dropdown: true } } %li.filter-dropdown-item{ data: { value: 'none' } } - %button.btn.btn-link + %button.btn.btn-link{ type: 'button' } No Milestone %li.filter-dropdown-item{ data: { value: 'upcoming' } } - %button.btn.btn-link + %button.btn.btn-link{ type: 'button' } Upcoming %li.filter-dropdown-item{ 'data-value' => 'started' } - %button.btn.btn-link + %button.btn.btn-link{ type: 'button' } Started %li.divider.droplab-item-ignore %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item - %button.btn.btn-link.js-data-value + %button.btn.btn-link.js-data-value{ type: 'button' } {{title}} #js-dropdown-label.filtered-search-input-dropdown-menu.dropdown-menu %ul{ data: { dropdown: true } } %li.filter-dropdown-item{ data: { value: 'none' } } - %button.btn.btn-link + %button.btn.btn-link{ type: 'button' } No Label %li.divider.droplab-item-ignore %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item - %button.btn.btn-link + %button.btn.btn-link{ type: 'button' } %span.dropdown-label-box{ style: 'background: {{color}}' } %span.label-title.js-data-value {{title}} #js-dropdown-my-reaction.filtered-search-input-dropdown-menu.dropdown-menu %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item - %button.btn.btn-link + %button.btn.btn-link{ type: 'button' } %gl-emoji %span.js-data-value.prepend-left-10 {{name}} + #js-dropdown-wip.filtered-search-input-dropdown-menu.dropdown-menu + %ul.filter-dropdown{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'yes', capitalize: true } } + %button.btn.btn-link{ type: 'button' } + = _('Yes') + %li.filter-dropdown-item{ data: { value: 'no', capitalize: true } } + %button.btn.btn-link{ type: 'button' } + = _('No') = render_if_exists 'shared/issuable/filter_weight', type: type |