diff options
Diffstat (limited to 'app/assets')
99 files changed, 1502 insertions, 374 deletions
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue index 85a15b38de1..df74eb2c2f7 100644 --- a/app/assets/javascripts/badges/components/badge_form.vue +++ b/app/assets/javascripts/badges/components/badge_form.vue @@ -90,7 +90,7 @@ export default { }, badgeImageUrlExample() { const exampleUrl = - 'https://example.gitlab.com/%{project_path}/badges/%{default_branch}/badge.svg'; + 'https://example.gitlab.com/%{project_path}/badges/%{default_branch}/pipeline.svg'; return sprintf(s__('Badges|e.g. %{exampleUrl}'), { exampleUrl, }); diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index f0ce2579ee7..8f47931d14a 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -4,6 +4,7 @@ import Icon from '~/vue_shared/components/icon.vue'; import { __ } from '~/locale'; import createFlash from '~/flash'; import { GlLoadingIcon } from '@gitlab/ui'; +import PanelResizer from '~/vue_shared/components/panel_resizer.vue'; import eventHub from '../../notes/event_hub'; import CompareVersions from './compare_versions.vue'; import DiffFile from './diff_file.vue'; @@ -11,6 +12,13 @@ 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'; +import { + TREE_LIST_WIDTH_STORAGE_KEY, + INITIAL_TREE_WIDTH, + MIN_TREE_WIDTH, + MAX_TREE_WIDTH, + TREE_HIDE_STATS_WIDTH, +} from '../constants'; export default { name: 'DiffsApp', @@ -23,6 +31,7 @@ export default { CommitWidget, TreeList, GlLoadingIcon, + PanelResizer, }, props: { endpoint: { @@ -54,8 +63,12 @@ export default { }, }, data() { + const treeWidth = + parseInt(localStorage.getItem(TREE_LIST_WIDTH_STORAGE_KEY), 10) || INITIAL_TREE_WIDTH; + return { assignedDiscussions: false, + treeWidth, }; }, computed: { @@ -96,6 +109,9 @@ export default { this.startVersion.version_index === this.mergeRequestDiff.version_index) ); }, + hideFileStats() { + return this.treeWidth <= TREE_HIDE_STATS_WIDTH; + }, }, watch: { diffViewType() { @@ -142,6 +158,7 @@ export default { 'startRenderDiffsQueue', 'assignDiscussionsToDiff', 'setHighlightedRow', + 'cacheTreeListWidth', ]), fetchData() { this.fetchDiffFiles() @@ -184,6 +201,8 @@ export default { } }, }, + minTreeWidth: MIN_TREE_WIDTH, + maxTreeWidth: MAX_TREE_WIDTH, }; </script> @@ -209,7 +228,21 @@ export default { :data-can-create-note="getNoteableData.current_user.can_create_note" class="files d-flex prepend-top-default" > - <div v-show="showTreeList" class="diff-tree-list"><tree-list /></div> + <div + v-show="showTreeList" + :style="{ width: `${treeWidth}px` }" + class="diff-tree-list js-diff-tree-list" + > + <panel-resizer + :size.sync="treeWidth" + :start-size="treeWidth" + :min-size="$options.minTreeWidth" + :max-size="$options.maxTreeWidth" + side="right" + @resize-end="cacheTreeListWidth" + /> + <tree-list :hide-file-stats="hideFileStats" /> + </div> <div class="diff-files-holder"> <commit-widget v-if="commit" :commit="commit" /> <template v-if="renderDiffFiles"> diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue index 6dc2f5d3f68..cb92093db32 100644 --- a/app/assets/javascripts/diffs/components/diff_content.vue +++ b/app/assets/javascripts/diffs/components/diff_content.vue @@ -1,7 +1,8 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue'; -import EmptyFileViewer from '~/vue_shared/components/diff_viewer/viewers/empty_file.vue'; +import NotDiffableViewer from '~/vue_shared/components/diff_viewer/viewers/not_diffable.vue'; +import NoPreviewViewer from '~/vue_shared/components/diff_viewer/viewers/no_preview.vue'; import InlineDiffView from './inline_diff_view.vue'; import ParallelDiffView from './parallel_diff_view.vue'; import NoteForm from '../../notes/components/note_form.vue'; @@ -9,6 +10,7 @@ import ImageDiffOverlay from './image_diff_overlay.vue'; import DiffDiscussions from './diff_discussions.vue'; import { IMAGE_DIFF_POSITION_TYPE } from '../constants'; import { getDiffMode } from '../store/utils'; +import { diffViewerModes } from '~/ide/constants'; export default { components: { @@ -18,7 +20,8 @@ export default { NoteForm, DiffDiscussions, ImageDiffOverlay, - EmptyFileViewer, + NotDiffableViewer, + NoPreviewViewer, }, props: { diffFile: { @@ -42,11 +45,17 @@ export default { diffMode() { return getDiffMode(this.diffFile); }, + diffViewerMode() { + return this.diffFile.viewer.name; + }, isTextFile() { - return this.diffFile.viewer.name === 'text'; + return this.diffViewerMode === diffViewerModes.text; + }, + noPreview() { + return this.diffViewerMode === diffViewerModes.no_preview; }, - errorMessage() { - return this.diffFile.viewer.error; + notDiffable() { + return this.diffViewerMode === diffViewerModes.not_diffable; }, diffFileCommentForm() { return this.getCommentFormForDiffFile(this.diffFile.file_hash); @@ -78,11 +87,10 @@ export default { <template> <div class="diff-content"> - <div v-if="!errorMessage" class="diff-viewer"> + <div class="diff-viewer"> <template v-if="isTextFile"> - <empty-file-viewer v-if="diffFile.empty" /> <inline-diff-view - v-else-if="isInlineView" + v-if="isInlineView" :diff-file="diffFile" :diff-lines="diffFile.highlighted_diff_lines || []" :help-page-path="helpPagePath" @@ -94,9 +102,12 @@ export default { :help-page-path="helpPagePath" /> </template> + <not-diffable-viewer v-else-if="notDiffable" /> + <no-preview-viewer v-else-if="noPreview" /> <diff-viewer v-else :diff-mode="diffMode" + :diff-viewer-mode="diffViewerMode" :new-path="diffFile.new_path" :new-sha="diffFile.diff_refs.head_sha" :old-path="diffFile.old_path" @@ -132,8 +143,5 @@ export default { </div> </diff-viewer> </div> - <div v-else class="diff-viewer"> - <div class="nothing-here-block" v-html="errorMessage"></div> - </div> </div> </template> diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index 449f7007077..1141a197c6a 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -7,6 +7,7 @@ import { GlLoadingIcon } from '@gitlab/ui'; import eventHub from '../../notes/event_hub'; import DiffFileHeader from './diff_file_header.vue'; import DiffContent from './diff_content.vue'; +import { diffViewerErrors } from '~/ide/constants'; export default { components: { @@ -33,15 +34,13 @@ export default { return { isLoadingCollapsedDiff: false, forkMessageVisible: false, + isCollapsed: this.file.viewer.collapsed || false, }; }, computed: { ...mapState('diffs', ['currentDiffFileId']), ...mapGetters(['isNotesFetched']), ...mapGetters('diffs', ['getDiffFileDiscussions']), - isCollapsed() { - return this.file.collapsed || false; - }, viewBlobLink() { return sprintf( __('You can %{linkStart}view the blob%{linkEnd} instead.'), @@ -52,17 +51,6 @@ export default { false, ); }, - showExpandMessage() { - return ( - this.isCollapsed || - (!this.file.highlighted_diff_lines && - !this.isLoadingCollapsedDiff && - !this.file.too_large && - this.file.text && - !this.file.renamed_file && - !this.file.mode_changed) - ); - }, showLoadingIcon() { return this.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed); }, @@ -73,9 +61,15 @@ export default { this.file.parallel_diff_lines.length > 0 ); }, + isFileTooLarge() { + return this.file.viewer.error === diffViewerErrors.too_large; + }, + errorMessage() { + return this.file.viewer.error_message; + }, }, watch: { - 'file.collapsed': function fileCollapsedWatch(newVal, oldVal) { + isCollapsed: function fileCollapsedWatch(newVal, oldVal) { if (!newVal && oldVal && !this.hasDiffLines) { this.handleLoadCollapsedDiff(); } @@ -85,13 +79,13 @@ export default { eventHub.$on(`loadCollapsedDiff/${this.file.file_hash}`, this.handleLoadCollapsedDiff); }, methods: { - ...mapActions('diffs', ['loadCollapsedDiff', 'assignDiscussionsToDiff']), + ...mapActions('diffs', ['loadCollapsedDiff', 'assignDiscussionsToDiff', 'setRenderIt']), handleToggle() { if (!this.hasDiffLines) { this.handleLoadCollapsedDiff(); } else { - this.file.collapsed = !this.file.collapsed; - this.file.renderIt = true; + this.isCollapsed = !this.isCollapsed; + this.setRenderIt(this.file); } }, handleLoadCollapsedDiff() { @@ -100,8 +94,8 @@ export default { this.loadCollapsedDiff(this.file) .then(() => { this.isLoadingCollapsedDiff = false; - this.file.collapsed = false; - this.file.renderIt = true; + this.isCollapsed = false; + this.setRenderIt(this.file); }) .then(() => { requestIdleCallback( @@ -164,21 +158,25 @@ export default { Cancel </button> </div> - - <diff-content - v-if="!isCollapsed && file.renderIt" - :class="{ hidden: isCollapsed || file.too_large }" - :diff-file="file" - :help-page-path="helpPagePath" - /> <gl-loading-icon v-if="showLoadingIcon" class="diff-content loading" /> - <div v-else-if="showExpandMessage" class="nothing-here-block diff-collapsed"> - {{ __('This diff is collapsed.') }} - <a class="click-to-expand js-click-to-expand" href="#" @click.prevent="handleToggle">{{ - __('Click to expand it.') - }}</a> - </div> - <div v-if="file.too_large" class="nothing-here-block diff-collapsed js-too-large-diff"> + <template v-else> + <div v-if="errorMessage" class="diff-viewer"> + <div class="nothing-here-block" v-html="errorMessage"></div> + </div> + <div v-else-if="isCollapsed" class="nothing-here-block diff-collapsed"> + {{ __('This diff is collapsed.') }} + <a class="click-to-expand js-click-to-expand" href="#" @click.prevent="handleToggle">{{ + __('Click to expand it.') + }}</a> + </div> + <diff-content + v-else + :class="{ hidden: isCollapsed || isFileTooLarge }" + :diff-file="file" + :help-page-path="helpPagePath" + /> + </template> + <div v-if="isFileTooLarge" class="nothing-here-block diff-collapsed js-too-large-diff"> {{ __('This source diff could not be displayed because it is too large.') }} <span v-html="viewBlobLink"></span> </div> diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index 60586d4a607..2b801898345 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -8,6 +8,7 @@ import FileIcon from '~/vue_shared/components/file_icon.vue'; import { GlTooltipDirective } from '@gitlab/ui'; import { truncateSha } from '~/lib/utils/text_utility'; import { __, s__, sprintf } from '~/locale'; +import { diffViewerModes } from '~/ide/constants'; import EditButton from './edit_button.vue'; import DiffStats from './diff_stats.vue'; @@ -118,6 +119,12 @@ export default { gfmCopyText() { return `\`${this.diffFile.file_path}\``; }, + isFileRenamed() { + return this.diffFile.viewer.name === diffViewerModes.renamed; + }, + isModeChanged() { + return this.diffFile.viewer.name === diffViewerModes.mode_changed; + }, }, mounted() { polyfillSticky(this.$refs.header); @@ -165,7 +172,7 @@ export default { aria-hidden="true" css-classes="js-file-icon append-right-5" /> - <span v-if="diffFile.renamed_file"> + <span v-if="isFileRenamed"> <strong v-gl-tooltip :title="diffFile.old_path" @@ -193,7 +200,7 @@ export default { css-class="btn-default btn-transparent btn-clipboard" /> - <small v-if="diffFile.mode_changed" ref="fileMode"> + <small v-if="isModeChanged" ref="fileMode"> {{ diffFile.a_mode }} → {{ diffFile.b_mode }} </small> diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue index 7e00b994541..8fc3af15bea 100644 --- a/app/assets/javascripts/diffs/components/tree_list.vue +++ b/app/assets/javascripts/diffs/components/tree_list.vue @@ -13,6 +13,12 @@ export default { Icon, FileRow, }, + props: { + hideFileStats: { + type: Boolean, + required: true, + }, + }, data() { return { search: '', @@ -40,6 +46,9 @@ export default { return acc; }, []); }, + fileRowExtraComponent() { + return this.hideFileStats ? null : FileRowStats; + }, }, methods: { ...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile', 'toggleFileFinder']), @@ -48,7 +57,6 @@ export default { }, }, shortcutKeyCharacter: `${/Mac/i.test(navigator.userAgent) ? '⌘' : 'Ctrl'}+P`, - FileRowStats, diffTreeFiltering: gon.features && gon.features.diffTreeFiltering, }; </script> @@ -98,7 +106,7 @@ export default { :file="file" :level="0" :hide-extra-on-tree="true" - :extra-component="$options.FileRowStats" + :extra-component="fileRowExtraComponent" :show-changed-icon="true" @toggleTreeOpen="toggleTreeOpen" @clickFile="scrollToFile" diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js index bd188d9de9e..7002655ea49 100644 --- a/app/assets/javascripts/diffs/constants.js +++ b/app/assets/javascripts/diffs/constants.js @@ -36,3 +36,9 @@ export const MR_TREE_SHOW_KEY = 'mr_tree_show'; export const TREE_TYPE = 'tree'; export const TREE_LIST_STORAGE_KEY = 'mr_diff_tree_list'; export const WHITESPACE_STORAGE_KEY = 'mr_show_whitespace'; +export const TREE_LIST_WIDTH_STORAGE_KEY = 'mr_tree_list_width'; + +export const INITIAL_TREE_WIDTH = 320; +export const MIN_TREE_WIDTH = 240; +export const MAX_TREE_WIDTH = 400; +export const TREE_HIDE_STATS_WIDTH = 260; diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 7fb66ce433b..82ff2e3be76 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -16,7 +16,9 @@ import { MR_TREE_SHOW_KEY, TREE_LIST_STORAGE_KEY, WHITESPACE_STORAGE_KEY, + TREE_LIST_WIDTH_STORAGE_KEY, } from '../constants'; +import { diffViewerModes } from '~/ide/constants'; export const setBaseConfig = ({ commit }, options) => { const { endpoint, projectPath } = options; @@ -91,7 +93,7 @@ export const renderFileForDiscussionId = ({ commit, rootState, state }, discussi commit(types.RENDER_FILE, file); } - if (file.collapsed) { + if (file.viewer.collapsed) { eventHub.$emit(`loadCollapsedDiff/${file.file_hash}`); scrollToElement(document.getElementById(file.file_hash)); } else { @@ -105,7 +107,8 @@ export const startRenderDiffsQueue = ({ state, commit }) => { const checkItem = () => new Promise(resolve => { const nextFile = state.diffFiles.find( - file => !file.renderIt && (!file.collapsed || !file.text), + file => + !file.renderIt && (!file.viewer.collapsed || !file.viewer.name === diffViewerModes.text), ); if (nextFile) { @@ -128,6 +131,8 @@ export const startRenderDiffsQueue = ({ state, commit }) => { return checkItem(); }; +export const setRenderIt = ({ commit }, file) => commit(types.RENDER_FILE, file); + export const setInlineDiffViewType = ({ commit }) => { commit(types.SET_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE); @@ -300,5 +305,9 @@ export const toggleFileFinder = ({ commit }, visible) => { commit(types.TOGGLE_FILE_FINDER_VISIBLE, visible); }; +export const cacheTreeListWidth = (_, size) => { + localStorage.setItem(TREE_LIST_WIDTH_STORAGE_KEY, size); +}; + // 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 0e1ad654a2b..4e7e5306995 100644 --- a/app/assets/javascripts/diffs/store/getters.js +++ b/app/assets/javascripts/diffs/store/getters.js @@ -4,7 +4,8 @@ export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW export const isInlineView = state => state.diffViewType === INLINE_DIFF_VIEW_TYPE; -export const hasCollapsedFile = state => state.diffFiles.some(file => file.collapsed); +export const hasCollapsedFile = state => + state.diffFiles.some(file => file.viewer && file.viewer.collapsed); export const commitId = state => (state.commit && state.commit.id ? state.commit.id : null); diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index 7bbafe66199..5a27388863c 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -144,6 +144,7 @@ export default { if (left || right) { return { + ...line, left: line.left ? mapDiscussions(line.left) : null, right: line.right ? mapDiscussions(line.right, () => !left) : null, }; diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js index effb6202327..247d1e65fea 100644 --- a/app/assets/javascripts/diffs/store/utils.js +++ b/app/assets/javascripts/diffs/store/utils.js @@ -1,6 +1,6 @@ import _ from 'underscore'; -import { diffModes } from '~/ide/constants'; import { truncatePathMiddleToLength } from '~/lib/utils/text_utility'; +import { diffModes, diffViewerModes } from '~/ide/constants'; import { LINE_POSITION_LEFT, LINE_POSITION_RIGHT, @@ -161,6 +161,7 @@ export function addContextLines(options) { const normalizedParallelLines = contextLines.map(line => ({ left: line, right: line, + line_code: line.line_code, })); if (options.bottom) { @@ -247,7 +248,8 @@ export function prepareDiffData(diffData) { Object.assign(file, { renderIt: showingLines < LINES_TO_BE_RENDERED_DIRECTLY, - collapsed: file.text && showingLines > MAX_LINES_TO_BE_RENDERED, + collapsed: + file.viewer.name === diffViewerModes.text && showingLines > MAX_LINES_TO_BE_RENDERED, discussions: [], }); } @@ -403,7 +405,9 @@ export const getDiffMode = diffFile => { const diffModeKey = Object.keys(diffModes).find(key => diffFile[`${key}_file`]); return ( diffModes[diffModeKey] || - (diffFile.mode_changed && diffModes.mode_changed) || + (diffFile.viewer && + diffFile.viewer.name === diffViewerModes.mode_changed && + diffViewerModes.mode_changed) || diffModes.replaced ); }; diff --git a/app/assets/javascripts/emoji/no_emoji_validator.js b/app/assets/javascripts/emoji/no_emoji_validator.js new file mode 100644 index 00000000000..0fd4dd74953 --- /dev/null +++ b/app/assets/javascripts/emoji/no_emoji_validator.js @@ -0,0 +1,63 @@ +import { __ } from '~/locale'; +import emojiRegex from 'emoji-regex'; + +const invalidInputClass = 'gl-field-error-outline'; + +export default class NoEmojiValidator { + constructor(opts = {}) { + const container = opts.container || ''; + this.noEmojiEmelents = document.querySelectorAll(`${container} .js-block-emoji`); + + this.noEmojiEmelents.forEach(element => + element.addEventListener('input', this.eventHandler.bind(this)), + ); + } + + eventHandler(event) { + this.inputDomElement = event.target; + this.inputErrorMessage = this.inputDomElement.nextSibling; + + const { value } = this.inputDomElement; + + this.validatePattern(value); + this.setValidationStateAndMessage(); + } + + validatePattern(value) { + const pattern = emojiRegex(); + this.hasEmojis = new RegExp(pattern).test(value); + + if (this.hasEmojis) { + this.inputDomElement.setCustomValidity(__('Invalid input, please avoid emojis')); + } else { + this.inputDomElement.setCustomValidity(''); + } + } + + setValidationStateAndMessage() { + if (!this.inputDomElement.checkValidity()) { + this.setInvalidState(); + } else { + this.clearFieldValidationState(); + } + } + + clearFieldValidationState() { + this.inputDomElement.classList.remove(invalidInputClass); + this.inputErrorMessage.classList.add('hide'); + } + + setInvalidState() { + this.inputDomElement.classList.add(invalidInputClass); + this.setErrorMessage(); + } + + setErrorMessage() { + if (this.hasEmojis) { + this.inputErrorMessage.innerHTML = this.inputDomElement.validationMessage; + } else { + this.inputErrorMessage.innerHTML = this.inputDomElement.title; + } + this.inputErrorMessage.classList.remove('hide'); + } +} diff --git a/app/assets/javascripts/error_tracking/store/actions.js b/app/assets/javascripts/error_tracking/store/actions.js index 2e192c958ba..11aec312368 100644 --- a/app/assets/javascripts/error_tracking/store/actions.js +++ b/app/assets/javascripts/error_tracking/store/actions.js @@ -2,7 +2,7 @@ import Service from '../services'; import * as types from './mutation_types'; import createFlash from '~/flash'; import Poll from '~/lib/utils/poll'; -import { __ } from '~/locale'; +import { __, sprintf } from '~/locale'; let eTagPoll; @@ -19,9 +19,17 @@ export function startPolling({ commit }, endpoint) { commit(types.SET_EXTERNAL_URL, data.external_url); commit(types.SET_LOADING, false); }, - errorCallback: () => { + errorCallback: response => { + let errorMessage = ''; + if (response && response.data && response.data.message) { + errorMessage = response.data.message; + } commit(types.SET_LOADING, false); - createFlash(__('Failed to load errors from Sentry')); + createFlash( + sprintf(__(`Failed to load errors from Sentry. Error message: %{errorMessage}`), { + errorMessage, + }), + ); }, }); 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 fba31f16d65..5090b0bdc3c 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -163,7 +163,7 @@ export default class FilteredSearchVisualTokens { const tokenValueElement = tokenValueContainer.querySelector('.value'); tokenValueElement.innerText = tokenValue; - if (tokenValue === 'none' || tokenValue === 'any') { + if (['none', 'any'].includes(tokenValue.toLowerCase())) { return; } 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 5119dbf32eb..11d5d9639b6 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue @@ -44,7 +44,7 @@ export default { <div class="d-flex ide-commit-editor-header align-items-center"> <file-icon :file-name="activeFile.name" :size="16" class="mr-2" /> <strong class="mr-2"> {{ activeFile.path }} </strong> - <changed-file-icon :file="activeFile" class="ml-0" /> + <changed-file-icon :file="activeFile" :is-centered="false" /> <div class="ml-auto"> <button v-if="!isStaged" diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index 04ecd4ba4e7..c9c4e9e86f8 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -51,8 +51,11 @@ export default { return __('Create file'); }, - isCreatingNew() { - return this.entryModal.type !== modalTypes.rename; + isCreatingNewFile() { + return this.entryModal.type === 'blob'; + }, + placeholder() { + return this.isCreatingNewFile ? 'dir/file_name' : 'dir/'; }, }, methods: { @@ -107,9 +110,12 @@ export default { v-model="entryName" type="text" class="form-control qa-full-file-path" - placeholder="/dir/file_name" + :placeholder="placeholder" /> - <ul v-if="isCreatingNew" class="prepend-top-default list-inline qa-template-list"> + <ul + v-if="isCreatingNewFile" + class="file-templates prepend-top-default list-inline qa-template-list" + > <li v-for="(template, index) in templateTypes" :key="index" class="list-inline-item"> <button type="button" diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index 804ebae4555..7c560c89695 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -24,6 +24,22 @@ export const diffModes = { mode_changed: 'mode_changed', }; +export const diffViewerModes = Object.freeze({ + not_diffable: 'not_diffable', + no_preview: 'no_preview', + added: 'added', + deleted: 'deleted', + renamed: 'renamed', + mode_changed: 'mode_changed', + text: 'text', + image: 'image', +}); + +export const diffViewerErrors = Object.freeze({ + too_large: 'too_large', + stored_externally: 'server_side_but_stored_externally', +}); + export const rightSidebarViews = { pipelines: { name: 'pipelines-list', keepAlive: true }, jobsDetail: { name: 'jobs-detail', keepAlive: false }, diff --git a/app/assets/javascripts/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_projects/components/import_projects_table.vue new file mode 100644 index 00000000000..777f8fa6691 --- /dev/null +++ b/app/assets/javascripts/import_projects/components/import_projects_table.vue @@ -0,0 +1,101 @@ +<script> +import { mapActions, mapState, mapGetters } from 'vuex'; +import { GlLoadingIcon } from '@gitlab/ui'; +import LoadingButton from '~/vue_shared/components/loading_button.vue'; +import { __, sprintf } from '~/locale'; +import ImportedProjectTableRow from './imported_project_table_row.vue'; +import ProviderRepoTableRow from './provider_repo_table_row.vue'; +import eventHub from '../event_hub'; + +export default { + name: 'ImportProjectsTable', + components: { + ImportedProjectTableRow, + ProviderRepoTableRow, + LoadingButton, + GlLoadingIcon, + }, + props: { + providerTitle: { + type: String, + required: true, + }, + }, + + computed: { + ...mapState(['importedProjects', 'providerRepos', 'isLoadingRepos']), + ...mapGetters(['isImportingAnyRepo', 'hasProviderRepos', 'hasImportedProjects']), + + emptyStateText() { + return sprintf(__('No %{providerTitle} repositories available to import'), { + providerTitle: this.providerTitle, + }); + }, + + fromHeaderText() { + return sprintf(__('From %{providerTitle}'), { providerTitle: this.providerTitle }); + }, + }, + + mounted() { + return this.fetchRepos(); + }, + + beforeDestroy() { + this.stopJobsPolling(); + this.clearJobsEtagPoll(); + }, + + methods: { + ...mapActions(['fetchRepos', 'fetchJobs', 'stopJobsPolling', 'clearJobsEtagPoll']), + + importAll() { + eventHub.$emit('importAll'); + }, + }, +}; +</script> + +<template> + <div> + <div class="d-flex justify-content-between align-items-end flex-wrap mb-3"> + <p class="light text-nowrap mt-2 my-sm-0"> + {{ s__('ImportProjects|Select the projects you want to import') }} + </p> + <loading-button + container-class="btn btn-success js-import-all" + :loading="isImportingAnyRepo" + :label="__('Import all repositories')" + :disabled="!hasProviderRepos" + type="button" + @click="importAll" + /> + </div> + <gl-loading-icon + v-if="isLoadingRepos" + class="js-loading-button-icon import-projects-loading-icon" + :size="4" + /> + <div v-else-if="hasProviderRepos || hasImportedProjects" class="table-responsive"> + <table class="table import-table"> + <thead> + <th class="import-jobs-from-col">{{ fromHeaderText }}</th> + <th class="import-jobs-to-col">{{ __('To GitLab') }}</th> + <th class="import-jobs-status-col">{{ __('Status') }}</th> + <th class="import-jobs-cta-col"></th> + </thead> + <tbody> + <imported-project-table-row + v-for="project in importedProjects" + :key="project.id" + :project="project" + /> + <provider-repo-table-row v-for="repo in providerRepos" :key="repo.id" :repo="repo" /> + </tbody> + </table> + </div> + <div v-else class="text-center"> + <strong>{{ emptyStateText }}</strong> + </div> + </div> +</template> diff --git a/app/assets/javascripts/import_projects/components/import_status.vue b/app/assets/javascripts/import_projects/components/import_status.vue new file mode 100644 index 00000000000..9e3347a657f --- /dev/null +++ b/app/assets/javascripts/import_projects/components/import_status.vue @@ -0,0 +1,47 @@ +<script> +import { GlLoadingIcon } from '@gitlab/ui'; +import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import STATUS_MAP from '../constants'; + +export default { + name: 'ImportStatus', + components: { + CiIcon, + GlLoadingIcon, + }, + props: { + status: { + type: String, + required: true, + }, + }, + + computed: { + mappedStatus() { + return STATUS_MAP[this.status]; + }, + + ciIconStatus() { + const { icon } = this.mappedStatus; + + return { + icon: `status_${icon}`, + group: icon, + }; + }, + }, +}; +</script> + +<template> + <div> + <gl-loading-icon + v-if="mappedStatus.loadingIcon" + :inline="true" + :class="mappedStatus.textClass" + class="align-middle mr-2" + /> + <ci-icon v-else css-classes="align-middle mr-2" :status="ciIconStatus" /> + <span :class="mappedStatus.textClass">{{ mappedStatus.text }}</span> + </div> +</template> diff --git a/app/assets/javascripts/import_projects/components/imported_project_table_row.vue b/app/assets/javascripts/import_projects/components/imported_project_table_row.vue new file mode 100644 index 00000000000..ab2bd87ee9f --- /dev/null +++ b/app/assets/javascripts/import_projects/components/imported_project_table_row.vue @@ -0,0 +1,55 @@ +<script> +import ImportStatus from './import_status.vue'; +import { STATUSES } from '../constants'; + +export default { + name: 'ImportedProjectTableRow', + components: { + ImportStatus, + }, + props: { + project: { + type: Object, + required: true, + }, + }, + + computed: { + displayFullPath() { + return this.project.fullPath.replace(/^\//, ''); + }, + + isFinished() { + return this.project.importStatus === STATUSES.FINISHED; + }, + }, +}; +</script> + +<template> + <tr class="js-imported-project import-row"> + <td> + <a + :href="project.providerLink" + rel="noreferrer noopener" + target="_blank" + class="js-provider-link" + > + {{ project.importSource }} + </a> + </td> + <td class="js-full-path">{{ displayFullPath }}</td> + <td><import-status :status="project.importStatus" /></td> + <td> + <a + v-if="isFinished" + class="btn btn-default js-go-to-project" + :href="project.fullPath" + rel="noreferrer noopener" + target="_blank" + > + {{ __('Go to project') }} + </a> + </td> + </tr> +</template> diff --git a/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue b/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue new file mode 100644 index 00000000000..7cc29fa1b91 --- /dev/null +++ b/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue @@ -0,0 +1,110 @@ +<script> +import { mapState, mapGetters, mapActions } from 'vuex'; +import Select2Select from '~/vue_shared/components/select2_select.vue'; +import { __ } from '~/locale'; +import LoadingButton from '~/vue_shared/components/loading_button.vue'; +import eventHub from '../event_hub'; +import { STATUSES } from '../constants'; +import ImportStatus from './import_status.vue'; + +export default { + name: 'ProviderRepoTableRow', + components: { + Select2Select, + LoadingButton, + ImportStatus, + }, + props: { + repo: { + type: Object, + required: true, + }, + }, + + data() { + return { + targetNamespace: this.$store.state.defaultTargetNamespace, + newName: this.repo.sanitizedName, + }; + }, + + computed: { + ...mapState(['namespaces', 'reposBeingImported', 'ciCdOnly']), + + ...mapGetters(['namespaceSelectOptions']), + + importButtonText() { + return this.ciCdOnly ? __('Connect') : __('Import'); + }, + + select2Options() { + return { + data: this.namespaceSelectOptions, + containerCssClass: + 'import-namespace-select js-namespace-select qa-project-namespace-select', + }; + }, + + isLoadingImport() { + return this.reposBeingImported.includes(this.repo.id); + }, + + status() { + return this.isLoadingImport ? STATUSES.SCHEDULING : STATUSES.NONE; + }, + }, + + created() { + eventHub.$on('importAll', () => this.importRepo()); + }, + + methods: { + ...mapActions(['fetchImport']), + + importRepo() { + return this.fetchImport({ + newName: this.newName, + targetNamespace: this.targetNamespace, + repo: this.repo, + }); + }, + }, +}; +</script> + +<template> + <tr class="qa-project-import-row js-provider-repo import-row"> + <td> + <a + :href="repo.providerLink" + rel="noreferrer noopener" + target="_blank" + class="js-provider-link" + > + {{ repo.fullName }} + </a> + </td> + <td class="d-flex flex-wrap flex-lg-nowrap"> + <select2-select v-model="targetNamespace" :options="select2Options" /> + <span class="px-2 import-slash-divider d-flex justify-content-center align-items-center" + >/</span + > + <input + v-model="newName" + type="text" + class="form-control import-project-name-input js-new-name qa-project-path-field" + /> + </td> + <td><import-status :status="status" /></td> + <td> + <button + v-if="!isLoadingImport" + type="button" + class="qa-import-button js-import-button btn btn-default" + @click="importRepo" + > + {{ importButtonText }} + </button> + </td> + </tr> +</template> diff --git a/app/assets/javascripts/import_projects/constants.js b/app/assets/javascripts/import_projects/constants.js new file mode 100644 index 00000000000..ad33ca158d2 --- /dev/null +++ b/app/assets/javascripts/import_projects/constants.js @@ -0,0 +1,48 @@ +import { __ } from '../locale'; + +// The `scheduling` status is only present on the client-side, +// it is used as the status when we are requesting to start an import. + +export const STATUSES = { + FINISHED: 'finished', + FAILED: 'failed', + SCHEDULED: 'scheduled', + STARTED: 'started', + NONE: 'none', + SCHEDULING: 'scheduling', +}; + +const STATUS_MAP = { + [STATUSES.FINISHED]: { + icon: 'success', + text: __('Done'), + textClass: 'text-success', + }, + [STATUSES.FAILED]: { + icon: 'failed', + text: __('Failed'), + textClass: 'text-danger', + }, + [STATUSES.SCHEDULED]: { + icon: 'pending', + text: __('Scheduled'), + textClass: 'text-warning', + }, + [STATUSES.STARTED]: { + icon: 'running', + text: __('Running…'), + textClass: 'text-info', + }, + [STATUSES.NONE]: { + icon: 'created', + text: __('Not started'), + textClass: 'text-muted', + }, + [STATUSES.SCHEDULING]: { + loadingIcon: true, + text: __('Scheduling'), + textClass: 'text-warning', + }, +}; + +export default STATUS_MAP; diff --git a/app/assets/javascripts/import_projects/event_hub.js b/app/assets/javascripts/import_projects/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/import_projects/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/import_projects/index.js b/app/assets/javascripts/import_projects/index.js new file mode 100644 index 00000000000..5c77484aee1 --- /dev/null +++ b/app/assets/javascripts/import_projects/index.js @@ -0,0 +1,47 @@ +import Vue from 'vue'; +import { mapActions } from 'vuex'; +import Translate from '../vue_shared/translate'; +import ImportProjectsTable from './components/import_projects_table.vue'; +import { parseBoolean } from '../lib/utils/common_utils'; +import store from './store'; + +Vue.use(Translate); + +export default function mountImportProjectsTable(mountElement) { + if (!mountElement) return undefined; + + const { + reposPath, + provider, + providerTitle, + canSelectNamespace, + jobsPath, + importPath, + ciCdOnly, + } = mountElement.dataset; + + return new Vue({ + el: mountElement, + store, + + created() { + this.setInitialData({ + reposPath, + provider, + jobsPath, + importPath, + defaultTargetNamespace: gon.current_username, + ciCdOnly: parseBoolean(ciCdOnly), + canSelectNamespace: parseBoolean(canSelectNamespace), + }); + }, + + methods: { + ...mapActions(['setInitialData']), + }, + + render(createElement) { + return createElement(ImportProjectsTable, { props: { providerTitle } }); + }, + }); +} diff --git a/app/assets/javascripts/import_projects/store/actions.js b/app/assets/javascripts/import_projects/store/actions.js new file mode 100644 index 00000000000..c44500937cc --- /dev/null +++ b/app/assets/javascripts/import_projects/store/actions.js @@ -0,0 +1,106 @@ +import Visibility from 'visibilityjs'; +import * as types from './mutation_types'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import Poll from '~/lib/utils/poll'; +import createFlash from '~/flash'; +import { s__, sprintf } from '~/locale'; +import axios from '~/lib/utils/axios_utils'; + +let eTagPoll; + +export const clearJobsEtagPoll = () => { + eTagPoll = null; +}; +export const stopJobsPolling = () => { + if (eTagPoll) eTagPoll.stop(); +}; +export const restartJobsPolling = () => { + if (eTagPoll) eTagPoll.restart(); +}; + +export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data); + +export const requestRepos = ({ commit }, repos) => commit(types.REQUEST_REPOS, repos); +export const receiveReposSuccess = ({ commit }, repos) => + commit(types.RECEIVE_REPOS_SUCCESS, repos); +export const receiveReposError = ({ commit }) => commit(types.RECEIVE_REPOS_ERROR); +export const fetchRepos = ({ state, dispatch }) => { + dispatch('requestRepos'); + + return axios + .get(state.reposPath) + .then(({ data }) => + dispatch('receiveReposSuccess', convertObjectPropsToCamelCase(data, { deep: true })), + ) + .then(() => dispatch('fetchJobs')) + .catch(() => { + createFlash( + sprintf(s__('ImportProjects|Requesting your %{provider} repositories failed'), { + provider: state.provider, + }), + ); + + dispatch('receiveReposError'); + }); +}; + +export const requestImport = ({ commit, state }, repoId) => { + if (!state.reposBeingImported.includes(repoId)) commit(types.REQUEST_IMPORT, repoId); +}; +export const receiveImportSuccess = ({ commit }, { importedProject, repoId }) => + commit(types.RECEIVE_IMPORT_SUCCESS, { importedProject, repoId }); +export const receiveImportError = ({ commit }, repoId) => + commit(types.RECEIVE_IMPORT_ERROR, repoId); +export const fetchImport = ({ state, dispatch }, { newName, targetNamespace, repo }) => { + dispatch('requestImport', repo.id); + + return axios + .post(state.importPath, { + ci_cd_only: state.ciCdOnly, + new_name: newName, + repo_id: repo.id, + target_namespace: targetNamespace, + }) + .then(({ data }) => + dispatch('receiveImportSuccess', { + importedProject: convertObjectPropsToCamelCase(data, { deep: true }), + repoId: repo.id, + }), + ) + .catch(() => { + createFlash(s__('ImportProjects|Importing the project failed')); + + dispatch('receiveImportError', { repoId: repo.id }); + }); +}; + +export const receiveJobsSuccess = ({ commit }, updatedProjects) => + commit(types.RECEIVE_JOBS_SUCCESS, updatedProjects); +export const fetchJobs = ({ state, dispatch }) => { + if (eTagPoll) return; + + eTagPoll = new Poll({ + resource: { + fetchJobs: () => axios.get(state.jobsPath), + }, + method: 'fetchJobs', + successCallback: ({ data }) => + dispatch('receiveJobsSuccess', convertObjectPropsToCamelCase(data, { deep: true })), + errorCallback: () => createFlash(s__('ImportProjects|Updating the imported projects failed')), + }); + + if (!Visibility.hidden()) { + eTagPoll.makeRequest(); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + dispatch('restartJobsPolling'); + } else { + dispatch('stopJobsPolling'); + } + }); +}; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/import_projects/store/getters.js b/app/assets/javascripts/import_projects/store/getters.js new file mode 100644 index 00000000000..f03474a8404 --- /dev/null +++ b/app/assets/javascripts/import_projects/store/getters.js @@ -0,0 +1,20 @@ +export const namespaceSelectOptions = state => { + const serializedNamespaces = state.namespaces.map(({ fullPath }) => ({ + id: fullPath, + text: fullPath, + })); + + return [ + { text: 'Groups', children: serializedNamespaces }, + { + text: 'Users', + children: [{ id: state.defaultTargetNamespace, text: state.defaultTargetNamespace }], + }, + ]; +}; + +export const isImportingAnyRepo = state => state.reposBeingImported.length > 0; + +export const hasProviderRepos = state => state.providerRepos.length > 0; + +export const hasImportedProjects = state => state.importedProjects.length > 0; diff --git a/app/assets/javascripts/import_projects/store/index.js b/app/assets/javascripts/import_projects/store/index.js new file mode 100644 index 00000000000..6ac9bfd8189 --- /dev/null +++ b/app/assets/javascripts/import_projects/store/index.js @@ -0,0 +1,15 @@ +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); + +export default new Vuex.Store({ + state: state(), + actions, + mutations, + getters, +}); diff --git a/app/assets/javascripts/import_projects/store/mutation_types.js b/app/assets/javascripts/import_projects/store/mutation_types.js new file mode 100644 index 00000000000..6ba3fd6f29e --- /dev/null +++ b/app/assets/javascripts/import_projects/store/mutation_types.js @@ -0,0 +1,11 @@ +export const SET_INITIAL_DATA = 'SET_INITIAL_DATA'; + +export const REQUEST_REPOS = 'REQUEST_REPOS'; +export const RECEIVE_REPOS_SUCCESS = 'RECEIVE_REPOS_SUCCESS'; +export const RECEIVE_REPOS_ERROR = 'RECEIVE_REPOS_ERROR'; + +export const REQUEST_IMPORT = 'REQUEST_IMPORT'; +export const RECEIVE_IMPORT_SUCCESS = 'RECEIVE_IMPORT_SUCCESS'; +export const RECEIVE_IMPORT_ERROR = 'RECEIVE_IMPORT_ERROR'; + +export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS'; diff --git a/app/assets/javascripts/import_projects/store/mutations.js b/app/assets/javascripts/import_projects/store/mutations.js new file mode 100644 index 00000000000..b88de0268e7 --- /dev/null +++ b/app/assets/javascripts/import_projects/store/mutations.js @@ -0,0 +1,55 @@ +import Vue from 'vue'; +import * as types from './mutation_types'; + +export default { + [types.SET_INITIAL_DATA](state, data) { + Object.assign(state, data); + }, + + [types.REQUEST_REPOS](state) { + state.isLoadingRepos = true; + }, + + [types.RECEIVE_REPOS_SUCCESS](state, { importedProjects, providerRepos, namespaces }) { + state.isLoadingRepos = false; + + state.importedProjects = importedProjects; + state.providerRepos = providerRepos; + state.namespaces = namespaces; + }, + + [types.RECEIVE_REPOS_ERROR](state) { + state.isLoadingRepos = false; + }, + + [types.REQUEST_IMPORT](state, repoId) { + state.reposBeingImported.push(repoId); + }, + + [types.RECEIVE_IMPORT_SUCCESS](state, { importedProject, repoId }) { + const existingRepoIndex = state.reposBeingImported.indexOf(repoId); + if (state.reposBeingImported.includes(repoId)) + state.reposBeingImported.splice(existingRepoIndex, 1); + + const providerRepoIndex = state.providerRepos.findIndex( + providerRepo => providerRepo.id === repoId, + ); + state.providerRepos.splice(providerRepoIndex, 1); + state.importedProjects.unshift(importedProject); + }, + + [types.RECEIVE_IMPORT_ERROR](state, repoId) { + const repoIndex = state.reposBeingImported.indexOf(repoId); + if (state.reposBeingImported.includes(repoId)) state.reposBeingImported.splice(repoIndex, 1); + }, + + [types.RECEIVE_JOBS_SUCCESS](state, updatedProjects) { + updatedProjects.forEach(updatedProject => { + const existingProject = state.importedProjects.find( + importedProject => importedProject.id === updatedProject.id, + ); + + Vue.set(existingProject, 'importStatus', updatedProject.importStatus); + }); + }, +}; diff --git a/app/assets/javascripts/import_projects/store/state.js b/app/assets/javascripts/import_projects/store/state.js new file mode 100644 index 00000000000..637fef6e53c --- /dev/null +++ b/app/assets/javascripts/import_projects/store/state.js @@ -0,0 +1,15 @@ +export default () => ({ + reposPath: '', + importPath: '', + jobsPath: '', + currentProjectId: '', + provider: '', + currentUsername: '', + importedProjects: [], + providerRepos: [], + namespaces: [], + reposBeingImported: [], + isLoadingRepos: false, + canSelectNamespace: false, + ciCdOnly: false, +}); diff --git a/app/assets/javascripts/issuable_suggestions/index.js b/app/assets/javascripts/issuable_suggestions/index.js index 2c80cf1797a..40916c9d27f 100644 --- a/app/assets/javascripts/issuable_suggestions/index.js +++ b/app/assets/javascripts/issuable_suggestions/index.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import defaultClient from '~/lib/graphql'; +import createDefaultClient from '~/lib/graphql'; import App from './components/app.vue'; Vue.use(VueApollo); @@ -10,7 +10,7 @@ export default function() { const issueTitle = document.getElementById('issue_title'); const { projectPath } = el.dataset; const apolloProvider = new VueApollo({ - defaultClient, + defaultClient: createDefaultClient(), }); return new Vue({ diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue index a2141dc3760..1691ac62100 100644 --- a/app/assets/javascripts/jobs/components/sidebar.vue +++ b/app/assets/javascripts/jobs/components/sidebar.vue @@ -110,7 +110,7 @@ export default { <div class="sidebar-container"> <div class="blocks-container"> <div class="block d-flex flex-nowrap align-items-center"> - <h4 class="my-0 mr-2">{{ job.name }}</h4> + <h4 class="my-0 mr-2 text-break-word">{{ job.name }}</h4> <div class="flex-grow-1 flex-shrink-0 text-right"> <gl-link v-if="job.retry_path" diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js index 20a0f142d9e..64e4e899f44 100644 --- a/app/assets/javascripts/lib/graphql.js +++ b/app/assets/javascripts/lib/graphql.js @@ -1,9 +1,11 @@ import ApolloClient from 'apollo-boost'; import csrf from '~/lib/utils/csrf'; -export default new ApolloClient({ - uri: `${gon.relative_url_root}/api/graphql`, - headers: { - [csrf.headerKey]: csrf.token, - }, -}); +export default (clientState = {}) => + new ApolloClient({ + uri: `${gon.relative_url_root}/api/graphql`, + headers: { + [csrf.headerKey]: csrf.token, + }, + clientState, + }); diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 0ceff10a02a..a73cdb73690 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -130,7 +130,7 @@ export const isInViewport = (el, offset = {}) => { rect.top >= (top || 0) && rect.left >= (left || 0) && rect.bottom <= window.innerHeight && - rect.right <= window.innerWidth + parseInt(rect.right, 10) <= window.innerWidth ); }; @@ -456,21 +456,6 @@ export const historyPushState = newUrl => { export const parseBoolean = value => (value && value.toString()) === 'true'; /** - * Converts permission provided as strings to booleans. - * - * @param {String} string - * @returns {Boolean} - */ -export const convertPermissionToBoolean = permission => { - if (process.env.NODE_ENV !== 'production') { - // eslint-disable-next-line no-console - console.warn('convertPermissionToBoolean is deprecated! Please use parseBoolean instead.'); - } - - return parseBoolean(permission); -}; - -/** * @callback backOffCallback * @param {Function} next * @param {Function} stop diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js index 198711cf427..a900ff34bf5 100644 --- a/app/assets/javascripts/lib/utils/poll.js +++ b/app/assets/javascripts/lib/utils/poll.js @@ -63,6 +63,10 @@ export default class Poll { const headers = normalizeHeaders(response.headers); const pollInterval = parseInt(headers[this.intervalHeader], 10); if (pollInterval > 0 && successCodes.indexOf(response.status) !== -1 && this.canPoll) { + if (this.timeoutID) { + clearTimeout(this.timeoutID); + } + this.timeoutID = setTimeout(() => { this.makeRequest(); }, pollInterval); @@ -101,15 +105,25 @@ export default class Poll { } /** - * Restarts polling after it has been stoped + * Enables polling after it has been stopped */ - restart(options) { - // update data + enable(options) { if (options && options.data) { this.options.data = options.data; } this.canPoll = true; + + if (options && options.response) { + this.checkConditions(options.response); + } + } + + /** + * Restarts polling after it has been stopped and makes a request + */ + restart(options) { + this.enable(options); this.makeRequest(); } } diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 63db4938cd7..1b722c0505a 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -78,7 +78,6 @@ function deferredInitialisation() { initUserPopovers(); if (document.querySelector('.search')) initSearchAutocomplete(); - if (document.querySelector('#js-peek')) initPerformanceBar({ container: '#js-peek' }); addSelectOnFocusBehaviour('.js-select-on-focus'); @@ -145,6 +144,8 @@ document.addEventListener('DOMContentLoaded', () => { const $sidebarGutterToggle = $('.js-sidebar-toggle'); let bootstrapBreakpoint = bp.getBreakpointSize(); + if (document.querySelector('#js-peek')) initPerformanceBar({ container: '#js-peek' }); + initLayoutNav(); // Set the default path for all cookies to GitLab's root directory diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue index 14c02db7bcc..9e031b03579 100644 --- a/app/assets/javascripts/monitoring/components/charts/area.vue +++ b/app/assets/javascripts/monitoring/components/charts/area.vue @@ -139,8 +139,7 @@ export default { return this.graphData.queries.map(query => query.label).join(', '); }, yAxisLabel() { - const [query] = this.graphData.queries; - return `${this.graphData.y_label} (${query.unit})`; + return `${this.graphData.y_label}`; }, }, watch: { diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index 376d4114efd..d8947e8ca50 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -5,6 +5,7 @@ import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue'; import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue'; import { GlSkeletonLoading } from '@gitlab/ui'; import { getDiffMode } from '~/diffs/store/utils'; +import { diffViewerModes } from '~/ide/constants'; export default { components: { @@ -31,6 +32,12 @@ export default { diffMode() { return getDiffMode(this.discussion.diff_file); }, + diffViewerMode() { + return this.discussion.diff_file.viewer.name; + }, + isTextFile() { + return this.diffViewerMode === diffViewerModes.text; + }, hasTruncatedDiffLines() { return ( this.discussion.truncated_diff_lines && this.discussion.truncated_diff_lines.length !== 0 @@ -58,18 +65,14 @@ export default { </script> <template> - <div :class="{ 'text-file': discussion.diff_file.text }" class="diff-file file-holder"> + <div :class="{ 'text-file': isTextFile }" class="diff-file file-holder"> <diff-file-header :discussion-path="discussion.discussion_path" :diff-file="discussion.diff_file" :can-current-user-fork="false" - :expanded="!discussion.diff_file.collapsed" + :expanded="!discussion.diff_file.viewer.collapsed" /> - <div - v-if="discussion.diff_file.text" - :class="$options.userColorSchemeClass" - class="diff-content code" - > + <div v-if="isTextFile" :class="$options.userColorSchemeClass" class="diff-content code"> <table> <template v-if="hasTruncatedDiffLines"> <tr @@ -109,6 +112,7 @@ export default { <div v-else> <diff-viewer :diff-mode="diffMode" + :diff-viewer-mode="diffViewerMode" :new-path="discussion.diff_file.new_path" :new-sha="discussion.diff_file.diff_refs.head_sha" :old-path="discussion.diff_file.old_path" diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index 969d5b69c25..de1ea0f58d6 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -23,11 +23,6 @@ export default { type: [String, Number], required: true, }, - discussionId: { - type: String, - required: false, - default: '', - }, noteUrl: { type: String, required: false, @@ -176,7 +171,7 @@ export default { v-if="showReplyButton" ref="replyButton" class="js-reply-button" - :note-id="discussionId" + @startReplying="$emit('startReplying')" /> <div v-if="canEdit" class="note-actions-item"> <button diff --git a/app/assets/javascripts/notes/components/note_actions/reply_button.vue b/app/assets/javascripts/notes/components/note_actions/reply_button.vue index b2f9d7f128a..f50cab81efe 100644 --- a/app/assets/javascripts/notes/components/note_actions/reply_button.vue +++ b/app/assets/javascripts/notes/components/note_actions/reply_button.vue @@ -1,5 +1,4 @@ <script> -import { mapActions } from 'vuex'; import { GlTooltipDirective, GlButton } from '@gitlab/ui'; import Icon from '~/vue_shared/components/icon.vue'; @@ -12,15 +11,6 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, - props: { - noteId: { - type: String, - required: true, - }, - }, - methods: { - ...mapActions(['convertToDiscussion']), - }, }; </script> @@ -32,7 +22,7 @@ export default { class="note-action-button" variant="transparent" :title="__('Reply to comment')" - @click="convertToDiscussion(noteId)" + @click="$emit('startReplying')" > <icon name="comment" css-classes="link-highlight" /> </gl-button> diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue index ff303d0f55a..fb1d98355b3 100644 --- a/app/assets/javascripts/notes/components/note_body.vue +++ b/app/assets/javascripts/notes/components/note_body.vue @@ -95,6 +95,7 @@ export default { <div ref="note-body" :class="{ 'js-task-list-container': canEdit }" class="note-body"> <suggestions v-if="hasSuggestion && !isEditing" + class="note-text md" :suggestions="note.suggestions" :note-html="note.note_html" :line-type="lineType" diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index b7e9f7c2028..2d6fd8b116f 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -26,6 +26,7 @@ import resolvable from '../mixins/resolvable'; import discussionNavigation from '../mixins/discussion_navigation'; import ReplyPlaceholder from './discussion_reply_placeholder.vue'; import jumpToNextDiscussionButton from './discussion_jump_to_next_button.vue'; +import eventHub from '../event_hub'; export default { name: 'NoteableDiscussion', @@ -93,6 +94,7 @@ export default { }, computed: { ...mapGetters([ + 'convertedDisscussionIds', 'getNoteableData', 'nextUnresolvedDiscussionId', 'unresolvedDiscussionsCount', @@ -245,6 +247,12 @@ export default { } }, }, + created() { + eventHub.$on('startReplying', this.onStartReplying); + }, + beforeDestroy() { + eventHub.$off('startReplying', this.onStartReplying); + }, methods: { ...mapActions([ 'saveNote', @@ -252,6 +260,7 @@ export default { 'removePlaceholderNotes', 'toggleResolveNote', 'expandDiscussion', + 'removeConvertedDiscussion', ]), truncateSha, componentName(note) { @@ -291,6 +300,10 @@ export default { } } + if (this.convertedDisscussionIds.includes(this.discussion.id)) { + this.removeConvertedDiscussion(this.discussion.id); + } + this.isReplying = false; this.resetAutoSave(); }, @@ -301,6 +314,10 @@ export default { note: { note: noteText }, }; + if (this.convertedDisscussionIds.includes(this.discussion.id)) { + postData.return_discussion = true; + } + if (this.discussion.for_commit) { postData.note_project_id = this.discussion.project_id; } @@ -340,6 +357,11 @@ Please check your network connection and try again.`; deleteNoteHandler(note) { this.$emit('noteDeleted', this.discussion, note); }, + onStartReplying(discussionId) { + if (this.discussion.id === discussionId) { + this.showReplyForm(); + } + }, }, }; </script> @@ -358,30 +380,32 @@ Please check your network connection and try again.`; :img-size="40" /> </div> - <note-header - :author="author" - :created-at="initialDiscussion.created_at" - :note-id="initialDiscussion.id" - :include-toggle="true" - :expanded="discussion.expanded" - @toggleHandler="toggleDiscussionHandler" - > - <span v-html="actionText"></span> - </note-header> - <note-edited-text - v-if="discussion.resolved" - :edited-at="discussion.resolved_at" - :edited-by="discussion.resolved_by" - :action-text="resolvedText" - class-name="discussion-headline-light js-discussion-headline" - /> - <note-edited-text - v-else-if="lastUpdatedAt" - :edited-at="lastUpdatedAt" - :edited-by="lastUpdatedBy" - action-text="Last updated" - class-name="discussion-headline-light js-discussion-headline" - /> + <div class="timeline-content"> + <note-header + :author="author" + :created-at="initialDiscussion.created_at" + :note-id="initialDiscussion.id" + :include-toggle="true" + :expanded="discussion.expanded" + @toggleHandler="toggleDiscussionHandler" + > + <span v-html="actionText"></span> + </note-header> + <note-edited-text + v-if="discussion.resolved" + :edited-at="discussion.resolved_at" + :edited-by="discussion.resolved_by" + :action-text="resolvedText" + class-name="discussion-headline-light js-discussion-headline" + /> + <note-edited-text + v-else-if="lastUpdatedAt" + :edited-at="lastUpdatedAt" + :edited-by="lastUpdatedBy" + action-text="Last updated" + class-name="discussion-headline-light js-discussion-headline" + /> + </div> </div> <div v-if="shouldShowDiscussions" class="discussion-body"> <component @@ -400,6 +424,7 @@ Please check your network connection and try again.`; :help-page-path="helpPagePath" :show-reply-button="canReply" @handleDeleteNote="deleteNoteHandler" + @startReplying="showReplyForm" > <note-edited-text v-if="discussion.resolved" diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 56108a58010..04e74a43acc 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -29,11 +29,6 @@ export default { type: Object, required: true, }, - discussion: { - type: Object, - required: false, - default: null, - }, line: { type: Object, required: false, @@ -49,6 +44,11 @@ export default { required: false, default: () => null, }, + showReplyButton: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -91,13 +91,6 @@ export default { } return ''; }, - showReplyButton() { - if (!this.discussion || !this.getNoteableData.current_user.can_create_note) { - return false; - } - - return this.discussion.individual_note && !this.commentsDisabled; - }, actionText() { if (!this.commit) { return ''; @@ -260,10 +253,10 @@ export default { :is-resolved="note.resolved" :is-resolving="isResolving" :resolved-by="note.resolved_by" - :discussion-id="discussionId" @handleEdit="editHandler" @handleDelete="deleteHandler" @handleResolve="resolveHandler" + @startReplying="$emit('startReplying')" /> </div> <div class="timeline-discussion-body"> diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 6d72b72e628..8d3f6d902f8 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -60,9 +60,11 @@ export default { ...mapGetters([ 'isNotesFetched', 'discussions', + 'convertedDisscussionIds', 'getNotesDataByProp', 'isLoading', 'commentsDisabled', + 'getNoteableData', ]), noteableType() { return this.noteableData.noteableType; @@ -78,6 +80,9 @@ export default { return this.discussions; }, + canReply() { + return this.getNoteableData.current_user.can_create_note && !this.commentsDisabled; + }, }, watch: { shouldShow() { @@ -85,8 +90,15 @@ export default { this.fetchNotes(); } }, + allDiscussions() { + if (this.discussonsCount) { + this.discussonsCount.textContent = this.allDiscussions.length; + } + }, }, created() { + this.discussonsCount = document.querySelector('.js-discussions-count'); + this.setNotesData(this.notesData); this.setNoteableData(this.noteableData); this.setUserData(this.userData); @@ -128,6 +140,7 @@ export default { 'setNotesFetchedState', 'expandDiscussion', 'startTaskList', + 'convertToDiscussion', ]), fetchNotes() { if (this.isFetching) return null; @@ -175,6 +188,11 @@ export default { } } }, + startReplying(discussionId) { + return this.convertToDiscussion(discussionId) + .then(() => this.$nextTick()) + .then(() => eventHub.$emit('startReplying', discussionId)); + }, }, systemNote: constants.SYSTEM_NOTE, }; @@ -193,7 +211,9 @@ export default { /> <placeholder-note v-else :key="discussion.id" :note="discussion.notes[0]" /> </template> - <template v-else-if="discussion.individual_note"> + <template + v-else-if="discussion.individual_note && !convertedDisscussionIds.includes(discussion.id)" + > <system-note v-if="discussion.notes[0].system" :key="discussion.id" @@ -203,7 +223,8 @@ export default { v-else :key="discussion.id" :note="discussion.notes[0]" - :discussion="discussion" + :show-reply-button="canReply" + @startReplying="startReplying(discussion.id)" /> </template> <noteable-discussion diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index ff65f14d529..1a0dba69a7c 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -83,12 +83,44 @@ export const updateNote = ({ commit, dispatch }, { endpoint, note }) => dispatch('startTaskList'); }); -export const replyToDiscussion = ({ commit }, { endpoint, data }) => +export const updateOrCreateNotes = ({ commit, state, getters, dispatch }, notes) => { + const { notesById } = getters; + + notes.forEach(note => { + if (notesById[note.id]) { + commit(types.UPDATE_NOTE, note); + } else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) { + const discussion = utils.findNoteObjectById(state.discussions, note.discussion_id); + + if (discussion) { + commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note); + } else if (note.type === constants.DIFF_NOTE) { + dispatch('fetchDiscussions', { path: state.notesData.discussionsPath }); + } else { + commit(types.ADD_NEW_NOTE, note); + } + } else { + commit(types.ADD_NEW_NOTE, note); + } + }); +}; + +export const replyToDiscussion = ({ commit, state, getters, dispatch }, { endpoint, data }) => service .replyToDiscussion(endpoint, data) .then(res => res.json()) .then(res => { - commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res); + if (res.discussion) { + commit(types.UPDATE_DISCUSSION, res.discussion); + + updateOrCreateNotes({ commit, state, getters, dispatch }, res.discussion.notes); + + dispatch('updateMergeRequestWidget'); + dispatch('startTaskList'); + dispatch('updateResolvableDiscussonsCounts'); + } else { + commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res); + } return res; }); @@ -262,25 +294,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => { if (resp.notes && resp.notes.length) { - const { notesById } = getters; - - resp.notes.forEach(note => { - if (notesById[note.id]) { - commit(types.UPDATE_NOTE, note); - } else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) { - const discussion = utils.findNoteObjectById(state.discussions, note.discussion_id); - - if (discussion) { - commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note); - } else if (note.type === constants.DIFF_NOTE) { - dispatch('fetchDiscussions', { path: state.notesData.discussionsPath }); - } else { - commit(types.ADD_NEW_NOTE, note); - } - } else { - commit(types.ADD_NEW_NOTE, note); - } - }); + updateOrCreateNotes({ commit, state, getters, dispatch }, resp.notes); dispatch('startTaskList'); } @@ -429,5 +443,8 @@ export const submitSuggestion = ( export const convertToDiscussion = ({ commit }, noteId) => commit(types.CONVERT_TO_DISCUSSION, noteId); +export const removeConvertedDiscussion = ({ commit }, noteId) => + commit(types.REMOVE_CONVERTED_DISCUSSION, noteId); + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index 0ffc0cb2593..5026c13dab5 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -4,6 +4,8 @@ import { collapseSystemNotes } from './collapse_utils'; export const discussions = state => collapseSystemNotes(state.discussions); +export const convertedDisscussionIds = state => state.convertedDisscussionIds; + export const targetNoteHash = state => state.targetNoteHash; export const getNotesData = state => state.notesData; diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js index 887e6d22b06..6168aeae35d 100644 --- a/app/assets/javascripts/notes/stores/modules/index.js +++ b/app/assets/javascripts/notes/stores/modules/index.js @@ -5,6 +5,7 @@ import mutations from '../mutations'; export default () => ({ state: { discussions: [], + convertedDisscussionIds: [], targetNoteHash: null, lastFetchedAt: null, diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js index 2bffedad336..796370920bb 100644 --- a/app/assets/javascripts/notes/stores/mutation_types.js +++ b/app/assets/javascripts/notes/stores/mutation_types.js @@ -18,6 +18,7 @@ export const SET_NOTES_LOADING_STATE = 'SET_NOTES_LOADING_STATE'; export const DISABLE_COMMENTS = 'DISABLE_COMMENTS'; export const APPLY_SUGGESTION = 'APPLY_SUGGESTION'; export const CONVERT_TO_DISCUSSION = 'CONVERT_TO_DISCUSSION'; +export const REMOVE_CONVERTED_DISCUSSION = 'REMOVE_CONVERTED_DISCUSSION'; // DISCUSSION export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION'; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index d167f8ef421..ae6f8b7790a 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -266,7 +266,14 @@ export default { }, [types.CONVERT_TO_DISCUSSION](state, discussionId) { - const discussion = utils.findNoteObjectById(state.discussions, discussionId); - Object.assign(discussion, { individual_note: false }); + const convertedDisscussionIds = [...state.convertedDisscussionIds, discussionId]; + Object.assign(state, { convertedDisscussionIds }); + }, + + [types.REMOVE_CONVERTED_DISCUSSION](state, discussionId) { + const convertedDisscussionIds = [...state.convertedDisscussionIds]; + + convertedDisscussionIds.splice(convertedDisscussionIds.indexOf(discussionId), 1); + Object.assign(state, { convertedDisscussionIds }); }, }; diff --git a/app/assets/javascripts/pages/import/gitea/status/index.js b/app/assets/javascripts/pages/import/gitea/status/index.js new file mode 100644 index 00000000000..dcd84f0faf9 --- /dev/null +++ b/app/assets/javascripts/pages/import/gitea/status/index.js @@ -0,0 +1,7 @@ +import mountImportProjectsTable from '~/import_projects'; + +document.addEventListener('DOMContentLoaded', () => { + const mountElement = document.getElementById('import-projects-mount-element'); + + mountImportProjectsTable(mountElement); +}); diff --git a/app/assets/javascripts/pages/import/github/status/index.js b/app/assets/javascripts/pages/import/github/status/index.js new file mode 100644 index 00000000000..dcd84f0faf9 --- /dev/null +++ b/app/assets/javascripts/pages/import/github/status/index.js @@ -0,0 +1,7 @@ +import mountImportProjectsTable from '~/import_projects'; + +document.addEventListener('DOMContentLoaded', () => { + const mountElement = document.getElementById('import-projects-mount-element'); + + mountImportProjectsTable(mountElement); +}); diff --git a/app/assets/javascripts/pages/sessions/new/index.js b/app/assets/javascripts/pages/sessions/new/index.js index d54bff88f70..e1a3f42a71f 100644 --- a/app/assets/javascripts/pages/sessions/new/index.js +++ b/app/assets/javascripts/pages/sessions/new/index.js @@ -1,5 +1,6 @@ import $ from 'jquery'; import UsernameValidator from './username_validator'; +import NoEmojiValidator from '../../../emoji/no_emoji_validator'; import SigninTabsMemoizer from './signin_tabs_memoizer'; import OAuthRememberMe from './oauth_remember_me'; import preserveUrlFragment from './preserve_url_fragment'; @@ -7,6 +8,7 @@ import preserveUrlFragment from './preserve_url_fragment'; document.addEventListener('DOMContentLoaded', () => { new UsernameValidator(); // eslint-disable-line no-new new SigninTabsMemoizer(); // eslint-disable-line no-new + new NoEmojiValidator(); // eslint-disable-line no-new new OAuthRememberMe({ container: $('.omniauth-container'), diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js index 39cd891c111..636308c5401 100644 --- a/app/assets/javascripts/pages/users/user_tabs.js +++ b/app/assets/javascripts/pages/users/user_tabs.js @@ -221,7 +221,7 @@ export default class UserTabs { const monthsAgo = UserTabs.getVisibleCalendarPeriod($calendarWrap); const calendarActivitiesPath = $calendarWrap.data('calendarActivitiesPath'); const utcOffset = $calendarWrap.data('utcOffset'); - const calendarHint = __('Issues, merge requests, pushes and comments.'); + const calendarHint = __('Issues, merge requests, pushes, and comments.'); $calendarWrap.html(CALENDAR_TEMPLATE); diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue index 0152e2fbe04..244d332f38f 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -59,28 +59,30 @@ export default { </script> <template> <div class="btn-group"> - <gl-button + <button v-gl-tooltip + type="button" :disabled="isLoading" class="dropdown-new btn btn-default js-pipeline-dropdown-manual-actions" - title="Manual job" + :title="__('Manual job')" data-toggle="dropdown" - aria-label="Manual job" + :aria-label="__('Manual job')" > - <icon name="play" class="icon-play" /> <i class="fa fa-caret-down" aria-hidden="true"> </i> + <icon name="play" class="icon-play" /> + <i class="fa fa-caret-down" aria-hidden="true"></i> <gl-loading-icon v-if="isLoading" /> - </gl-button> + </button> <ul class="dropdown-menu dropdown-menu-right"> <li v-for="action in actions" :key="action.path"> <gl-button :class="{ disabled: isActionDisabled(action) }" :disabled="isActionDisabled(action)" - class="js-pipeline-action-link no-btn btn" + class="js-pipeline-action-link no-btn btn d-flex align-items-center justify-content-between flex-wrap" @click="onClickAction(action)" > {{ action.name }} - <span v-if="action.scheduled_at" class="pull-right"> + <span v-if="action.scheduled_at"> <icon name="clock" /> <gl-countdown :end-date-string="action.scheduled_at" /> </span> diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue index 908b10afee6..2ab0ad4d013 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue @@ -1,5 +1,5 @@ <script> -import { GlLink, GlButton, GlTooltipDirective } from '@gitlab/ui'; +import { GlLink, GlTooltipDirective } from '@gitlab/ui'; import Icon from '~/vue_shared/components/icon.vue'; export default { @@ -9,7 +9,6 @@ export default { components: { Icon, GlLink, - GlButton, }, props: { artifacts: { @@ -21,20 +20,22 @@ export default { </script> <template> <div class="btn-group" role="group"> - <gl-button + <button v-gl-tooltip - class="dropdown-toggle build-artifacts js-pipeline-dropdown-download" - title="Artifacts" + type="button" + class="dropdown-toggle build-artifacts btn btn-default js-pipeline-dropdown-download" + :title="__('Artifacts')" data-toggle="dropdown" - aria-label="Artifacts" + :aria-label="__('Artifacts')" > - <icon name="download" /> <i class="fa fa-caret-down" aria-hidden="true"> </i> - </gl-button> + <icon name="download" /> + <i class="fa fa-caret-down" aria-hidden="true"></i> + </button> <ul class="dropdown-menu dropdown-menu-right"> <li v-for="(artifact, i) in artifacts" :key="i"> - <gl-link :href="artifact.path" rel="nofollow" download> - Download {{ artifact.name }} artifacts - </gl-link> + <gl-link :href="artifact.path" rel="nofollow" download + >Download {{ artifact.name }} artifacts</gl-link + > </li> </ul> </div> diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js index 32bfa47e5f2..74ca3071364 100644 --- a/app/assets/javascripts/pipelines/mixins/pipelines.js +++ b/app/assets/javascripts/pipelines/mixins/pipelines.js @@ -94,8 +94,7 @@ export default { this.isLoading = false; this.successCallback(response); - // restart polling - this.poll.restart({ data: this.requestData }); + this.poll.enable({ data: this.requestData, response }); }) .catch(() => { this.isLoading = false; diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index d65e73a3f9c..f021698a7ea 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -145,6 +145,26 @@ const bindEvents = () => { text: 'Pages/Hexo', icon: '.template-option .icon-hexo', }, + nfhugo: { + text: 'Netlify/Hugo', + icon: '.template-option .icon-netlify', + }, + nfjekyll: { + text: 'Netlify/Jekyll', + icon: '.template-option .icon-netlify', + }, + nfplainhtml: { + text: 'Netlify/Plain HTML', + icon: '.template-option .icon-netlify', + }, + nfgitbook: { + text: 'Netlify/GitBook', + icon: '.template-option .icon-netlify', + }, + nfhexo: { + text: 'Netlify/Hexo', + icon: '.template-option .icon-netlify', + }, }; const selectedTemplate = templates[value]; diff --git a/app/assets/javascripts/releases/store/actions.js b/app/assets/javascripts/releases/store/actions.js index baa2251403e..b5c4d54ac33 100644 --- a/app/assets/javascripts/releases/store/actions.js +++ b/app/assets/javascripts/releases/store/actions.js @@ -11,7 +11,7 @@ export const requestReleases = ({ commit }) => commit(types.REQUEST_RELEASES); /** * Fetches the main endpoint. * Will dispatch requestNamespace action before starting the request. - * Will dispatch receiveNamespaceSuccess if the request is successfull + * Will dispatch receiveNamespaceSuccess if the request is successful * Will dispatch receiveNamesapceError if the request returns an error * * @param {String} projectId diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue index a1d3a09cca4..33963d5e1e6 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue @@ -73,14 +73,14 @@ export default { <gl-button :aria-label="ariaLabel" variant="blank" - class="commit-edit-toggle mr-2" + class="commit-edit-toggle square s24 mr-2" @click.stop="toggle()" > <icon :name="collapseIcon" :size="16" /> </gl-button> <span v-if="expanded">{{ __('Collapse') }}</span> <span v-else> - <span v-html="message"></span> + <span class="vertical-align-middle" v-html="message"></span> <gl-button variant="link" class="modify-message-button"> {{ modifyLinkMessage }} </gl-button> diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index abbbe19c5ef..57c4dfbe3b7 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -315,7 +315,7 @@ export default { :endpoint="mr.testResultsPath" /> - <div class="mr-widget-section p-0"> + <div class="mr-widget-section"> <component :is="componentName" :mr="mr" :service="service" /> <section v-if="shouldRenderCollaborationStatus" class="mr-info-list mr-links"> diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue index bb7710f708e..e9ab6f5ba7a 100644 --- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue +++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue @@ -37,6 +37,11 @@ export default { required: false, default: 12, }, + isCentered: { + type: Boolean, + required: false, + default: true, + }, }, computed: { changedIcon() { @@ -78,7 +83,12 @@ export default { </script> <template> - <span v-gl-tooltip.right :title="tooltipTitle" class="file-changed-icon ml-auto"> + <span + v-gl-tooltip.right + :title="tooltipTitle" + :class="{ 'ml-auto': isCentered }" + class="file-changed-icon" + > <icon v-if="showIcon" :name="changedIcon" :size="size" :css-classes="changedIconClass" /> </span> </template> diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue index b8eb555106f..2f498c4fa2a 100644 --- a/app/assets/javascripts/vue_shared/components/ci_icon.vue +++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue @@ -46,6 +46,11 @@ export default { required: false, default: false, }, + cssClasses: { + type: String, + required: false, + default: '', + }, }, computed: { cssClass() { @@ -59,5 +64,5 @@ export default { }; </script> <template> - <span :class="cssClass"> <icon :name="icon" :size="size" /> </span> + <span :class="cssClass"> <icon :name="icon" :size="size" :css-classes="cssClasses" /> </span> </template> diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue index 75c66ed850b..ebb253ff422 100644 --- a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue @@ -1,6 +1,5 @@ <script> -import { diffModes } from '~/ide/constants'; -import { viewerInformationForPath } from '../content_viewer/lib/viewer_utils'; +import { diffViewerModes, diffModes } from '~/ide/constants'; import ImageDiffViewer from './viewers/image_diff_viewer.vue'; import DownloadDiffViewer from './viewers/download_diff_viewer.vue'; import RenamedFile from './viewers/renamed.vue'; @@ -12,6 +11,10 @@ export default { type: String, required: true, }, + diffViewerMode: { + type: String, + required: true, + }, newPath: { type: String, required: true, @@ -46,7 +49,7 @@ export default { }, computed: { viewer() { - if (this.diffMode === diffModes.renamed) { + if (this.diffViewerMode === diffViewerModes.renamed) { return RenamedFile; } else if (this.diffMode === diffModes.mode_changed) { return ModeChanged; @@ -54,11 +57,8 @@ export default { if (!this.newPath) return null; - const previewInfo = viewerInformationForPath(this.newPath); - if (!previewInfo) return DownloadDiffViewer; - - switch (previewInfo.id) { - case 'image': + switch (this.diffViewerMode) { + case diffViewerModes.image: return ImageDiffViewer; default: return DownloadDiffViewer; diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/no_preview.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/no_preview.vue new file mode 100644 index 00000000000..c5cdddf2f64 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/no_preview.vue @@ -0,0 +1,5 @@ +<template> + <div class="nothing-here-block"> + {{ __('No preview for this file type') }} + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/not_diffable.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/not_diffable.vue new file mode 100644 index 00000000000..d4d3038f066 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/not_diffable.vue @@ -0,0 +1,5 @@ +<template> + <div class="nothing-here-block"> + {{ __('This diff was suppressed by a .gitattributes entry.') }} + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue index f54033efc54..0cbcdbf2eb4 100644 --- a/app/assets/javascripts/vue_shared/components/file_row.vue +++ b/app/assets/javascripts/vue_shared/components/file_row.vue @@ -136,6 +136,7 @@ export default { <div v-else :class="fileClass" + :title="file.name" class="file-row" role="button" @click="clickFile" diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue index c33665c24f6..dcda701f049 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue @@ -130,6 +130,6 @@ export default { <template> <div> <div class="flash-container js-suggestions-flash"></div> - <div v-show="isRendered" ref="container" class="note-text md" v-html="noteHtml"></div> + <div v-show="isRendered" ref="container" v-html="noteHtml"></div> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/panel_resizer.vue b/app/assets/javascripts/vue_shared/components/panel_resizer.vue index bf736a378dd..8d81940eb91 100644 --- a/app/assets/javascripts/vue_shared/components/panel_resizer.vue +++ b/app/assets/javascripts/vue_shared/components/panel_resizer.vue @@ -28,11 +28,12 @@ export default { data() { return { size: this.startSize, + isDragging: false, }; }, computed: { className() { - return `drag-${this.side}`; + return [`position-${this.side}-0`, { 'is-dragging': this.isDragging }]; }, cursorStyle() { if (this.enabled) { @@ -57,6 +58,7 @@ export default { startDrag(e) { if (this.enabled) { e.preventDefault(); + this.isDragging = true; this.startPos = e.clientX; this.currentStartSize = this.size; document.addEventListener('mousemove', this.drag); @@ -80,6 +82,7 @@ export default { }, endDrag(e) { e.preventDefault(); + this.isDragging = false; document.removeEventListener('mousemove', this.drag); this.$emit('resize-end', this.size); }, @@ -91,7 +94,7 @@ export default { <div :class="className" :style="cursorStyle" - class="drag-handle" + class="position-absolute position-top-0 position-bottom-0 drag-handle" @mousedown="startDrag" @dblclick="resetSize" ></div> diff --git a/app/assets/javascripts/vue_shared/components/select2_select.vue b/app/assets/javascripts/vue_shared/components/select2_select.vue new file mode 100644 index 00000000000..3074ea859cc --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/select2_select.vue @@ -0,0 +1,35 @@ +<script> +import $ from 'jquery'; +import 'select2/select2'; + +export default { + name: 'Select2Select', + props: { + options: { + type: Object, + required: false, + default: () => ({}), + }, + value: { + type: String, + required: false, + default: '', + }, + }, + + mounted() { + $(this.$refs.dropdownInput) + .val(this.value) + .select2(this.options) + .on('change', event => this.$emit('input', event.target.value)); + }, + + beforeDestroy() { + $(this.$refs.dropdownInput).select2('destroy'); + }, +}; +</script> + +<template> + <input ref="dropdownInput" type="hidden" /> +</template> diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index bdf20866197..83ad8766cb5 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -40,16 +40,6 @@ @import "components/**/*"; /* - * Code highlight - */ -@import "highlight/dark"; -@import "highlight/monokai"; -@import "highlight/solarized_dark"; -@import "highlight/solarized_light"; -@import "highlight/white"; -@import "highlight/none"; - -/* * Styles for JS behaviors. */ @import "behaviors"; diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 62d471bc30c..555ea276c6c 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -65,3 +65,4 @@ @import 'framework/terms'; @import 'framework/read_more'; @import 'framework/flex_grid'; +@import 'framework/system_messages'; diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index 4fb787887a1..70d50c74ca9 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -63,15 +63,15 @@ // // Pass in any number of transitions @mixin transition($transitions...) { - $unfoldedTransitions: (); + $unfolded-transitions: (); @each $transition in $transitions { - $unfoldedTransitions: append($unfoldedTransitions, unfoldTransition($transition), comma); + $unfolded-transitions: append($unfolded-transitions, unfold-transition($transition), comma); } - transition: $unfoldedTransitions; + transition: $unfolded-transitions; } -@mixin disableAllAnimation { +@mixin disable-all-animation { /*CSS transitions*/ -o-transition-property: none !important; -moz-transition-property: none !important; @@ -92,27 +92,27 @@ animation: none !important; } -@function unfoldTransition ($transition) { +@function unfold-transition ($transition) { // Default values $property: all; $duration: $general-hover-transition-duration; $easing: $general-hover-transition-curve; // Browser default is ease, which is what we want $delay: null; // Browser default is 0, which is what we want - $defaultProperties: ($property, $duration, $easing, $delay); + $default-properties: ($property, $duration, $easing, $delay); // Grab transition properties if they exist - $unfoldedTransition: (); - @for $i from 1 through length($defaultProperties) { + $unfolded-transition: (); + @for $i from 1 through length($default-properties) { $p: null; @if $i <= length($transition) { $p: nth($transition, $i); } @else { - $p: nth($defaultProperties, $i); + $p: nth($default-properties, $i); } - $unfoldedTransition: append($unfoldedTransition, $p); + $unfolded-transition: append($unfolded-transition, $p); } - @return $unfoldedTransition; + @return $unfolded-transition; } .btn { diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index ad650d45314..5cfd5bbd4f5 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -15,7 +15,7 @@ margin-top: 3px; padding: $gl-padding; z-index: 300; - width: 300px; + width: $award-emoji-width; font-size: 14px; background-color: $white-light; border: 1px solid $border-white-light; @@ -55,6 +55,10 @@ transform: none; } } + + @include media-breakpoint-down(xs) { + width: $award-emoji-width-xs; + } } .emoji-search { @@ -229,10 +233,10 @@ height: $default-icon-size; width: $default-icon-size; border-radius: 50%; + } - path { - fill: $border-gray-normal; - } + path { + fill: $border-gray-normal; } } @@ -243,6 +247,10 @@ left: 10px; bottom: 6px; opacity: 0; + + path { + fill: $award-emoji-positive-add-lines; + } } .award-control-text { diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index d164cc56e44..cb2c8879c5f 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -166,7 +166,8 @@ @include btn-outline($white-light, $green-600, $green-500, $green-500, $white-light, $green-600, $green-600, $green-700); } - &.btn-remove { + &.btn-remove, + &.btn-danger { @include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700); } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index c5c3b66438c..fa424532879 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -48,6 +48,15 @@ color: $brand-info; } +.text-break-word { + word-break: break-all; +} + +.text-underline, +.text-underline:hover { + text-decoration: underline; +} + .hint { font-style: italic; color: $gl-gray-400; } .light { color: $gl-text-color; } @@ -442,3 +451,15 @@ img.emoji { .position-left-0 { left: 0; } .position-right-0 { right: 0; } .position-top-0 { top: 0; } + +.drag-handle { + width: 4px; + + &:hover { + background-color: $white-normal; + } + + &.is-dragging { + background-color: $gray-600; + } +} diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index f708a26bb32..d6c4e68f68f 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -149,14 +149,6 @@ margin: 10px 0; } - // Border around images in issue and MR comments. - img:not(.emoji) { - border: 1px solid $white-normal; - padding: 5px; - margin: 5px 0; - // Ensure that image does not exceed viewport - max-height: calc(100vh - 100px); - } table:not(.js-syntax-highlight) { @include markdown-table; @@ -228,7 +220,7 @@ .cur { .avatar { - @include disableAllAnimation; + @include disable-all-animation; border: 1px solid $white-light; } } diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 9837b1a6bd0..3b0869e31a9 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -36,10 +36,6 @@ width: fit-content; } - tbody { - background-color: $white-light; - } - tr { th { border-bottom: solid 2px $gl-gray-100; @@ -117,11 +113,6 @@ } } -@mixin dark-diff-match-line { - color: $dark-diff-match-bg; - background: $dark-diff-match-color; -} - @mixin webkit-prefix($property, $value) { #{'-webkit-' + $property}: $value; #{$property}: $value; diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index ace46e32b18..3703b7568c8 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -111,10 +111,11 @@ body.modal-open { flex-grow: 1; height: 56px; padding: $gl-btn-padding $gl-btn-padding 0; + text-align: right; - > svg { - float: right; - height: 100%; + .illustration { + height: inherit; + width: initial; } } } diff --git a/app/assets/stylesheets/framework/system_messages.scss b/app/assets/stylesheets/framework/system_messages.scss new file mode 100644 index 00000000000..3d66136938f --- /dev/null +++ b/app/assets/stylesheets/framework/system_messages.scss @@ -0,0 +1,110 @@ +.header-message, +.footer-message { + padding: 0 15px; + border: 1px solid transparent; + border-radius: 0; + position: fixed; + left: 0; + width: 100%; + text-align: center; + margin: 0; + z-index: 1000; + + p { + @include str-truncated(100%); + margin-top: 0; + margin-bottom: 0; + } +} + +.header-message { + top: 0; + height: $system-header-height; + line-height: $system-header-height; +} + +.footer-message { + bottom: 0; + height: $system-footer-height; + line-height: $system-footer-height; +} + +.with-performance-bar { + .header-message { + top: $performance-bar-height; + } +} + +// System Header +.with-system-header { + // main navigation + // login page + .navbar-gitlab, + .fixed-top { + top: $system-header-height; + } + + // left sidebar eg: project page + // right sidebar eg: MR page + .nav-sidebar, + .right-sidebar { + top: $system-header-height + $header-height; + } + + .content-wrapper { + margin-top: $system-header-height + $header-height; + } + + // Performance Bar + // System Header + &.with-performance-bar { + // main navigation + header.navbar-gitlab { + top: $performance-bar-height + $system-header-height; + } + + .layout-page { + margin-top: $header-height + $performance-bar-height + $system-header-height; + } + + // left sidebar eg: project page + // right sidebar eg: MR page + .nav-sidebar, + .right-sidebar { + top: $header-height + $performance-bar-height + $system-header-height; + } + } +} + +// System Footer +.with-system-footer { + // left sidebar eg: project page + // right sidebar eg: mr page + .nav-sidebar, + .right-sidebar, + // navless pages' footer eg: login page + // navless pages' footer border eg: login page + &.devise-layout-html body .footer-container, + &.devise-layout-html body hr.footer-fixed { + bottom: $system-footer-height; + } +} + +.fullscreen-layout { + .header-message, + .footer-message { + position: static; + top: auto; + bottom: auto; + } + + .content-wrapper { + .with-system-header & { + margin-top: 0; + } + + .with-system-footer & { + margin-top: 0; + } + } +} diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index a08639936c0..1b36c1f4862 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -49,13 +49,6 @@ word-wrap: normal; } - // Multi-line code blocks should scroll horizontally - pre { - code { - white-space: pre; - } - } - kbd { display: inline-block; padding: 3px 5px; @@ -166,6 +159,10 @@ overflow-x: auto; border-radius: 2px; + // Multi-line code blocks should scroll horizontally + code { + white-space: pre; + } &.plain-readme { background: none; @@ -303,11 +300,10 @@ body { } .page-title-empty { - margin-top: 0; + margin: 12px 0; line-height: 1.3; font-size: 1.25em; font-weight: $gl-font-weight-bold; - margin: 12px 0; } h1, @@ -375,6 +371,16 @@ code { .md:not(.use-csslab) { @include md-typography; + + &:not(.wiki) { + img:not(.emoji) { + border: 1px solid $white-normal; + padding: 5px; + margin: 5px 0; + // Ensure that image does not exceed viewport + max-height: calc(100vh - 100px); + } + } } /** diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 96dab609a13..27c54cb0b75 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -251,7 +251,7 @@ $gl-padding-top: 10px; $gl-sidebar-padding: 22px; $gl-bar-padding: 3px; $input-horizontal-padding: 12px; -$browserScrollbarSize: 10px; +$browser-scrollbar-size: 10px; /* * Misc @@ -276,6 +276,8 @@ $general-hover-transition-duration: 100ms; $general-hover-transition-curve: linear; $highlight-changes-color: rgb(235, 255, 232); $performance-bar-height: 35px; +$system-header-height: 35px; +$system-footer-height: $system-header-height; $flash-height: 52px; $context-header-height: 60px; $breadcrumb-min-height: 48px; @@ -405,6 +407,8 @@ $status-icon-size: 22px; $award-emoji-menu-shadow: rgba(0, 0, 0, 0.175); $award-emoji-positive-add-bg: #fed159; $award-emoji-positive-add-lines: #bb9c13; +$award-emoji-width: 376px; +$award-emoji-width-xs: 300px; /* * Search Box diff --git a/app/assets/stylesheets/highlight/common.scss b/app/assets/stylesheets/highlight/common.scss new file mode 100644 index 00000000000..2b0794759d5 --- /dev/null +++ b/app/assets/stylesheets/highlight/common.scss @@ -0,0 +1,18 @@ +@import "../framework/variables"; + +@mixin diff-background($background, $idiff, $border) { + background: $background; + + &.line_content span.idiff { + background: $idiff; + } + + &.diff-line-num { + border-color: $border; + } +} + +@mixin dark-diff-match-line { + color: $dark-diff-match-bg; + background: $dark-diff-match-color; +} diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/themes/dark.scss index 604f806dc58..16893dd047e 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/themes/dark.scss @@ -1,5 +1,7 @@ /* https://github.com/MozMorris/tomorrow-pygments */ +@import "../common"; + /* * Dark syntax colors */ @@ -125,7 +127,7 @@ $dark-il: #de935f; .diff-line-num.new, .line_content.new { - @include diff_background($dark-new-bg, $dark-new-idiff, $dark-border); + @include diff-background($dark-new-bg, $dark-new-idiff, $dark-border); &::before, a { @@ -135,7 +137,7 @@ $dark-il: #de935f; .diff-line-num.old, .line_content.old { - @include diff_background($dark-old-bg, $dark-old-idiff, $dark-border); + @include diff-background($dark-old-bg, $dark-old-idiff, $dark-border); &::before, a { diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/themes/monokai.scss index 8e2720511da..37fe61b925c 100644 --- a/app/assets/stylesheets/highlight/monokai.scss +++ b/app/assets/stylesheets/highlight/themes/monokai.scss @@ -1,5 +1,7 @@ /* https://github.com/richleland/pygments-css/blob/master/monokai.css */ +@import "../common"; + /* * Monokai Colors */ @@ -125,7 +127,7 @@ $monokai-gi: #a6e22e; .diff-line-num.new, .line_content.new { - @include diff_background($monokai-new-bg, $monokai-new-idiff, $monokai-diff-border); + @include diff-background($monokai-new-bg, $monokai-new-idiff, $monokai-diff-border); &::before, a { @@ -135,7 +137,7 @@ $monokai-gi: #a6e22e; .diff-line-num.old, .line_content.old { - @include diff_background($monokai-old-bg, $monokai-old-idiff, $monokai-diff-border); + @include diff-background($monokai-old-bg, $monokai-old-idiff, $monokai-diff-border); &::before, a { diff --git a/app/assets/stylesheets/highlight/none.scss b/app/assets/stylesheets/highlight/themes/none.scss index 7ced4e82e66..b4217aac37a 100644 --- a/app/assets/stylesheets/highlight/none.scss +++ b/app/assets/stylesheets/highlight/themes/none.scss @@ -2,9 +2,9 @@ * None Syntax Colors */ +@import "../common"; - -@mixin matchLine { +@mixin match-line { color: $black-transparent; background-color: $white-normal; } @@ -45,7 +45,7 @@ &.match .line_content, .new-nonewline.line_content, .old-nonewline.line_content { - @include matchLine; + @include match-line; } .diff-line-num { @@ -121,7 +121,7 @@ } &.match { - @include matchLine; + @include match-line; } &.hll:not(.empty-cell) { diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/themes/solarized-dark.scss index cd1f0f6650f..a4e9eda22c9 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/themes/solarized-dark.scss @@ -1,5 +1,7 @@ /* https://gist.github.com/qguv/7936275 */ +@import "../common"; + /* * Solarized dark colors */ @@ -129,7 +131,7 @@ $solarized-dark-il: #2aa198; .diff-line-num.new, .line_content.new { - @include diff_background($solarized-dark-new-bg, $solarized-dark-new-idiff, $solarized-dark-border); + @include diff-background($solarized-dark-new-bg, $solarized-dark-new-idiff, $solarized-dark-border); &::before, a { @@ -139,7 +141,7 @@ $solarized-dark-il: #2aa198; .diff-line-num.old, .line_content.old { - @include diff_background($solarized-dark-old-bg, $solarized-dark-old-idiff, $solarized-dark-border); + @include diff-background($solarized-dark-old-bg, $solarized-dark-old-idiff, $solarized-dark-border); &::before, a { diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/themes/solarized-light.scss index 09c3ea36414..b604d1ccb6c 100644 --- a/app/assets/stylesheets/highlight/solarized_light.scss +++ b/app/assets/stylesheets/highlight/themes/solarized-light.scss @@ -1,5 +1,7 @@ /* https://gist.github.com/qguv/7936275 */ +@import "../common"; + /* * Solarized light syntax colors */ @@ -90,7 +92,7 @@ $solarized-light-vg: #268bd2; $solarized-light-vi: #268bd2; $solarized-light-il: #2aa198; -@mixin matchLine { +@mixin match-line { color: $black-transparent; background: $solarized-light-matchline-bg; } @@ -125,7 +127,7 @@ $solarized-light-il: #2aa198; &.match .line_content, &.old-nonewline .line_content, &.new-nonewline .line_content { - @include matchLine; + @include match-line; } td.diff-line-num.hll:not(.empty-cell), @@ -136,7 +138,7 @@ $solarized-light-il: #2aa198; .diff-line-num.new, .line_content.new { - @include diff_background($solarized-light-new-bg, + @include diff-background($solarized-light-new-bg, $solarized-light-new-idiff, $solarized-light-border); &::before, @@ -147,7 +149,7 @@ $solarized-light-il: #2aa198; .diff-line-num.old, .line_content.old { - @include diff_background($solarized-light-old-bg, $solarized-light-old-idiff, $solarized-light-border); + @include diff-background($solarized-light-old-bg, $solarized-light-old-idiff, $solarized-light-border); &::before, a { @@ -168,7 +170,7 @@ $solarized-light-il: #2aa198; } .line_content.match { - @include matchLine; + @include match-line; } &:not(.diff-expanded) + .diff-expanded, diff --git a/app/assets/stylesheets/highlight/themes/white.scss b/app/assets/stylesheets/highlight/themes/white.scss new file mode 100644 index 00000000000..7239086f649 --- /dev/null +++ b/app/assets/stylesheets/highlight/themes/white.scss @@ -0,0 +1,3 @@ +.code.white { + @import "../white_base"; +} diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss deleted file mode 100644 index 355c8d223f7..00000000000 --- a/app/assets/stylesheets/highlight/white.scss +++ /dev/null @@ -1,3 +0,0 @@ -.code.white { - @import "white_base"; -} diff --git a/app/assets/stylesheets/highlight/white_base.scss b/app/assets/stylesheets/highlight/white_base.scss index 90a5250c247..23ec3380ce9 100644 --- a/app/assets/stylesheets/highlight/white_base.scss +++ b/app/assets/stylesheets/highlight/white_base.scss @@ -1,5 +1,7 @@ /* https://github.com/aahan/pygments-github-style */ +@import "./common"; + /* * White Syntax Colors */ @@ -70,7 +72,7 @@ $white-gc-color: #999; $white-gc-bg: #eaf2f5; -@mixin matchLine { +@mixin match-line { color: $black-transparent; background-color: $gray-light; } @@ -105,7 +107,7 @@ pre.code, &.match .line_content, .new-nonewline.line_content, .old-nonewline.line_content { - @include matchLine; + @include match-line; } .diff-line-num { @@ -185,7 +187,7 @@ pre.code, } &.match { - @include matchLine; + @include match-line; } &.hll:not(.empty-cell) { diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index 2ac98b5d18f..a80158943c6 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -682,25 +682,6 @@ $ide-commit-header-height: 48px; flex: 1; } -.drag-handle { - position: absolute; - top: 0; - bottom: 0; - width: 4px; - - &:hover { - background-color: $white-normal; - } - - &.drag-right { - right: 0; - } - - &.drag-left { - left: 0; - } -} - .ide-commit-list-container { display: flex; flex: 1; diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 65f46e3852a..fa5a182243c 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -75,7 +75,11 @@ @include build-trace-top-bar(35px); &.has-archived-block { - top: $header-height + $performance-bar-height + 28px; + top: $header-height + 28px; + + .with-performance-bar & { + top: $header-height + $performance-bar-height + 28px; + } } &.affix { diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 37ed5ae674a..cb5f1a84005 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -34,7 +34,6 @@ .detail-page-header-actions { align-self: center; - flex-shrink: 0; flex: 0 0 auto; @include media-breakpoint-down(xs) { diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index e3b98b26a11..d001dff7986 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -602,18 +602,6 @@ } } -@mixin diff_background($background, $idiff, $border) { - background: $background; - - &.line_content span.idiff { - background: $idiff; - } - - &.diff-line-num { - border-color: $border; - } -} - .files { .diff-file:last-child { margin-bottom: 0; @@ -1038,12 +1026,30 @@ } .diff-tree-list { - width: 320px; + position: -webkit-sticky; + position: sticky; + $top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px; + top: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px; + max-height: calc(100vh - #{$top-pos}); + padding-right: $gl-padding; + z-index: 202; + + .with-performance-bar & { + $performance-bar-top-pos: $performance-bar-height + $top-pos; + top: $performance-bar-top-pos; + max-height: calc(100vh - #{$performance-bar-top-pos}); + } + + .drag-handle { + bottom: 16px; + transform: translateX(-6px); + } } .diff-files-holder { flex: 1; min-width: 0; + z-index: 201; } .compare-versions-container { @@ -1051,23 +1057,12 @@ } .tree-list-holder { - position: -webkit-sticky; - position: sticky; - $top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px; - top: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px; - max-height: calc(100vh - #{$top-pos}); - padding-right: $gl-padding; + height: 100%; .file-row { margin-left: 0; margin-right: 0; } - - .with-performance-bar & { - $performance-bar-top-pos: $performance-bar-height + $top-pos; - top: $performance-bar-top-pos; - max-height: calc(100vh - #{$performance-bar-top-pos}); - } } .tree-list-scroll { diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 5a988b184b6..655b297295a 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -182,9 +182,8 @@ .template-selector-dropdowns-wrap { display: inline-block; - margin-left: 8px; - vertical-align: top; margin: 5px 0 0 8px; + vertical-align: top; @media(max-width: map-get($grid-breakpoints, md)-1) { display: block; diff --git a/app/assets/stylesheets/pages/import.scss b/app/assets/stylesheets/pages/import.scss index a4f76a9495a..7f800367cad 100644 --- a/app/assets/stylesheets/pages/import.scss +++ b/app/assets/stylesheets/pages/import.scss @@ -1,20 +1,51 @@ -.import-jobs-from-col, .import-jobs-to-col { - width: 40%; + width: 39%; } .import-jobs-status-col { - width: 20%; + width: 15%; } -.btn-import { - .loading-icon { - display: none; +.import-jobs-cta-col { + width: 1%; +} + +.import-project-name-input { + border-radius: 0 $border-radius-default $border-radius-default 0; + position: relative; + left: -1px; + max-width: 300px; +} + +.import-namespace-select { + width: auto !important; + + > .select2-choice { + border-radius: $border-radius-default 0 0 $border-radius-default; + position: relative; + left: 1px; } +} - &.is-loading { - .loading-icon { - display: inline-block; - } +.import-slash-divider { + background-color: $gray-lightest; + border: 1px solid $border-color; +} + +.import-row { + height: 55px; +} + +.import-table { + .import-jobs-from-col, + .import-jobs-to-col, + .import-jobs-status-col, + .import-jobs-cta-col { + border-bottom-width: 1px; + padding-left: $gl-padding; } } + +.import-projects-loading-icon { + margin-top: $gl-padding-32; +} diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 135730d71e9..cfd3faab122 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -82,7 +82,6 @@ } .mr-widget-body, -.mr-widget-section, .mr-widget-content, .mr-widget-footer { padding: $gl-padding; @@ -735,9 +734,11 @@ .mr-version-controls { position: relative; - z-index: 103; + z-index: 203; background: $gray-light; color: $gl-text-color; + margin-top: -1px; + border-top: 1px solid $border-color; .mr-version-menus-container { display: flex; @@ -789,7 +790,6 @@ position: sticky; top: $header-height + $mr-tabs-height; width: 100%; - border-top: 1px solid $border-color; &.is-fileTreeOpen { margin-left: -16px; @@ -808,12 +808,9 @@ .merge-request-tabs-holder { top: $header-height; - z-index: 200; + z-index: 300; background-color: $white-light; - - @include media-breakpoint-down(md) { - border-bottom: 1px solid $border-color; - } + border-bottom: 1px solid $border-color; @include media-breakpoint-up(sm) { position: sticky; @@ -1019,3 +1016,8 @@ z-index: 99999; background: $black-transparent; } + +.source-branch-removal-status { + padding-left: 50px; + padding-bottom: $gl-padding; +} diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 23b9e4f9416..7e7eff1346a 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -494,11 +494,6 @@ $note-form-margin-left: 72px; .discussion-notes { margin-left: 0; border-left: 0; - - .notes { - position: relative; - @include vertical-line(52px); - } } .note-wrapper { @@ -550,6 +545,11 @@ $note-form-margin-left: 72px; .note-header-info { padding-bottom: 0; } + + .timeline-content { + overflow-x: auto; + overflow-y: hidden; + } } .unresolved { @@ -597,7 +597,6 @@ $note-form-margin-left: 72px; .note-headline-meta { display: inline-block; - white-space: nowrap; .system-note-message { white-space: normal; @@ -607,6 +606,10 @@ $note-form-margin-left: 72px; color: $gl-text-color-disabled; } + .note-timestamp { + white-space: nowrap; + } + a:hover { text-decoration: underline; } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 66866aedfba..277030ad3af 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -704,8 +704,8 @@ .scrolling-tabs-container { .scrolling-tabs { margin-top: $gl-padding-8; - margin-bottom: $gl-padding-8 - $browserScrollbarSize; - padding-bottom: $browserScrollbarSize; + margin-bottom: $gl-padding-8 - $browser-scrollbar-size; + padding-bottom: $browser-scrollbar-size; flex-wrap: wrap; border-bottom: 0; } @@ -713,7 +713,7 @@ .fade-left, .fade-right { top: 0; - height: calc(100% - #{$browserScrollbarSize}); + height: calc(100% - #{$browser-scrollbar-size}); .fa { top: 50%; |