Commit 04123bae authored by Nick Thomas's avatar Nick Thomas

Merge branch 'master' into ce-to-ee-2018-11-06

parents db4ea63d e077c54b
<script> <script>
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import Tooltip from '~/vue_shared/directives/tooltip'; import Tooltip from '~/vue_shared/directives/tooltip';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default { export default {
name: 'Badge', name: 'Badge',
components: { components: {
Icon, Icon,
Tooltip, Tooltip,
GlLoadingIcon,
}, },
directives: { directives: {
Tooltip, Tooltip,
......
...@@ -4,6 +4,7 @@ import { mapActions, mapState } from 'vuex'; ...@@ -4,6 +4,7 @@ import { mapActions, mapState } from 'vuex';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue'; import LoadingButton from '~/vue_shared/components/loading_button.vue';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import createEmptyBadge from '../empty_badge'; import createEmptyBadge from '../empty_badge';
import Badge from './badge.vue'; import Badge from './badge.vue';
...@@ -14,6 +15,7 @@ export default { ...@@ -14,6 +15,7 @@ export default {
components: { components: {
Badge, Badge,
LoadingButton, LoadingButton,
GlLoadingIcon,
}, },
props: { props: {
isEditing: { isEditing: {
......
<script> <script>
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import BadgeListRow from './badge_list_row.vue'; import BadgeListRow from './badge_list_row.vue';
import { GROUP_BADGE } from '../constants'; import { GROUP_BADGE } from '../constants';
...@@ -7,6 +8,7 @@ export default { ...@@ -7,6 +8,7 @@ export default {
name: 'BadgeList', name: 'BadgeList',
components: { components: {
BadgeListRow, BadgeListRow,
GlLoadingIcon,
}, },
computed: { computed: {
...mapState(['badges', 'isLoading', 'kind']), ...mapState(['badges', 'isLoading', 'kind']),
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import { PROJECT_BADGE } from '../constants'; import { PROJECT_BADGE } from '../constants';
import Badge from './badge.vue'; import Badge from './badge.vue';
...@@ -10,6 +11,7 @@ export default { ...@@ -10,6 +11,7 @@ export default {
components: { components: {
Badge, Badge,
Icon, Icon,
GlLoadingIcon,
}, },
props: { props: {
badge: { badge: {
......
<script> <script>
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import boardNewIssue from './board_new_issue.vue'; import boardNewIssue from './board_new_issue.vue';
import boardCard from './board_card.vue'; import boardCard from './board_card.vue';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
...@@ -11,6 +12,7 @@ export default { ...@@ -11,6 +12,7 @@ export default {
components: { components: {
boardCard, boardCard,
boardNewIssue, boardNewIssue,
GlLoadingIcon,
}, },
props: { props: {
groupId: { groupId: {
......
...@@ -6,6 +6,7 @@ import ModalList from './list.vue'; ...@@ -6,6 +6,7 @@ import ModalList from './list.vue';
import ModalFooter from './footer.vue'; import ModalFooter from './footer.vue';
import EmptyState from './empty_state.vue'; import EmptyState from './empty_state.vue';
import ModalStore from '../../stores/modal_store'; import ModalStore from '../../stores/modal_store';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default { export default {
components: { components: {
...@@ -13,6 +14,7 @@ export default { ...@@ -13,6 +14,7 @@ export default {
ModalHeader, ModalHeader,
ModalList, ModalList,
ModalFooter, ModalFooter,
GlLoadingIcon,
}, },
props: { props: {
newIssuePath: { newIssuePath: {
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import $ from 'jquery'; import $ from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
import Api from '../../api'; import Api from '../../api';
...@@ -9,6 +10,7 @@ export default { ...@@ -9,6 +10,7 @@ export default {
name: 'BoardProjectSelect', name: 'BoardProjectSelect',
components: { components: {
Icon, Icon,
GlLoadingIcon,
}, },
props: { props: {
groupId: { groupId: {
......
...@@ -3,5 +3,4 @@ import './polyfills'; ...@@ -3,5 +3,4 @@ import './polyfills';
import './jquery'; import './jquery';
import './bootstrap'; import './bootstrap';
import './vue'; import './vue';
import './gitlab_ui';
import '../lib/utils/axios_utils'; import '../lib/utils/axios_utils';
...@@ -5,6 +5,7 @@ import 'core-js/fn/array/find-index'; ...@@ -5,6 +5,7 @@ import 'core-js/fn/array/find-index';
import 'core-js/fn/array/from'; import 'core-js/fn/array/from';
import 'core-js/fn/array/includes'; import 'core-js/fn/array/includes';
import 'core-js/fn/object/assign'; import 'core-js/fn/object/assign';
import 'core-js/fn/object/values';
import 'core-js/fn/promise'; import 'core-js/fn/promise';
import 'core-js/fn/string/code-point-at'; import 'core-js/fn/string/code-point-at';
import 'core-js/fn/string/from-code-point'; import 'core-js/fn/string/from-code-point';
......
<script> <script>
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
export default { export default {
components: {
GlLoadingIcon,
},
props: { props: {
deployKey: { deployKey: {
type: Object, type: Object,
......
...@@ -6,11 +6,13 @@ import eventHub from '../eventhub'; ...@@ -6,11 +6,13 @@ import eventHub from '../eventhub';
import DeployKeysService from '../service'; import DeployKeysService from '../service';
import DeployKeysStore from '../store'; import DeployKeysStore from '../store';
import KeysPanel from './keys_panel.vue'; import KeysPanel from './keys_panel.vue';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default { export default {
components: { components: {
KeysPanel, KeysPanel,
NavigationTabs, NavigationTabs,
GlLoadingIcon,
}, },
props: { props: {
endpoint: { endpoint: {
......
...@@ -3,6 +3,7 @@ import { mapState, mapGetters, mapActions } from 'vuex'; ...@@ -3,6 +3,7 @@ import { mapState, mapGetters, mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { __ } from '~/locale'; import { __ } from '~/locale';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import eventHub from '../../notes/event_hub'; import eventHub from '../../notes/event_hub';
import CompareVersions from './compare_versions.vue'; import CompareVersions from './compare_versions.vue';
import DiffFile from './diff_file.vue'; import DiffFile from './diff_file.vue';
...@@ -21,6 +22,7 @@ export default { ...@@ -21,6 +22,7 @@ export default {
HiddenFilesWarning, HiddenFilesWarning,
CommitWidget, CommitWidget,
TreeList, TreeList,
GlLoadingIcon,
}, },
props: { props: {
endpoint: { endpoint: {
...@@ -223,7 +225,10 @@ export default { ...@@ -223,7 +225,10 @@ export default {
:commit="commit" :commit="commit"
/> />
<div class="files d-flex prepend-top-default"> <div
:data-can-create-note="getNoteableData.current_user.can_create_note"
class="files d-flex prepend-top-default"
>
<div <div
v-show="showTreeList" v-show="showTreeList"
class="diff-tree-list" class="diff-tree-list"
......
<script> <script>
import { mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue'; import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import { diffModes } from '~/ide/constants';
import InlineDiffView from './inline_diff_view.vue'; import InlineDiffView from './inline_diff_view.vue';
import ParallelDiffView from './parallel_diff_view.vue'; import ParallelDiffView from './parallel_diff_view.vue';
import NoteForm from '../../notes/components/note_form.vue';
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';
export default { export default {
components: { components: {
InlineDiffView, InlineDiffView,
ParallelDiffView, ParallelDiffView,
DiffViewer, DiffViewer,
NoteForm,
DiffDiscussions,
ImageDiffOverlay,
}, },
props: { props: {
diffFile: { diffFile: {
...@@ -23,13 +30,38 @@ export default { ...@@ -23,13 +30,38 @@ export default {
endpoint: state => state.diffs.endpoint, endpoint: state => state.diffs.endpoint,
}), }),
...mapGetters('diffs', ['isInlineView', 'isParallelView']), ...mapGetters('diffs', ['isInlineView', 'isParallelView']),
...mapGetters('diffs', ['getCommentFormForDiffFile']),
...mapGetters(['getNoteableData', 'noteableType']),
diffMode() { diffMode() {
const diffModeKey = Object.keys(diffModes).find(key => this.diffFile[`${key}File`]); return getDiffMode(this.diffFile);
return diffModes[diffModeKey] || diffModes.replaced;
}, },
isTextFile() { isTextFile() {
return this.diffFile.viewer.name === 'text'; return this.diffFile.viewer.name === 'text';
}, },
diffFileCommentForm() {
return this.getCommentFormForDiffFile(this.diffFile.fileHash);
},
showNotesContainer() {
return this.diffFile.discussions.length || this.diffFileCommentForm;
},
},
methods: {
...mapActions('diffs', ['saveDiffDiscussion', 'closeDiffFileCommentForm']),
handleSaveNote(note) {
this.saveDiffDiscussion({
note,
formData: {
noteableData: this.getNoteableData,
noteableType: this.noteableType,
diffFile: this.diffFile,
positionType: IMAGE_DIFF_POSITION_TYPE,
x: this.diffFileCommentForm.x,
y: this.diffFileCommentForm.y,
width: this.diffFileCommentForm.width,
height: this.diffFileCommentForm.height,
},
});
},
}, },
}; };
</script> </script>
...@@ -56,7 +88,37 @@ export default { ...@@ -56,7 +88,37 @@ export default {
:new-sha="diffFile.diffRefs.headSha" :new-sha="diffFile.diffRefs.headSha"
:old-path="diffFile.oldPath" :old-path="diffFile.oldPath"
:old-sha="diffFile.diffRefs.baseSha" :old-sha="diffFile.diffRefs.baseSha"
:project-path="projectPath"/> :file-hash="diffFile.fileHash"
:project-path="projectPath"
>
<image-diff-overlay
slot="image-overlay"
:discussions="diffFile.discussions"
:file-hash="diffFile.fileHash"
:can-comment="getNoteableData.current_user.can_create_note"
/>
<div
v-if="showNotesContainer"
class="note-container"
>
<diff-discussions
v-if="diffFile.discussions.length"
class="diff-file-discussions"
:discussions="diffFile.discussions"
:should-collapse-discussions="true"
:render-avatar-badge="true"
/>
<note-form
v-if="diffFileCommentForm"
ref="noteForm"
:is-editing="false"
:save-button-title="__('Comment')"
class="diff-comment-form new-note discussion-form discussion-form-container"
@handleFormUpdate="handleSaveNote"
@cancelForm="closeDiffFileCommentForm(diffFile.fileHash)"
/>
</div>
</diff-viewer>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import noteableDiscussion from '../../notes/components/noteable_discussion.vue'; import noteableDiscussion from '../../notes/components/noteable_discussion.vue';
export default { export default {
components: { components: {
noteableDiscussion, noteableDiscussion,
Icon,
}, },
props: { props: {
discussions: { discussions: {
type: Array, type: Array,
required: true, required: true,
}, },
shouldCollapseDiscussions: {
type: Boolean,
required: false,
default: false,
},
renderAvatarBadge: {
type: Boolean,
required: false,
default: false,
},
}, },
methods: { methods: {
...mapActions(['toggleDiscussion']),
...mapActions('diffs', ['removeDiscussionsFromDiff']), ...mapActions('diffs', ['removeDiscussionsFromDiff']),
deleteNoteHandler(discussion) { deleteNoteHandler(discussion) {
if (discussion.notes.length <= 1) { if (discussion.notes.length <= 1) {
this.removeDiscussionsFromDiff(discussion); this.removeDiscussionsFromDiff(discussion);
} }
}, },
isExpanded(discussion) {
return this.shouldCollapseDiscussions ? discussion.expanded : true;
},
}, },
}; };
</script> </script>
...@@ -26,22 +42,54 @@ export default { ...@@ -26,22 +42,54 @@ export default {
<template> <template>
<div> <div>
<div <div
v-for="discussion in discussions" v-for="(discussion, index) in discussions"
:key="discussion.id" :key="discussion.id"
class="discussion-notes diff-discussions" :class="{
collapsed: !isExpanded(discussion)
}"
class="discussion-notes diff-discussions position-relative"
> >
<ul <ul
:data-discussion-id="discussion.id" :data-discussion-id="discussion.id"
class="notes" class="notes"
> >
<template v-if="shouldCollapseDiscussions">
<button
:class="{
'diff-notes-collapse': discussion.expanded,
'btn-transparent badge badge-pill': !discussion.expanded
}"
type="button"
class="js-diff-notes-toggle"
@click="toggleDiscussion({ discussionId: discussion.id })"
>
<icon
v-if="discussion.expanded"
name="collapse"
class="collapse-icon"
/>
<template v-else>
{{ index + 1 }}
</template>
</button>
</template>
<noteable-discussion <noteable-discussion
v-show="isExpanded(discussion)"
:discussion="discussion" :discussion="discussion"
:render-header="false" :render-header="false"
:render-diff-file="false" :render-diff-file="false"
:always-expanded="true" :always-expanded="true"
:discussions-by-diff-order="true" :discussions-by-diff-order="true"
@noteDeleted="deleteNoteHandler" @noteDeleted="deleteNoteHandler"
/> >
<span
v-if="renderAvatarBadge"
slot="avatar-badge"
class="badge badge-pill"
>
{{ index + 1 }}
</span>
</noteable-discussion>
</ul> </ul>
</div> </div>
</div> </div>
......
...@@ -3,6 +3,7 @@ import { mapActions, mapGetters, mapState } from 'vuex'; ...@@ -3,6 +3,7 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore'; import _ from 'underscore';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import DiffFileHeader from './diff_file_header.vue'; import DiffFileHeader from './diff_file_header.vue';
import DiffContent from './diff_content.vue'; import DiffContent from './diff_content.vue';
...@@ -10,6 +11,7 @@ export default { ...@@ -10,6 +11,7 @@ export default {
components: { components: {
DiffFileHeader, DiffFileHeader,
DiffContent, DiffContent,
GlLoadingIcon,
}, },
props: { props: {
file: { file: {
......
<script>
import { mapActions, mapGetters } from 'vuex';
import _ from 'underscore';
import Icon from '~/vue_shared/components/icon.vue';
export default {
name: 'ImageDiffOverlay',
components: {
Icon,
},
props: {
discussions: {
type: [Array, Object],
required: true,
},
fileHash: {
type: String,
required: true,
},
canComment: {
type: Boolean,
required: false,
default: false,
},
showCommentIcon: {
type: Boolean,
required: false,
default: false,
},
badgeClass: {
type: String,
required: false,
default: 'badge badge-pill',
},
shouldToggleDiscussion: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
...mapGetters('diffs', ['getDiffFileByHash', 'getCommentFormForDiffFile']),
currentCommentForm() {
return this.getCommentFormForDiffFile(this.fileHash);
},
allDiscussions() {
return _.isArray(this.discussions) ? this.discussions : [this.discussions];
},
},
methods: {
...mapActions(['toggleDiscussion']),
...mapActions('diffs', ['openDiffFileCommentForm']),
getImageDimensions() {
return {
width: this.$parent.width,
height: this.$parent.height,
};
},
getPositionForObject(meta) {
const { x, y, width, height } = meta;
const imageWidth = this.getImageDimensions().width;
const imageHeight = this.getImageDimensions().height;
const widthRatio = imageWidth / width;
const heightRatio = imageHeight / height;
return {
x: Math.round(x * widthRatio),
y: Math.round(y * heightRatio),
};
},
getPosition(discussion) {
const { x, y } = this.getPositionForObject(discussion.position);
return {
left: `${x}px`,
top: `${y}px`,
};
},
clickedImage(x, y) {
const { width, height } = this.getImageDimensions();
this.openDiffFileCommentForm({
fileHash: this.fileHash,
width,
height,
x,
y,
});
},
},
};
</script>
<template>
<div class="position-absolute w-100 h-100 image-diff-overlay">
<button
v-if="canComment"
type="button"
class="btn-transparent position-absolute image-diff-overlay-add-comment w-100 h-100 js-add-image-diff-note-button"
@click="clickedImage($event.offsetX, $event.offsetY)"
>
<span class="sr-only">
{{ __('Add image comment') }}
</span>
</button>
<button
v-for="(discussion, index) in allDiscussions"
:key="discussion.id"
:style="getPosition(discussion)"
:class="badgeClass"
:disabled="!shouldToggleDiscussion"
class="js-image-badge"
type="button"
@click="toggleDiscussion({ discussionId: discussion.id })"
>
<icon
v-if="showCommentIcon"
name="image-comment-dark"
/>
<template v-else>
{{ index + 1 }}
</template>
</button>
<button
v-if="currentCommentForm"
:style="{
left: `${currentCommentForm.x}px`,
top: `${currentCommentForm.y}px`
}"
:aria-label="__('Comment form position')"
class="btn-transparent comment-indicator"
type="button"
>
<icon
name="image-comment-dark"
/>
</button>
</div>
</template>
...@@ -12,6 +12,7 @@ export const NOTE_TYPE = 'Note'; ...@@ -12,6 +12,7 @@ export const NOTE_TYPE = 'Note';
export const NEW_LINE_TYPE = 'new'; export const NEW_LINE_TYPE = 'new';
export const OLD_LINE_TYPE = 'old'; export const OLD_LINE_TYPE = 'old';
export const TEXT_DIFF_POSITION_TYPE = 'text'; export const TEXT_DIFF_POSITION_TYPE = 'text';
export const IMAGE_DIFF_POSITION_TYPE = 'image';
export const LINE_POSITION_LEFT = 'left'; export const LINE_POSITION_LEFT = 'left';
export const LINE_POSITION_RIGHT = 'right'; export const LINE_POSITION_RIGHT = 'right';
......
...@@ -50,8 +50,8 @@ export const assignDiscussionsToDiff = ( ...@@ -50,8 +50,8 @@ export const assignDiscussionsToDiff = (
}; };
export const removeDiscussionsFromDiff = ({ commit }, removeDiscussion) => { export const removeDiscussionsFromDiff = ({ commit }, removeDiscussion) => {
const { fileHash, line_code } = removeDiscussion; const { fileHash, line_code, id } = removeDiscussion;
commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash, lineCode: line_code }); commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash, lineCode: line_code, id });
}; };
export const startRenderDiffsQueue = ({ state, commit }) => { export const startRenderDiffsQueue = ({ state, commit }) => {
...@@ -189,6 +189,7 @@ export const saveDiffDiscussion = ({ dispatch }, { note, formData }) => { ...@@ -189,6 +189,7 @@ export const saveDiffDiscussion = ({ dispatch }, { note, formData }) => {
return dispatch('saveNote', postData, { root: true }) return dispatch('saveNote', postData, { root: true })
.then(result => dispatch('updateDiscussion', result.discussion, { root: true })) .then(result => dispatch('updateDiscussion', result.discussion, { root: true }))
.then(discussion => dispatch('assignDiscussionsToDiff', [discussion])) .then(discussion => dispatch('assignDiscussionsToDiff', [discussion]))
.then(() => dispatch('closeDiffFileCommentForm', formData.diffFile.fileHash))
.catch(() => createFlash(s__('MergeRequests|Saving the comment failed'))); .catch(() => createFlash(s__('MergeRequests|Saving the comment failed')));
}; };
...@@ -210,5 +211,19 @@ export const toggleShowTreeList = ({ commit, state }) => { ...@@ -210,5 +211,19 @@ export const toggleShowTreeList = ({ commit, state }) => {
localStorage.setItem(MR_TREE_SHOW_KEY, state.showTreeList); localStorage.setItem(MR_TREE_SHOW_KEY, state.showTreeList);
}; };
export const openDiffFileCommentForm = ({ commit, getters }, formData) => {
const form = getters.getCommentFormForDiffFile(formData.fileHash);
if (form) {
commit(types.UPDATE_DIFF_FILE_COMMENT_FORM, formData);
} else {
commit(types.OPEN_DIFF_FILE_COMMENT_FORM, formData);
}
};
export const closeDiffFileCommentForm = ({ commit }, fileHash) => {
commit(types.CLOSE_DIFF_FILE_COMMENT_FORM, fileHash);
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -114,5 +114,8 @@ export const allBlobs = state => Object.values(state.treeEntries).filter(f => f. ...@@ -114,5 +114,8 @@ export const allBlobs = state => Object.values(state.treeEntries).filter(f => f.
export const diffFilesLength = state => state.diffFiles.length; export const diffFilesLength = state => state.diffFiles.length;
export const getCommentFormForDiffFile = state => fileHash =>
state.commentForms.find(form => form.fileHash === fileHash);
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -24,4 +24,6 @@ export default () => ({ ...@@ -24,4 +24,6 @@ export default () => ({
showTreeList: showTreeList:
storedTreeShow === null ? bp.getBreakpointSize() !== 'xs' : storedTreeShow === 'true', storedTreeShow === null ? bp.getBreakpointSize() !== 'xs' : storedTreeShow === 'true',
currentDiffFileId: '', currentDiffFileId: '',
projectPath: '',
commentForms: [],
}); });
...@@ -14,3 +14,7 @@ export const REMOVE_LINE_DISCUSSIONS_FOR_FILE = 'REMOVE_LINE_DISCUSSIONS_FOR_FIL ...@@ -14,3 +14,7 @@ export const REMOVE_LINE_DISCUSSIONS_FOR_FILE = 'REMOVE_LINE_DISCUSSIONS_FOR_FIL
export const TOGGLE_FOLDER_OPEN = 'TOGGLE_FOLDER_OPEN'; export const TOGGLE_FOLDER_OPEN = 'TOGGLE_FOLDER_OPEN';
export const TOGGLE_SHOW_TREE_LIST = 'TOGGLE_SHOW_TREE_LIST'; export const TOGGLE_SHOW_TREE_LIST = 'TOGGLE_SHOW_TREE_LIST';
export const UPDATE_CURRENT_DIFF_FILE_ID = 'UPDATE_CURRENT_DIFF_FILE_ID'; export const UPDATE_CURRENT_DIFF_FILE_ID = 'UPDATE_CURRENT_DIFF_FILE_ID';
export const OPEN_DIFF_FILE_COMMENT_FORM = 'OPEN_DIFF_FILE_COMMENT_FORM';
export const UPDATE_DIFF_FILE_COMMENT_FORM = 'UPDATE_DIFF_FILE_COMMENT_FORM';
export const CLOSE_DIFF_FILE_COMMENT_FORM = 'CLOSE_DIFF_FILE_COMMENT_FORM';
...@@ -153,9 +153,10 @@ export default { ...@@ -153,9 +153,10 @@ export default {
}); });
}, },
[types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode }) { [types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode, id }) {
const selectedFile = state.diffFiles.find(f => f.fileHash === fileHash); const selectedFile = state.diffFiles.find(f => f.fileHash === fileHash);
if (selectedFile) { if (selectedFile) {
if (selectedFile.parallelDiffLines) {
const targetLine = selectedFile.parallelDiffLines.find( const targetLine = selectedFile.parallelDiffLines.find(
line => line =>
(line.left && line.left.lineCode === lineCode) || (line.left && line.left.lineCode === lineCode) ||
...@@ -168,6 +169,7 @@ export default { ...@@ -168,6 +169,7 @@ export default {
discussions: [], discussions: [],
}); });
} }
}
if (selectedFile.highlightedDiffLines) { if (selectedFile.highlightedDiffLines) {
const targetInlineLine = selectedFile.highlightedDiffLines.find( const targetInlineLine = selectedFile.highlightedDiffLines.find(
...@@ -180,6 +182,12 @@ export default { ...@@ -180,6 +182,12 @@ export default {
}); });
} }
} }
if (selectedFile.discussions && selectedFile.discussions.length) {
selectedFile.discussions = selectedFile.discussions.filter(
discussion => discussion.id !== id,
);
}
} }
}, },
[types.TOGGLE_FOLDER_OPEN](state, path) { [types.TOGGLE_FOLDER_OPEN](state, path) {
...@@ -191,4 +199,25 @@ export default { ...@@ -191,4 +199,25 @@ export default {
[types.UPDATE_CURRENT_DIFF_FILE_ID](state, fileId) { [types.UPDATE_CURRENT_DIFF_FILE_ID](state, fileId) {
state.currentDiffFileId = fileId; state.currentDiffFileId = fileId;
}, },
[types.OPEN_DIFF_FILE_COMMENT_FORM](state, formData) {
state.commentForms.push({
...formData,
});
},
[types.UPDATE_DIFF_FILE_COMMENT_FORM](state, formData) {
const { fileHash } = formData;
state.commentForms = state.commentForms.map(form => {
if (form.fileHash === fileHash) {
return {
...formData,
};
}
return form;
});
},
[types.CLOSE_DIFF_FILE_COMMENT_FORM](state, fileHash) {
state.commentForms = state.commentForms.filter(form => form.fileHash !== fileHash);
},
}; };
import _ from 'underscore'; import _ from 'underscore';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { diffModes } from '~/ide/constants';
import { import {
LINE_POSITION_LEFT, LINE_POSITION_LEFT,
LINE_POSITION_RIGHT, LINE_POSITION_RIGHT,
...@@ -34,6 +35,7 @@ export function getFormData(params) { ...@@ -34,6 +35,7 @@ export function getFormData(params) {
noteTargetLine, noteTargetLine,
diffViewType, diffViewType,
linePosition, linePosition,
positionType,
} = params; } = params;
const position = JSON.stringify({ const position = JSON.stringify({
...@@ -42,9 +44,13 @@ export function getFormData(params) { ...@@ -42,9 +44,13 @@ export function getFormData(params) {
head_sha: diffFile.diffRefs.headSha, head_sha: diffFile.diffRefs.headSha,
old_path: diffFile.oldPath, old_path: diffFile.oldPath,
new_path: diffFile.newPath, new_path: diffFile.newPath,
position_type: TEXT_DIFF_POSITION_TYPE, position_type: positionType || TEXT_DIFF_POSITION_TYPE,
old_line: noteTargetLine.oldLine, old_line: noteTargetLine ? noteTargetLine.oldLine : null,
new_line: noteTargetLine.newLine, new_line: noteTargetLine ? noteTargetLine.newLine : null,
x: params.x,
y: params.y,
width: params.width,
height: params.height,
}); });
const postData = { const postData = {
...@@ -66,7 +72,7 @@ export function getFormData(params) { ...@@ -66,7 +72,7 @@ export function getFormData(params) {
diffFile.diffRefs.startSha && diffFile.diffRefs.headSha diffFile.diffRefs.startSha && diffFile.diffRefs.headSha
? DIFF_NOTE_TYPE ? DIFF_NOTE_TYPE
: LEGACY_DIFF_NOTE_TYPE, : LEGACY_DIFF_NOTE_TYPE,
line_code: noteTargetLine.lineCode, line_code: noteTargetLine ? noteTargetLine.lineCode : null,
}, },
}; };
...@@ -225,6 +231,7 @@ export function prepareDiffData(diffData) { ...@@ -225,6 +231,7 @@ export function prepareDiffData(diffData) {
Object.assign(file, { Object.assign(file, {
renderIt: showingLines < LINES_TO_BE_RENDERED_DIRECTLY, renderIt: showingLines < LINES_TO_BE_RENDERED_DIRECTLY,
collapsed: file.text && showingLines > MAX_LINES_TO_BE_RENDERED, collapsed: file.text && showingLines > MAX_LINES_TO_BE_RENDERED,
discussions: [],
}); });
} }
} }
...@@ -320,3 +327,8 @@ export const generateTreeList = files => ...@@ -320,3 +327,8 @@ export const generateTreeList = files =>
}, },
{ treeEntries: {}, tree: [] }, { treeEntries: {}, tree: [] },
); );
export const getDiffMode = diffFile => {
const diffModeKey = Object.keys(diffModes).find(key => diffFile[`${key}File`]);
return diffModes[diffModeKey] || diffModes.replaced;
};
<script> <script>
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import tablePagination from '../../vue_shared/components/table_pagination.vue'; import tablePagination from '../../vue_shared/components/table_pagination.vue';
import environmentTable from '../components/environments_table.vue'; import environmentTable from '../components/environments_table.vue';
...@@ -6,6 +7,7 @@ export default { ...@@ -6,6 +7,7 @@ export default {
components: { components: {
environmentTable, environmentTable,
tablePagination, tablePagination,
GlLoadingIcon,
}, },
props: { props: {
isLoading: { isLoading: {
......
...@@ -4,6 +4,7 @@ import { formatTime } from '~/lib/utils/datetime_utility'; ...@@ -4,6 +4,7 @@ import { formatTime } from '~/lib/utils/datetime_utility';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default { export default {
directives: { directives: {
...@@ -11,6 +12,7 @@ export default { ...@@ -11,6 +12,7 @@ export default {
}, },
components: { components: {
Icon, Icon,
GlLoadingIcon,
}, },
props: { props: {
actions: { actions: {
......
...@@ -9,10 +9,12 @@ import { s__ } from '~/locale'; ...@@ -9,10 +9,12 @@ import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default { export default {
components: { components: {
Icon, Icon,
GlLoadingIcon,
}, },
directives: { directives: {
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
/** /**
* Render environments table. * Render environments table.
*/ */
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import environmentItem from './environment_item.vue'; import environmentItem from './environment_item.vue';
import deployBoard from 'ee/environments/components/deploy_board_component.vue'; // eslint-disable-line import/order import deployBoard from 'ee/environments/components/deploy_board_component.vue'; // eslint-disable-line import/order
...@@ -10,6 +11,7 @@ export default { ...@@ -10,6 +11,7 @@ export default {
components: { components: {
environmentItem, environmentItem,
deployBoard, deployBoard,
GlLoadingIcon,
}, },
props: { props: {
......
<script> <script>
import { mapState, mapActions, mapGetters } from 'vuex'; import { mapState, mapActions, mapGetters } from 'vuex';
import AccessorUtilities from '~/lib/utils/accessor'; import AccessorUtilities from '~/lib/utils/accessor';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import store from '../store/'; import store from '../store/';
import { FREQUENT_ITEMS, STORAGE_KEY } from '../constants'; import { FREQUENT_ITEMS, STORAGE_KEY } from '../constants';
...@@ -14,6 +15,7 @@ export default { ...@@ -14,6 +15,7 @@ export default {
components: { components: {
FrequentItemsSearchInput, FrequentItemsSearchInput,
FrequentItemsList, FrequentItemsList,
GlLoadingIcon,
}, },
mixins: [frequentItemsMixin], mixins: [frequentItemsMixin],
props: { props: {
......
...@@ -8,6 +8,7 @@ import { HIDDEN_CLASS } from '~/lib/utils/constants'; ...@@ -8,6 +8,7 @@ import { HIDDEN_CLASS } from '~/lib/utils/constants';
import { getParameterByName } from '~/lib/utils/common_utils'; import { getParameterByName } from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility'; import { mergeUrlParams } from '~/lib/utils/url_utility';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import { COMMON_STR, CONTENT_LIST_CLASS } from '../constants'; import { COMMON_STR, CONTENT_LIST_CLASS } from '../constants';
import groupsComponent from './groups.vue'; import groupsComponent from './groups.vue';
...@@ -16,6 +17,7 @@ export default { ...@@ -16,6 +17,7 @@ export default {
components: { components: {
DeprecatedModal, DeprecatedModal,
groupsComponent, groupsComponent,
GlLoadingIcon,
}, },
props: { props: {
action: { action: {
......
...@@ -2,12 +2,14 @@ ...@@ -2,12 +2,14 @@
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import _ from 'underscore'; import _ from 'underscore';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Item from './item.vue'; import Item from './item.vue';
export default { export default {
components: { components: {
Item, Item,
Icon, Icon,
GlLoadingIcon,
}, },
data() { data() {
return { return {
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default { export default {
components: {
GlLoadingIcon,
},
props: { props: {
message: { message: {
type: Object, type: Object,
......
...@@ -2,10 +2,12 @@ ...@@ -2,10 +2,12 @@
import $ from 'jquery'; import $ from 'jquery';
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue'; import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default { export default {
components: { components: {
DropdownButton, DropdownButton,
GlLoadingIcon,
}, },
props: { props: {
data: { data: {
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Stage from './stage.vue'; import Stage from './stage.vue';
export default { export default {
components: { components: {
Stage, Stage,
GlLoadingIcon,
}, },
props: { props: {
stages: { stages: {
......
<script> <script>
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
import Icon from '../../../vue_shared/components/icon.vue'; import Icon from '../../../vue_shared/components/icon.vue';
import CiIcon from '../../../vue_shared/components/ci_icon.vue'; import CiIcon from '../../../vue_shared/components/ci_icon.vue';
...@@ -12,6 +13,7 @@ export default { ...@@ -12,6 +13,7 @@ export default {
Icon, Icon,
CiIcon, CiIcon,
Item, Item,
GlLoadingIcon,
}, },
props: { props: {
stage: { stage: {
......
...@@ -3,6 +3,7 @@ import { mapActions, mapState } from 'vuex'; ...@@ -3,6 +3,7 @@ import { mapActions, mapState } from 'vuex';
import _ from 'underscore'; import _ from 'underscore';
import { __ } from '~/locale'; import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Item from './item.vue'; import Item from './item.vue';
import TokenedInput from '../shared/tokened_input.vue'; import TokenedInput from '../shared/tokened_input.vue';
...@@ -16,6 +17,7 @@ export default { ...@@ -16,6 +17,7 @@ export default {
TokenedInput, TokenedInput,
Item, Item,
Icon, Icon,
GlLoadingIcon,
}, },
data() { data() {
return { return {
......
<script> <script>
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore'; import _ from 'underscore';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import { sprintf, __ } from '../../../locale'; import { sprintf, __ } from '../../../locale';
import Icon from '../../../vue_shared/components/icon.vue'; import Icon from '../../../vue_shared/components/icon.vue';
import CiIcon from '../../../vue_shared/components/ci_icon.vue'; import CiIcon from '../../../vue_shared/components/ci_icon.vue';
...@@ -17,6 +18,7 @@ export default { ...@@ -17,6 +18,7 @@ export default {
Tab, Tab,
JobsList, JobsList,
EmptyState, EmptyState,
GlLoadingIcon,
}, },
computed: { computed: {
...mapState(['pipelinesEmptyStateSvgPath', 'links']), ...mapState(['pipelinesEmptyStateSvgPath', 'links']),
......
...@@ -3,6 +3,7 @@ import { mapActions, mapGetters, mapState } from 'vuex'; ...@@ -3,6 +3,7 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore'; import _ from 'underscore';
import { Manager } from 'smooshpack'; import { Manager } from 'smooshpack';
import { listen } from 'codesandbox-api'; import { listen } from 'codesandbox-api';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Navigator from './navigator.vue'; import Navigator from './navigator.vue';
import { packageJsonPath } from '../../constants'; import { packageJsonPath } from '../../constants';
import { createPathWithExt } from '../../utils'; import { createPathWithExt } from '../../utils';
...@@ -10,6 +11,7 @@ import { createPathWithExt } from '../../utils'; ...@@ -10,6 +11,7 @@ import { createPathWithExt } from '../../utils';
export default { export default {
components: { components: {
Navigator, Navigator,
GlLoadingIcon,
}, },
data() { data() {
return { return {
......
<script> <script>
import { listen } from 'codesandbox-api'; import { listen } from 'codesandbox-api';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default { export default {
components: { components: {
Icon, Icon,
GlLoadingIcon,
}, },
props: { props: {
manager: { manager: {
......
...@@ -3,12 +3,14 @@ import _ from 'underscore'; ...@@ -3,12 +3,14 @@ import _ from 'underscore';
import { mapGetters, mapState, mapActions } from 'vuex'; import { mapGetters, mapState, mapActions } from 'vuex';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui'; import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import { isScrolledToBottom } from '~/lib/utils/scroll_utils'; import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
import { polyfillSticky } from '~/lib/utils/sticky';
import bp from '~/breakpoints'; import bp from '~/breakpoints';
import CiHeader from '~/vue_shared/components/header_ci_component.vue'; import CiHeader from '~/vue_shared/components/header_ci_component.vue';
import Callout from '~/vue_shared/components/callout.vue'; import Callout from '~/vue_shared/components/callout.vue';
// ee-only start // ee-only start
import SharedRunner from 'ee/jobs/components/shared_runner_limit_block.vue'; import SharedRunner from 'ee/jobs/components/shared_runner_limit_block.vue';
// ee-only end // ee-only end
import Icon from '~/vue_shared/components/icon.vue';
import createStore from '../store'; import createStore from '../store';
import EmptyState from './empty_state.vue'; import EmptyState from './empty_state.vue';
import EnvironmentsBlock from './environments_block.vue'; import EnvironmentsBlock from './environments_block.vue';
...@@ -28,6 +30,7 @@ export default { ...@@ -28,6 +30,7 @@ export default {
EnvironmentsBlock, EnvironmentsBlock,
ErasedBlock, ErasedBlock,
GlLoadingIcon, GlLoadingIcon,
Icon,
Log, Log,
LogTopBar, LogTopBar,
StuckBlock, StuckBlock,
...@@ -102,6 +105,14 @@ export default { ...@@ -102,6 +105,14 @@ export default {
if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) { if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) {
this.fetchStages(); this.fetchStages();
} }
if (newVal.archived) {
this.$nextTick(() => {
if (this.$refs.sticky) {
polyfillSticky(this.$refs.sticky);
}
});
}
}, },
}, },
created() { created() {
...@@ -119,16 +130,13 @@ export default { ...@@ -119,16 +130,13 @@ export default {
window.addEventListener('resize', this.onResize); window.addEventListener('resize', this.onResize);
window.addEventListener('scroll', this.updateScroll); window.addEventListener('scroll', this.updateScroll);
}, },
mounted() { mounted() {
this.updateSidebar(); this.updateSidebar();
}, },
destroyed() { destroyed() {
window.removeEventListener('resize', this.onResize); window.removeEventListener('resize', this.onResize);
window.removeEventListener('scroll', this.updateScroll); window.removeEventListener('scroll', this.updateScroll);
}, },
methods: { methods: {
...mapActions([ ...mapActions([
'setJobEndpoint', 'setJobEndpoint',
...@@ -231,14 +239,28 @@ export default { ...@@ -231,14 +239,28 @@ export default {
:erased-at="job.erased_at" :erased-at="job.erased_at"
/> />
<div
v-if="job.archived"
ref="sticky"
class="js-archived-job prepend-top-default archived-sticky sticky-top"
>
<icon
name="lock"
class="align-text-bottom"
/>
{{ __('This job is archived. Only the complete pipeline can be retried.') }}
</div>
<!--job log --> <!--job log -->
<div <div
v-if="hasTrace" v-if="hasTrace"
class="build-trace-container prepend-top-default"> class="build-trace-container"
>
<log-top-bar <log-top-bar
:class="{ :class="{
'sidebar-expanded': isSidebarOpen, 'sidebar-expanded': isSidebarOpen,
'sidebar-collapsed': !isSidebarOpen 'sidebar-collapsed': !isSidebarOpen,
'has-archived-block': job.archived
}" }"
:erase-path="job.erase_path" :erase-path="job.erase_path"
:size="traceSize" :size="traceSize"
......
...@@ -69,7 +69,7 @@ export default { ...@@ -69,7 +69,7 @@ export default {
}; };
</script> </script>
<template> <template>
<div class="top-bar affix"> <div class="top-bar">
<!-- truncate information --> <!-- truncate information -->
<div class="js-truncated-info truncated-info d-none d-sm-block float-left"> <div class="js-truncated-info truncated-info d-none d-sm-block float-left">
<template v-if="isTraceSizeVisible"> <template v-if="isTraceSizeVisible">
......
...@@ -14,7 +14,7 @@ window.timeago = timeago; ...@@ -14,7 +14,7 @@ window.timeago = timeago;
* *
* @param {Boolean} abbreviated * @param {Boolean} abbreviated
*/ */
const getMonthNames = abbreviated => { export const getMonthNames = abbreviated => {
if (abbreviated) { if (abbreviated) {
return [ return [
s__('Jan'), s__('Jan'),
......
...@@ -11,7 +11,6 @@ import bp from './breakpoints'; ...@@ -11,7 +11,6 @@ import bp from './breakpoints';
import { parseUrlPathname, handleLocationHash, isMetaClick } from './lib/utils/common_utils'; import { parseUrlPathname, handleLocationHash, isMetaClick } from './lib/utils/common_utils';
import { isInVueNoteablePage } from './lib/utils/dom_utils'; import { isInVueNoteablePage } from './lib/utils/dom_utils';
import { getLocationHash } from './lib/utils/url_utility'; import { getLocationHash } from './lib/utils/url_utility';
import initDiscussionTab from './image_diff/init_discussion_tab';
import Diff from './diff'; import Diff from './diff';
import { localTimeAgo } from './lib/utils/datetime_utility'; import { localTimeAgo } from './lib/utils/datetime_utility';
import syntaxHighlight from './syntax_highlight'; import syntaxHighlight from './syntax_highlight';
...@@ -207,8 +206,6 @@ export default class MergeRequestTabs { ...@@ -207,8 +206,6 @@ export default class MergeRequestTabs {
} }
this.resetViewContainer(); this.resetViewContainer();
this.destroyPipelinesView(); this.destroyPipelinesView();
initDiscussionTab();
} }
if (this.setUrl) { if (this.setUrl) {
this.setCurrentAction(action); this.setCurrentAction(action);
......
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import imageDiffHelper from '~/image_diff/helpers/index';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue'; import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
import { GlSkeletonLoading } from '@gitlab-org/gitlab-ui'; import { GlSkeletonLoading } from '@gitlab-org/gitlab-ui';
import { trimFirstCharOfLineContent } from '~/diffs/store/utils'; import { trimFirstCharOfLineContent, getDiffMode } from '~/diffs/store/utils';
export default { export default {
components: { components: {
DiffFileHeader, DiffFileHeader,
GlSkeletonLoading, GlSkeletonLoading,
DiffViewer,
ImageDiffOverlay,
}, },
props: { props: {
discussion: { discussion: {
...@@ -25,7 +28,11 @@ export default { ...@@ -25,7 +28,11 @@ export default {
computed: { computed: {
...mapState({ ...mapState({
noteableData: state => state.notes.noteableData, noteableData: state => state.notes.noteableData,
projectPath: state => state.diffs.projectPath,
}), }),
diffMode() {
return getDiffMode(this.diffFile);
},
hasTruncatedDiffLines() { hasTruncatedDiffLines() {
return this.discussion.truncatedDiffLines && this.discussion.truncatedDiffLines.length !== 0; return this.discussion.truncatedDiffLines && this.discussion.truncatedDiffLines.length !== 0;
}, },
...@@ -62,11 +69,7 @@ export default { ...@@ -62,11 +69,7 @@ export default {
}, },
}, },
mounted() { mounted() {
if (this.isImageDiff) { if (!this.hasTruncatedDiffLines) {
const canCreateNote = false;
const renderCommentBadge = true;
imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge);
} else if (!this.hasTruncatedDiffLines) {
this.fetchDiff(); this.fetchDiff();
} }
}, },
...@@ -160,7 +163,24 @@ export default { ...@@ -160,7 +163,24 @@ export default {
<div <div
v-else v-else
> >
<div v-html="imageDiffHtml"></div> <diff-viewer
:diff-mode="diffMode"
:new-path="diffFile.newPath"
:new-sha="diffFile.diffRefs.headSha"
:old-path="diffFile.oldPath"
:old-sha="diffFile.diffRefs.baseSha"
:file-hash="diffFile.fileHash"
:project-path="projectPath"
>
<image-diff-overlay
slot="image-overlay"
:discussions="discussion"
:file-hash="diffFile.fileHash"
:show-comment-icon="true"
:should-toggle-discussion="false"
badge-class="image-comment-badge"
/>
</diff-viewer>
<slot></slot> <slot></slot>
</div> </div>
</div> </div>
......
...@@ -9,11 +9,13 @@ import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg'; ...@@ -9,11 +9,13 @@ import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg';
import ellipsisSvg from 'icons/_ellipsis_v.svg'; import ellipsisSvg from 'icons/_ellipsis_v.svg';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default { export default {
name: 'NoteActions', name: 'NoteActions',
components: { components: {
Icon, Icon,
GlLoadingIcon,
}, },
directives: { directives: {
tooltip, tooltip,
......
...@@ -360,11 +360,18 @@ Please check your network connection and try again.`; ...@@ -360,11 +360,18 @@ Please check your network connection and try again.`;
<ul class="notes"> <ul class="notes">
<component <component
:is="componentName(note)" :is="componentName(note)"
v-for="note in discussion.notes" v-for="(note, index) in discussion.notes"
:key="note.id" :key="note.id"
:note="componentData(note)" :note="componentData(note)"
@handleDeleteNote="deleteNoteHandler" @handleDeleteNote="deleteNoteHandler"
/> >
<slot
v-if="index === 0"
slot="avatar-badge"
name="avatar-badge"
>
</slot>
</component>
</ul> </ul>
<draft-note <draft-note
v-if="showDraft(discussion.reply_id)" v-if="showDraft(discussion.reply_id)"
......
...@@ -185,7 +185,13 @@ export default { ...@@ -185,7 +185,13 @@ export default {
:img-src="author.avatar_url" :img-src="author.avatar_url"
:img-alt="author.name" :img-alt="author.name"
:img-size="40" :img-size="40"
/> >
<slot
slot="avatar-badge"
name="avatar-badge"
>
</slot>
</user-avatar-link>
</div> </div>
<div class="timeline-content"> <div class="timeline-content">
<div class="note-header"> <div class="note-header">
......
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import StageColumnComponent from './stage_column_component.vue'; import StageColumnComponent from './stage_column_component.vue';
import LinkedPipelinesColumn from 'ee/pipelines/components/graph/linked_pipelines_column.vue'; // eslint-disable-line import/order import LinkedPipelinesColumn from 'ee/pipelines/components/graph/linked_pipelines_column.vue'; // eslint-disable-line import/order
...@@ -7,6 +8,7 @@ export default { ...@@ -7,6 +8,7 @@ export default {
components: { components: {
LinkedPipelinesColumn, LinkedPipelinesColumn,
StageColumnComponent, StageColumnComponent,
GlLoadingIcon,
}, },
props: { props: {
isLoading: { isLoading: {
......
<script> <script>
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import ciHeader from '../../vue_shared/components/header_ci_component.vue'; import ciHeader from '../../vue_shared/components/header_ci_component.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
...@@ -6,6 +7,7 @@ export default { ...@@ -6,6 +7,7 @@ export default {
name: 'PipelineHeaderSection', name: 'PipelineHeaderSection',
components: { components: {
ciHeader, ciHeader,
GlLoadingIcon,
}, },
props: { props: {
pipeline: { pipeline: {
......
...@@ -4,6 +4,7 @@ import eventHub from '../event_hub'; ...@@ -4,6 +4,7 @@ import eventHub from '../event_hub';
import Icon from '../../vue_shared/components/icon.vue'; import Icon from '../../vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default { export default {
directives: { directives: {
...@@ -12,6 +13,7 @@ export default { ...@@ -12,6 +13,7 @@ export default {
components: { components: {
Icon, Icon,
GlCountdown, GlCountdown,
GlLoadingIcon,
}, },
props: { props: {
actions: { actions: {
......
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
*/ */
import $ from 'jquery'; import $ from 'jquery';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import { __ } from '../../locale'; import { __ } from '../../locale';
import Flash from '../../flash'; import Flash from '../../flash';
import axios from '../../lib/utils/axios_utils'; import axios from '../../lib/utils/axios_utils';
...@@ -26,6 +27,7 @@ export default { ...@@ -26,6 +27,7 @@ export default {
components: { components: {
Icon, Icon,
JobItem, JobItem,
GlLoadingIcon,
}, },
directives: { directives: {
......
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import { __ } from '../../locale'; import { __ } from '../../locale';
import Flash from '../../flash'; import Flash from '../../flash';
import Poll from '../../lib/utils/poll'; import Poll from '../../lib/utils/poll';
...@@ -13,6 +14,7 @@ export default { ...@@ -13,6 +14,7 @@ export default {
PipelinesTableComponent, PipelinesTableComponent,
SvgBlankState, SvgBlankState,
EmptyState, EmptyState,
GlLoadingIcon,
}, },
data() { data() {
return { return {
......
...@@ -2,6 +2,7 @@ import _ from 'underscore'; ...@@ -2,6 +2,7 @@ import _ from 'underscore';
import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue'; import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue'; import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue'; import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import store from '../store'; import store from '../store';
...@@ -11,6 +12,7 @@ export default { ...@@ -11,6 +12,7 @@ export default {
DropdownButton, DropdownButton,
DropdownSearchInput, DropdownSearchInput,
DropdownHiddenInput, DropdownHiddenInput,
GlLoadingIcon,
}, },
props: { props: {
fieldId: { fieldId: {
......
...@@ -5,6 +5,7 @@ import Poll from '~/lib/utils/poll'; ...@@ -5,6 +5,7 @@ import Poll from '~/lib/utils/poll';
import Flash from '~/flash'; import Flash from '~/flash';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import CommitPipelineService from '../services/commit_pipeline_service'; import CommitPipelineService from '../services/commit_pipeline_service';
export default { export default {
...@@ -13,6 +14,7 @@ export default { ...@@ -13,6 +14,7 @@ export default {
}, },
components: { components: {
ciIcon, ciIcon,
GlLoadingIcon,
}, },
props: { props: {
endpoint: { endpoint: {
......
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Flash from '../../flash'; import Flash from '../../flash';
import store from '../stores'; import store from '../stores';
import collapsibleContainer from './collapsible_container.vue'; import collapsibleContainer from './collapsible_container.vue';
...@@ -9,6 +10,7 @@ export default { ...@@ -9,6 +10,7 @@ export default {
name: 'RegistryListApp', name: 'RegistryListApp',
components: { components: {
collapsibleContainer, collapsibleContainer,
GlLoadingIcon,
}, },
props: { props: {
endpoint: { endpoint: {
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Flash from '../../flash'; import Flash from '../../flash';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
...@@ -12,6 +13,7 @@ export default { ...@@ -12,6 +13,7 @@ export default {
components: { components: {
clipboardButton, clipboardButton,
tableRegistry, tableRegistry,
GlLoadingIcon,
}, },
directives: { directives: {
tooltip, tooltip,
......
<script> <script>
import CiIcon from '~/vue_shared/components/ci_icon.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Popover from '~/vue_shared/components/help_popover.vue'; import Popover from '~/vue_shared/components/help_popover.vue';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
/** /**
* Renders the summary row for each report * Renders the summary row for each report
...@@ -15,6 +16,7 @@ export default { ...@@ -15,6 +16,7 @@ export default {
components: { components: {
CiIcon, CiIcon,
Popover, Popover,
GlLoadingIcon,
}, },
props: { props: {
summary: { summary: {
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { __, n__, sprintf } from '~/locale'; import { __, n__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default { export default {
directives: { directives: {
...@@ -9,6 +10,7 @@ export default { ...@@ -9,6 +10,7 @@ export default {
}, },
components: { components: {
userAvatarImage, userAvatarImage,
GlLoadingIcon,
}, },
props: { props: {
loading: { loading: {
......
<script> <script>
import { __ } from '~/locale'; import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
...@@ -13,6 +14,7 @@ export default { ...@@ -13,6 +14,7 @@ export default {
}, },
components: { components: {
Icon, Icon,
GlLoadingIcon,
}, },
props: { props: {
issuableId: { issuableId: {
......
<script> <script>
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import ciIcon from '../../vue_shared/components/ci_icon.vue'; import ciIcon from '../../vue_shared/components/ci_icon.vue';
export default { export default {
components: { components: {
ciIcon, ciIcon,
GlLoadingIcon,
}, },
props: { props: {
status: { status: {
......
<script> <script>
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import statusIcon from '../mr_widget_status_icon.vue'; import statusIcon from '../mr_widget_status_icon.vue';
...@@ -6,6 +7,7 @@ export default { ...@@ -6,6 +7,7 @@ export default {
name: 'MRWidgetAutoMergeFailed', name: 'MRWidgetAutoMergeFailed',
components: { components: {
statusIcon, statusIcon,
GlLoadingIcon,
}, },
props: { props: {
mr: { mr: {
......
...@@ -6,6 +6,7 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; ...@@ -6,6 +6,7 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import MrWidgetAuthorTime from '../../components/mr_widget_author_time.vue'; import MrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
import statusIcon from '../mr_widget_status_icon.vue'; import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default { export default {
name: 'MRWidgetMerged', name: 'MRWidgetMerged',
...@@ -16,6 +17,7 @@ export default { ...@@ -16,6 +17,7 @@ export default {
MrWidgetAuthorTime, MrWidgetAuthorTime,
statusIcon, statusIcon,
ClipboardButton, ClipboardButton,
GlLoadingIcon,
}, },
props: { props: {
mr: { mr: {
......
<script> <script>
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import simplePoll from '../../../lib/utils/simple_poll'; import simplePoll from '../../../lib/utils/simple_poll';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import statusIcon from '../mr_widget_status_icon.vue'; import statusIcon from '../mr_widget_status_icon.vue';
...@@ -8,6 +9,7 @@ export default { ...@@ -8,6 +9,7 @@ export default {
name: 'MRWidgetRebase', name: 'MRWidgetRebase',
components: { components: {
statusIcon, statusIcon,
GlLoadingIcon,
}, },
props: { props: {
mr: { mr: {
......
...@@ -17,19 +17,37 @@ export default { ...@@ -17,19 +17,37 @@ export default {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
innerCssClasses: {
type: [Array, Object, String],
required: false,
default: '',
},
}, },
data() { data() {
return { return {
width: 0, width: 0,
height: 0, height: 0,
isZoomable: false, isLoaded: false,
isZoomed: false,
}; };
}, },
computed: { computed: {
fileSizeReadable() { fileSizeReadable() {
return numberToHumanSize(this.fileSize); return numberToHumanSize(this.fileSize);
}, },
dimensionStyles() {
if (!this.isLoaded) return {};
return {
width: `${this.width}px`,
height: `${this.height}px`,
};
},
hasFileSize() {
return this.fileSize > 0;
},
hasDimensions() {
return this.width && this.height;
},
}, },
beforeDestroy() { beforeDestroy() {
window.removeEventListener('resize', this.resizeThrottled, false); window.removeEventListener('resize', this.resizeThrottled, false);
...@@ -48,51 +66,52 @@ export default { ...@@ -48,51 +66,52 @@ export default {
const { contentImg } = this.$refs; const { contentImg } = this.$refs;
if (contentImg) { if (contentImg) {
this.isZoomable =
contentImg.naturalWidth > contentImg.width ||
contentImg.naturalHeight > contentImg.height;
this.width = contentImg.naturalWidth; this.width = contentImg.naturalWidth;
this.height = contentImg.naturalHeight; this.height = contentImg.naturalHeight;
this.$nextTick(() => {
this.isLoaded = true;
this.$emit('imgLoaded', { this.$emit('imgLoaded', {
width: this.width, width: this.width,
height: this.height, height: this.height,
renderedWidth: contentImg.clientWidth, renderedWidth: contentImg.clientWidth,
renderedHeight: contentImg.clientHeight, renderedHeight: contentImg.clientHeight,
}); });
});
} }
}, },
onImgClick() {
if (this.isZoomable) this.isZoomed = !this.isZoomed;
},
}, },
}; };
</script> </script>
<template> <template>
<div class="file-container"> <div>
<div class="file-content image_file"> <div
:class="innerCssClasses"
:style="dimensionStyles"
class="position-relative"
>
<img <img
ref="contentImg" ref="contentImg"
:class="{ 'is-zoomable': isZoomable, 'is-zoomed': isZoomed }"
:src="path" :src="path"
:alt="path"
@load="onImgLoad" @load="onImgLoad"
@click="onImgClick"/> />
<slot name="image-overlay"></slot>
</div>
<p <p
v-if="renderInfo" v-if="renderInfo"
class="file-info prepend-top-10"> class="image-info"
<template v-if="fileSize>0"> >
<template v-if="hasFileSize">
{{ fileSizeReadable }} {{ fileSizeReadable }}
</template> </template>
<template v-if="fileSize>0 && width && height"> <template v-if="hasFileSize && hasDimensions">
| |
</template> </template>
<template v-if="width && height"> <template v-if="hasDimensions">
W: {{ width }} | H: {{ height }} <strong>W</strong>: {{ width }} | <strong>H</strong>: {{ height }}
</template> </template>
</p> </p>
</div> </div>
</div>
</template> </template>
...@@ -69,6 +69,13 @@ export default { ...@@ -69,6 +69,13 @@ export default {
:new-path="fullNewPath" :new-path="fullNewPath"
:old-path="fullOldPath" :old-path="fullOldPath"
:project-path="projectPath" :project-path="projectPath"
/> >
<slot
slot="image-overlay"
name="image-overlay"
>
</slot>
</component>
<slot></slot>
</div> </div>
</template> </template>
...@@ -15,11 +15,6 @@ export default { ...@@ -15,11 +15,6 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
projectPath: {
type: String,
required: false,
default: '',
},
}, },
data() { data() {
return { return {
...@@ -120,7 +115,6 @@ export default { ...@@ -120,7 +115,6 @@ export default {
key="onionOldImg" key="onionOldImg"
:render-info="false" :render-info="false"
:path="oldPath" :path="oldPath"
:project-path="projectPath"
@imgLoaded="onionOldImgLoaded" @imgLoaded="onionOldImgLoaded"
/> />
</div> </div>
...@@ -136,9 +130,14 @@ export default { ...@@ -136,9 +130,14 @@ export default {
key="onionNewImg" key="onionNewImg"
:render-info="false" :render-info="false"
:path="newPath" :path="newPath"
:project-path="projectPath"
@imgLoaded="onionNewImgLoaded" @imgLoaded="onionNewImgLoaded"
/> >
<slot
slot="image-overlay"
name="image-overlay"
>
</slot>
</image-viewer>
</div> </div>
<div class="controls"> <div class="controls">
<div class="transparent"></div> <div class="transparent"></div>
......
...@@ -16,11 +16,6 @@ export default { ...@@ -16,11 +16,6 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
projectPath: {
type: String,
required: false,
default: '',
},
}, },
data() { data() {
return { return {
...@@ -117,16 +112,14 @@ export default { ...@@ -117,16 +112,14 @@ export default {
'height': swipeMaxPixelHeight, 'height': swipeMaxPixelHeight,
}" }"
class="swipe-frame"> class="swipe-frame">
<div class="frame deleted">
<image-viewer <image-viewer
key="swipeOldImg" key="swipeOldImg"
ref="swipeOldImg" ref="swipeOldImg"
:render-info="false" :render-info="false"
:path="oldPath" :path="oldPath"
:project-path="projectPath" class="frame deleted"
@imgLoaded="swipeOldImgLoaded" @imgLoaded="swipeOldImgLoaded"
/> />
</div>
<div <div
ref="swipeWrap" ref="swipeWrap"
:style="{ :style="{
...@@ -134,15 +127,19 @@ export default { ...@@ -134,15 +127,19 @@ export default {
'height': swipeMaxPixelHeight, 'height': swipeMaxPixelHeight,
}" }"
class="swipe-wrap"> class="swipe-wrap">
<div class="frame added">
<image-viewer <image-viewer
key="swipeNewImg" key="swipeNewImg"
:render-info="false" :render-info="false"
:path="newPath" :path="newPath"
:project-path="projectPath" class="frame added"
@imgLoaded="swipeNewImgLoaded" @imgLoaded="swipeNewImgLoaded"
/> >
</div> <slot
slot="image-overlay"
name="image-overlay"
>
</slot>
</image-viewer>
</div> </div>
<span <span
ref="swipeBar" ref="swipeBar"
......
...@@ -14,28 +14,29 @@ export default { ...@@ -14,28 +14,29 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
projectPath: {
type: String,
required: false,
default: '',
},
}, },
}; };
</script> </script>
<template> <template>
<div class="two-up view row"> <div class="two-up view">
<div class="col-sm-6 frame deleted">
<image-viewer <image-viewer
:path="oldPath" :path="oldPath"
:project-path="projectPath" :render-info="true"
inner-css-classes="frame deleted"
class="wrap"
/> />
</div>
<div class="col-sm-6 frame added">
<image-viewer <image-viewer
:path="newPath" :path="newPath"
:project-path="projectPath" :render-info="true"
/> :inner-css-classes="['frame', 'added']"
</div> class="wrap"
>
<slot
slot="image-overlay"
name="image-overlay"
>
</slot>
</image-viewer>
</div> </div>
</template> </template>
...@@ -8,9 +8,6 @@ import { diffModes, imageViewMode } from '../constants'; ...@@ -8,9 +8,6 @@ import { diffModes, imageViewMode } from '../constants';
export default { export default {
components: { components: {
ImageViewer, ImageViewer,
TwoUpViewer,
SwipeViewer,
OnionSkinViewer,
}, },
props: { props: {
diffMode: { diffMode: {
...@@ -25,17 +22,32 @@ export default { ...@@ -25,17 +22,32 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
projectPath: {
type: String,
required: false,
default: '',
},
}, },
data() { data() {
return { return {
mode: imageViewMode.twoup, mode: imageViewMode.twoup,
}; };
}, },
computed: {
imageViewComponent() {
switch (this.mode) {
case imageViewMode.twoup:
return TwoUpViewer;
case imageViewMode.swipe:
return SwipeViewer;
case imageViewMode.onion:
return OnionSkinViewer;
default:
return undefined;
}
},
isNew() {
return this.diffMode === diffModes.new;
},
imagePath() {
return this.isNew ? this.newPath : this.oldPath;
},
},
methods: { methods: {
changeMode(newMode) { changeMode(newMode) {
this.mode = newMode; this.mode = newMode;
...@@ -52,15 +64,16 @@ export default { ...@@ -52,15 +64,16 @@ export default {
v-if="diffMode === $options.diffModes.replaced" v-if="diffMode === $options.diffModes.replaced"
class="diff-viewer"> class="diff-viewer">
<div class="image js-replaced-image"> <div class="image js-replaced-image">
<two-up-viewer <component
v-if="mode === $options.imageViewMode.twoup" :is="imageViewComponent"
v-bind="$props"/> v-bind="$props"
<swipe-viewer >
v-else-if="mode === $options.imageViewMode.swipe" <slot
v-bind="$props"/> slot="image-overlay"
<onion-skin-viewer name="image-overlay"
v-else-if="mode === $options.imageViewMode.onion" >
v-bind="$props"/> </slot>
</component>
</div> </div>
<div class="view-modes"> <div class="view-modes">
<ul class="view-modes-menu"> <ul class="view-modes-menu">
...@@ -87,23 +100,27 @@ export default { ...@@ -87,23 +100,27 @@ export default {
</li> </li>
</ul> </ul>
</div> </div>
<div class="note-container"></div>
</div>
<div
v-else-if="diffMode === $options.diffModes.new"
class="diff-viewer added">
<image-viewer
:path="newPath"
:project-path="projectPath"
/>
</div> </div>
<div <div
v-else v-else
class="diff-viewer deleted"> class="diff-viewer"
>
<div class="image">
<image-viewer <image-viewer
:path="oldPath" :path="imagePath"
:project-path="projectPath" :inner-css-classes="['frame', {
/> 'added': isNew,
'deleted': diffMode === $options.diffModes.deleted
}]"
>
<slot
v-if="isNew"
slot="image-overlay"
name="image-overlay"
>
</slot>
</image-viewer>
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { __ } from '~/locale'; import { __ } from '~/locale';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default { export default {
components: {
GlLoadingIcon,
},
props: { props: {
isDisabled: { isDisabled: {
type: Boolean, type: Boolean,
......
<script> <script>
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import getIconForFile from './file_icon/file_icon_map'; import getIconForFile from './file_icon/file_icon_map';
import icon from '../../vue_shared/components/icon.vue'; import icon from '../../vue_shared/components/icon.vue';
...@@ -17,6 +18,7 @@ import icon from '../../vue_shared/components/icon.vue'; ...@@ -17,6 +18,7 @@ import icon from '../../vue_shared/components/icon.vue';
export default { export default {
components: { components: {
icon, icon,
GlLoadingIcon,
}, },
props: { props: {
fileName: { fileName: {
......
...@@ -9,6 +9,7 @@ export default { ...@@ -9,6 +9,7 @@ export default {
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
props: { props: {
endDateString: { endDateString: {
type: String, type: String,
......
<script> <script>
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
/* eslint-disable vue/require-default-prop */ /* eslint-disable vue/require-default-prop */
/* This is a re-usable vue component for rendering a button /* This is a re-usable vue component for rendering a button
that will probably be sending off ajax requests and need that will probably be sending off ajax requests and need
...@@ -18,6 +19,9 @@ ...@@ -18,6 +19,9 @@
*/ */
export default { export default {
components: {
GlLoadingIcon,
},
props: { props: {
loading: { loading: {
type: Boolean, type: Boolean,
......
<script> <script>
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import datePicker from '../pikaday.vue'; import datePicker from '../pikaday.vue';
import toggleSidebar from './toggle_sidebar.vue'; import toggleSidebar from './toggle_sidebar.vue';
import collapsedCalendarIcon from './collapsed_calendar_icon.vue'; import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
...@@ -10,6 +11,7 @@ export default { ...@@ -10,6 +11,7 @@ export default {
datePicker, datePicker,
toggleSidebar, toggleSidebar,
collapsedCalendarIcon, collapsedCalendarIcon,
GlLoadingIcon,
}, },
props: { props: {
blockClass: { blockClass: {
......
...@@ -4,6 +4,7 @@ import { __ } from '~/locale'; ...@@ -4,6 +4,7 @@ import { __ } from '~/locale';
import LabelsSelect from '~/labels_select'; import LabelsSelect from '~/labels_select';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue'; import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import DropdownTitle from './dropdown_title.vue'; import DropdownTitle from './dropdown_title.vue';
import DropdownValue from './dropdown_value.vue'; import DropdownValue from './dropdown_value.vue';
import DropdownValueCollapsed from './dropdown_value_collapsed.vue'; import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
...@@ -24,6 +25,7 @@ export default { ...@@ -24,6 +25,7 @@ export default {
DropdownSearchInput, DropdownSearchInput,
DropdownFooter, DropdownFooter,
DropdownCreateLabel, DropdownCreateLabel,
GlLoadingIcon,
}, },
props: { props: {
showCreate: { showCreate: {
......
<script> <script>
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import { s__ } from '../../locale'; import { s__ } from '../../locale';
import icon from './icon.vue'; import icon from './icon.vue';
...@@ -10,6 +11,7 @@ const LABEL_OFF = s__('ToggleButton|Toggle Status: OFF'); ...@@ -10,6 +11,7 @@ const LABEL_OFF = s__('ToggleButton|Toggle Status: OFF');
export default { export default {
components: { components: {
icon, icon,
GlLoadingIcon,
}, },
model: { model: {
......
...@@ -99,6 +99,6 @@ export default { ...@@ -99,6 +99,6 @@ export default {
v-tooltip v-tooltip
:title="tooltipText" :title="tooltipText"
:tooltip-placement="tooltipPlacement" :tooltip-placement="tooltipPlacement"
>{{ username }}</span> >{{ username }}</span><slot name="avatar-badge"></slot>
</gl-link> </gl-link>
</template> </template>
...@@ -671,3 +671,8 @@ $modal-body-height: 134px; ...@@ -671,3 +671,8 @@ $modal-body-height: 134px;
$modal-border-color: #e9ecef; $modal-border-color: #e9ecef;
$priority-label-empty-state-width: 114px; $priority-label-empty-state-width: 114px;
/*
Issues Analytics
*/
$issues-analytics-popover-boarder-color: rgba(0, 0, 0, 0.15);
...@@ -54,9 +54,52 @@ ...@@ -54,9 +54,52 @@
@include build-trace(); @include build-trace();
} }
.archived-sticky {
top: $header-height;
border-radius: 2px 2px 0 0;
color: $orange-600;
background-color: $orange-100;
border: 1px solid $border-gray-normal;
border-bottom: 0;
padding: 3px 12px;
margin: auto;
align-items: center;
.with-performance-bar & {
top: $header-height + $performance-bar-height;
}
}
.top-bar { .top-bar {
@include build-trace-top-bar(35px); @include build-trace-top-bar(35px);
&.has-archived-block {
top: $header-height + $performance-bar-height + 28px;
}
&.affix {
top: $header-height;
// with sidebar
&.sidebar-expanded {
right: 306px;
left: 16px;
}
// without sidebar
&.sidebar-collapsed {
right: 16px;
left: 16px;
}
}
&.affix-top {
position: absolute;
right: 0;
left: 0;
top: 0;
}
.truncated-info { .truncated-info {
.truncated-info-size { .truncated-info-size {
margin: 0 5px; margin: 0 5px;
......
...@@ -421,21 +421,13 @@ ...@@ -421,21 +421,13 @@
.diff-file-container { .diff-file-container {
.frame.deleted { .frame.deleted {
border: 0;
background-color: inherit;
.image_file img {
border: 1px solid $deleted; border: 1px solid $deleted;
} background-color: inherit;
} }
.frame.added { .frame.added {
border: 0;
background-color: inherit;
.image_file img {
border: 1px solid $added; border: 1px solid $added;
} background-color: inherit;
} }
.swipe.view, .swipe.view,
...@@ -481,6 +473,11 @@ ...@@ -481,6 +473,11 @@
bottom: -25px; bottom: -25px;
} }
} }
.discussion-notes .discussion-notes {
margin-left: 0;
border-left: 0;
}
} }
.file-content .diff-file { .file-content .diff-file {
...@@ -804,7 +801,7 @@ ...@@ -804,7 +801,7 @@
// double jagged line divider // double jagged line divider
.discussion-notes + .discussion-notes::before, .discussion-notes + .discussion-notes::before,
.discussion-notes + .discussion-form::before { .diff-file-discussions + .discussion-form::before {
content: ''; content: '';
position: relative; position: relative;
display: block; display: block;
...@@ -844,6 +841,13 @@ ...@@ -844,6 +841,13 @@
background-repeat: repeat; background-repeat: repeat;
} }
.diff-file-discussions + .discussion-form::before {
width: auto;
margin-left: -16px;
margin-right: -16px;
margin-bottom: 16px;
}
.notes { .notes {
position: relative; position: relative;
} }
...@@ -870,11 +874,13 @@ ...@@ -870,11 +874,13 @@
} }
} }
.files:not([data-can-create-note]) .frame { .files:not([data-can-create-note="true"]) .frame {
cursor: auto; cursor: auto;
} }
.frame.click-to-comment { .frame,
.frame.click-to-comment,
.btn-transparent.image-diff-overlay-add-comment {
position: relative; position: relative;
cursor: image-url('illustrations/image_comment_light_cursor.svg') cursor: image-url('illustrations/image_comment_light_cursor.svg')
$image-comment-cursor-left-offset $image-comment-cursor-top-offset, $image-comment-cursor-left-offset $image-comment-cursor-top-offset,
...@@ -910,6 +916,7 @@ ...@@ -910,6 +916,7 @@
.frame .badge.badge-pill, .frame .badge.badge-pill,
.image-diff-avatar-link .badge.badge-pill, .image-diff-avatar-link .badge.badge-pill,
.user-avatar-link .badge.badge-pill,
.notes > .badge.badge-pill { .notes > .badge.badge-pill {
position: absolute; position: absolute;
background-color: $blue-400; background-color: $blue-400;
...@@ -944,7 +951,8 @@ ...@@ -944,7 +951,8 @@
} }
} }
.image-diff-avatar-link { .image-diff-avatar-link,
.user-avatar-link {
position: relative; position: relative;
.badge.badge-pill, .badge.badge-pill,
...@@ -1073,3 +1081,14 @@ ...@@ -1073,3 +1081,14 @@
top: 0; top: 0;
} }
} }
.image-diff-overlay,
.image-diff-overlay-add-comment {
top: 0;
left: 0;
&:active,
&:focus {
outline: 0;
}
}
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
# #
# Automatically sets the layout and ensures an administrator is logged in # Automatically sets the layout and ensures an administrator is logged in
class Admin::ApplicationController < ApplicationController class Admin::ApplicationController < ApplicationController
prepend EE::Admin::ApplicationController
before_action :authenticate_admin! before_action :authenticate_admin!
layout 'admin' layout 'admin'
......
# frozen_string_literal: true # frozen_string_literal: true
class Admin::ProjectsController < Admin::ApplicationController class Admin::ProjectsController < Admin::ApplicationController
prepend EE::Admin::ProjectsController
include MembersPresentation include MembersPresentation
before_action :project, only: [:show, :transfer, :repository_check] before_action :project, only: [:show, :transfer, :repository_check]
......
# frozen_string_literal: true # frozen_string_literal: true
class Projects::AutocompleteSourcesController < Projects::ApplicationController class Projects::AutocompleteSourcesController < Projects::ApplicationController
before_action :load_autocomplete_service, except: [:members] prepend EE::Projects::AutocompleteSourcesController
def members def members
render json: ::Projects::ParticipantsService.new(@project, current_user).execute(target) render json: ::Projects::ParticipantsService.new(@project, current_user).execute(target)
end end
def issues def issues
render json: @autocomplete_service.issues render json: autocomplete_service.issues
end end
def merge_requests def merge_requests
render json: @autocomplete_service.merge_requests render json: autocomplete_service.merge_requests
end end
def labels def labels
render json: @autocomplete_service.labels_as_hash(target) render json: autocomplete_service.labels_as_hash(target)
end end
def milestones def milestones
render json: @autocomplete_service.milestones render json: autocomplete_service.milestones
end end
def commands def commands
render json: @autocomplete_service.commands(target, params[:type]) render json: autocomplete_service.commands(target, params[:type])
end end
def snippets def snippets
render json: @autocomplete_service.snippets render json: autocomplete_service.snippets
end end
private private
def load_autocomplete_service def autocomplete_service
@autocomplete_service = ::Projects::AutocompleteService.new(@project, current_user) @autocomplete_service ||= ::Projects::AutocompleteService.new(@project, current_user)
end end
def target def target
......
# frozen_string_literal: true # frozen_string_literal: true
# Snippets Finder # Finder for retrieving snippets that a user can see, optionally scoped to a
# project or snippets author.
# #
# Used to filter Snippets collections by a set of params # Basic usage:
# #
# Arguments. # user = User.find(1)
# #
# current_user - The current user, nil also can be used. # SnippetsFinder.new(user).execute
# params:
# visibility (integer) - Individual snippet visibility: Public(20), internal(10) or private(0).
# project (Project) - Project related.
# author (User) - Author related.
# #
# params are optional # To limit the snippets to a specific project, supply the `project:` option:
#
# user = User.find(1)
# project = Project.find(1)
#
# SnippetsFinder.new(user, project: project).execute
#
# Limiting snippets to an author can be done by supplying the `author:` option:
#
# user = User.find(1)
# project = Project.find(1)
#
# SnippetsFinder.new(user, author: user).execute
#
# To filter snippets using a specific visibility level, you can provide the
# `scope:` option:
#
# user = User.find(1)
# project = Project.find(1)
#
# SnippetsFinder.new(user, author: user, scope: :are_public).execute
#
# Valid `scope:` values are:
#
# * `:are_private`
# * `:are_internal`
# * `:are_public`
#
# Any other value will be ignored.
class SnippetsFinder < UnionFinder class SnippetsFinder < UnionFinder
include Gitlab::Allowable
include FinderMethods include FinderMethods
attr_accessor :current_user, :project, :params attr_accessor :current_user, :project, :author, :scope
def initialize(current_user, params = {}) def initialize(current_user = nil, params = {})
@current_user = current_user @current_user = current_user
@params = params
@project = params[:project] @project = params[:project]
end @author = params[:author]
@scope = params[:scope].to_s
def execute
items = init_collection
items = by_author(items)
items = by_visibility(items)
items.fresh if project && author
end raise(
ArgumentError,
private 'Filtering by both an author and a project is not supported, ' \
'as this finder is not optimised for this use case'
def init_collection )
if project.present?
authorized_snippets_from_project
else
authorized_snippets
end end
end end
def authorized_snippets_from_project def execute
if can?(current_user, :read_project_snippet, project) base =
if project.team.member?(current_user) if project
project.snippets snippets_for_a_single_project
else
project.snippets.public_to_user(current_user)
end
else else
Snippet.none snippets_for_multiple_projects
end
end end
# rubocop: disable CodeReuse/ActiveRecord base.with_optional_visibility(visibility_from_scope).fresh
def authorized_snippets
# This query was intentionally converted to a raw one to get it work in Rails 5.0.
# In Rails 5.0 and 5.1 there's a bug: https://github.com/rails/arel/issues/531
# Please convert it back when on rails 5.2 as it works again as expected since 5.2.
Snippet.where("#{feature_available_projects} OR #{not_project_related}")
.public_or_visible_to_user(current_user)
end end
# rubocop: enable CodeReuse/ActiveRecord
# Returns a collection of projects that is either public or visible to the # Produces a query that retrieves snippets from multiple projects.
# logged in user.
# #
# A caller must pass in a block to modify individual parts of # The resulting query will, depending on the user's permissions, include the
# the query, e.g. to apply .with_feature_available_for_user on top of it. # following collections of snippets:
# This is useful for performance as we can stick those additional filters #
# at the bottom of e.g. the UNION. # 1. Snippets that don't belong to any project.
# rubocop: disable CodeReuse/ActiveRecord # 2. Snippets of projects that are visible to the current user (e.g. snippets
def projects_for_user # in public projects).
return yield(Project.public_to_user) unless current_user # 3. Snippets of projects that the current user is a member of.
#
# If the current_user is allowed to see all projects, # Each collection is constructed in isolation, allowing for greater control
# we can shortcut and just return. # over the resulting SQL query.
return yield(Project.all) if current_user.full_private_access? def snippets_for_multiple_projects
queries = [global_snippets]
authorized_projects = yield(Project.where('EXISTS (?)', current_user.authorizations_for_projects))
levels = Gitlab::VisibilityLevel.levels_for_user(current_user)
visible_projects = yield(Project.where(visibility_level: levels))
# We use a UNION here instead of OR clauses since this results in better if Ability.allowed?(current_user, :read_cross_project)
# performance. queries << snippets_of_visible_projects
Project.from_union([authorized_projects, visible_projects]) queries << snippets_of_authorized_projects if current_user
end end
# rubocop: enable CodeReuse/ActiveRecord
def feature_available_projects find_union(queries, Snippet)
# Don't return any project related snippets if the user cannot read cross project end
return table[:id].eq(nil).to_sql unless Ability.allowed?(current_user, :read_cross_project)
projects = projects_for_user do |part|
part.with_feature_available_for_user(:snippets, current_user)
end.select(:id)
# This query was intentionally converted to a raw one to get it work in Rails 5.0. def snippets_for_a_single_project
# In Rails 5.0 and 5.1 there's a bug: https://github.com/rails/arel/issues/531 Snippet.for_project_with_user(project, current_user)
# Please convert it back when on rails 5.2 as it works again as expected since 5.2.
"snippets.project_id IN (#{projects.to_sql})"
end end
def not_project_related def global_snippets
table[:project_id].eq(nil).to_sql snippets_for_author_or_visible_to_user.only_global_snippets
end end
def table # Returns the snippets that the current user (logged in or not) can view.
Snippet.arel_table def snippets_of_visible_projects
snippets_for_author_or_visible_to_user
.only_include_projects_visible_to(current_user)
.only_include_projects_with_snippets_enabled
end end
# rubocop: disable CodeReuse/ActiveRecord # Returns the snippets that the currently logged in user has access to by
def by_visibility(items) # being a member of the project the snippets belong to.
visibility = params[:visibility] || visibility_from_scope #
# This method requires that `current_user` returns a `User` instead of `nil`,
# and is optimised for this specific scenario.
def snippets_of_authorized_projects
base = author ? snippets_for_author : Snippet.all
return items unless visibility base
.only_include_projects_with_snippets_enabled(include_private: true)
.only_include_authorized_projects(current_user)
end
items.where(visibility_level: visibility) def snippets_for_author_or_visible_to_user
if author
snippets_for_author
elsif current_user
Snippet.visible_to_or_authored_by(current_user)
else
Snippet.public_to_user
end
end end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord def snippets_for_author
def by_author(items) base = author.snippets
return items unless params[:author]
items.where(author_id: params[:author].id) if author == current_user
# If the current user is also the author of all snippets, then we can
# include private snippets.
base
else
base.public_to_user(current_user)
end
end end
# rubocop: enable CodeReuse/ActiveRecord
def visibility_from_scope def visibility_from_scope
case params[:scope].to_s case scope
when 'are_private' when 'are_private'
Snippet::PRIVATE Snippet::PRIVATE
when 'are_internal' when 'are_internal'
......
...@@ -218,7 +218,8 @@ module ApplicationSettingsHelper ...@@ -218,7 +218,8 @@ module ApplicationSettingsHelper
:user_oauth_applications, :user_oauth_applications,
:version_check_enabled, :version_check_enabled,
:web_ide_clientside_preview_enabled, :web_ide_clientside_preview_enabled,
:diff_max_patch_bytes :diff_max_patch_bytes,
:commit_email_hostname
] ]
end end
......
...@@ -22,7 +22,7 @@ module AvatarsHelper ...@@ -22,7 +22,7 @@ module AvatarsHelper
end end
def avatar_icon_for_email(email = nil, size = nil, scale = 2, only_path: true) def avatar_icon_for_email(email = nil, size = nil, scale = 2, only_path: true)
user = User.find_by_any_email(email.try(:downcase)) user = User.find_by_any_email(email)
if user if user
avatar_icon_for_user(user, size, scale, only_path: only_path) avatar_icon_for_user(user, size, scale, only_path: only_path)
else else
......
# frozen_string_literal: true # frozen_string_literal: true
module ProfilesHelper module ProfilesHelper
def commit_email_select_options(user)
private_email = user.private_commit_email
verified_emails = user.verified_emails - [private_email]
[
[s_("Profiles|Use a private email - %{email}").html_safe % { email: private_email }, Gitlab::PrivateCommitEmail::TOKEN],
verified_emails
]
end
def selected_commit_email(user)
user.read_attribute(:commit_email) || user.commit_email
end
def attribute_provider_label(attribute) def attribute_provider_label(attribute)
user_synced_attributes_metadata = current_user.user_synced_attributes_metadata user_synced_attributes_metadata = current_user.user_synced_attributes_metadata
if user_synced_attributes_metadata&.synced?(attribute) if user_synced_attributes_metadata&.synced?(attribute)
......
...@@ -189,6 +189,8 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -189,6 +189,8 @@ class ApplicationSetting < ActiveRecord::Base
validates :user_default_internal_regex, js_regex: true, allow_nil: true validates :user_default_internal_regex, js_regex: true, allow_nil: true
validates :commit_email_hostname, format: { with: /\A[^@]+\z/ }
validates :archive_builds_in_seconds, validates :archive_builds_in_seconds,
allow_nil: true, allow_nil: true,
numericality: { only_integer: true, greater_than_or_equal_to: 1.day.seconds } numericality: { only_integer: true, greater_than_or_equal_to: 1.day.seconds }
...@@ -301,10 +303,15 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -301,10 +303,15 @@ class ApplicationSetting < ActiveRecord::Base
user_default_internal_regex: nil, user_default_internal_regex: nil,
user_show_add_ssh_key_message: true, user_show_add_ssh_key_message: true,
usage_stats_set_by_user_id: nil, usage_stats_set_by_user_id: nil,
diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES,
commit_email_hostname: default_commit_email_hostname
} }
end end
def self.default_commit_email_hostname
"users.noreply.#{Gitlab.config.gitlab.host}"
end
def self.create_from_defaults def self.create_from_defaults
create(defaults) create(defaults)
end end
...@@ -360,6 +367,10 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -360,6 +367,10 @@ class ApplicationSetting < ActiveRecord::Base
Array(read_attribute(:repository_storages)) Array(read_attribute(:repository_storages))
end end
def commit_email_hostname
super.presence || self.class.default_commit_email_hostname
end
def default_project_visibility=(level) def default_project_visibility=(level)
super(Gitlab::VisibilityLevel.level_value(level)) super(Gitlab::VisibilityLevel.level_value(level))
end end
......
...@@ -818,7 +818,7 @@ module Ci ...@@ -818,7 +818,7 @@ module Ci
end end
end end
def predefined_variables def predefined_variables # rubocop:disable Metrics/AbcSize
Gitlab::Ci::Variables::Collection.new.tap do |variables| Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI', value: 'true') variables.append(key: 'CI', value: 'true')
variables.append(key: 'GITLAB_CI', value: 'true') variables.append(key: 'GITLAB_CI', value: 'true')
...@@ -838,6 +838,8 @@ module Ci ...@@ -838,6 +838,8 @@ module Ci
variables.append(key: "CI_COMMIT_TAG", value: ref) if tag? variables.append(key: "CI_COMMIT_TAG", value: ref) if tag?
variables.append(key: "CI_PIPELINE_TRIGGERED", value: 'true') if trigger_request variables.append(key: "CI_PIPELINE_TRIGGERED", value: 'true') if trigger_request
variables.append(key: "CI_JOB_MANUAL", value: 'true') if action? variables.append(key: "CI_JOB_MANUAL", value: 'true') if action?
variables.append(key: "CI_NODE_INDEX", value: self.options[:instance].to_s) if self.options&.include?(:instance)
variables.append(key: "CI_NODE_TOTAL", value: (self.options&.dig(:parallel) || 1).to_s)
variables.concat(legacy_variables) variables.concat(legacy_variables)
end end
end end
......
...@@ -260,7 +260,7 @@ class Commit ...@@ -260,7 +260,7 @@ class Commit
request_cache(:author) { author_email.downcase } request_cache(:author) { author_email.downcase }
def committer def committer
@committer ||= User.find_by_any_email(committer_email.downcase) @committer ||= User.find_by_any_email(committer_email)
end end
def parents def parents
......
# frozen_string_literal: true # frozen_string_literal: true
require 'set'
class Compare class Compare
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
...@@ -77,4 +79,13 @@ class Compare ...@@ -77,4 +79,13 @@ class Compare
head_sha: head_commit_sha head_sha: head_commit_sha
) )
end end
def modified_paths
paths = Set.new
diffs.diff_files.each do |diff|
paths.add diff.old_path
paths.add diff.new_path
end
paths.to_a
end
end end
...@@ -411,6 +411,18 @@ class MergeRequest < ActiveRecord::Base ...@@ -411,6 +411,18 @@ class MergeRequest < ActiveRecord::Base
merge_request_diff&.real_size || diffs.real_size merge_request_diff&.real_size || diffs.real_size
end end
def modified_paths(past_merge_request_diff: nil)
diffs = if past_merge_request_diff
past_merge_request_diff
elsif compare
compare
else
self.merge_request_diff
end
diffs.modified_paths
end
def diff_base_commit def diff_base_commit
if persisted? if persisted?
merge_request_diff.base_commit merge_request_diff.base_commit
......
...@@ -6,6 +6,7 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -6,6 +6,7 @@ class MergeRequestDiff < ActiveRecord::Base
include ManualInverseAssociation include ManualInverseAssociation
include IgnorableColumn include IgnorableColumn
include EachBatch include EachBatch
include Gitlab::Utils::StrongMemoize
# Don't display more than 100 commits at once # Don't display more than 100 commits at once
COMMITS_SAFE_SIZE = 100 COMMITS_SAFE_SIZE = 100
...@@ -234,6 +235,12 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -234,6 +235,12 @@ class MergeRequestDiff < ActiveRecord::Base
end end
# rubocop: enable CodeReuse/ServiceClass # rubocop: enable CodeReuse/ServiceClass
def modified_paths
strong_memoize(:modified_paths) do
merge_request_diff_files.pluck(:new_path, :old_path).flatten.uniq
end
end
private private
def create_merge_request_diff_files(diffs) def create_merge_request_diff_files(diffs)
......
...@@ -2072,6 +2072,10 @@ class Project < ActiveRecord::Base ...@@ -2072,6 +2072,10 @@ class Project < ActiveRecord::Base
storage_version != LATEST_STORAGE_VERSION storage_version != LATEST_STORAGE_VERSION
end end
def snippets_visible?(user = nil)
Ability.allowed?(user, :read_project_snippet, self)
end
private private
def use_hashed_storage def use_hashed_storage
......
...@@ -64,6 +64,62 @@ class Snippet < ActiveRecord::Base ...@@ -64,6 +64,62 @@ class Snippet < ActiveRecord::Base
attr_spammable :title, spam_title: true attr_spammable :title, spam_title: true
attr_spammable :content, spam_description: true attr_spammable :content, spam_description: true
def self.with_optional_visibility(value = nil)
if value
where(visibility_level: value)
else
all
end
end
def self.only_global_snippets
where(project_id: nil)
end
def self.only_include_projects_visible_to(current_user = nil)
levels = Gitlab::VisibilityLevel.levels_for_user(current_user)
joins(:project).where('projects.visibility_level IN (?)', levels)
end
def self.only_include_projects_with_snippets_enabled(include_private: false)
column = ProjectFeature.access_level_attribute(:snippets)
levels = [ProjectFeature::ENABLED, ProjectFeature::PUBLIC]
levels << ProjectFeature::PRIVATE if include_private
joins(project: :project_feature)
.where(project_features: { column => levels })
end
def self.only_include_authorized_projects(current_user)
where(
'EXISTS (?)',
ProjectAuthorization
.select(1)
.where('project_id = snippets.project_id')
.where(user_id: current_user.id)
)
end
def self.for_project_with_user(project, user = nil)
return none unless project.snippets_visible?(user)
if user && project.team.member?(user)
project.snippets
else
project.snippets.public_to_user(user)
end
end
def self.visible_to_or_authored_by(user)
where(
'snippets.visibility_level IN (?) OR snippets.author_id = ?',
Gitlab::VisibilityLevel.levels_for_user(user),
user.id
)
end
def self.reference_prefix def self.reference_prefix
'$' '$'
end end
...@@ -82,27 +138,6 @@ class Snippet < ActiveRecord::Base ...@@ -82,27 +138,6 @@ class Snippet < ActiveRecord::Base
@link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/) @link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/)
end end
# Returns a collection of snippets that are either public or visible to the
# logged in user.
#
# This method does not verify the user actually has the access to the project
# the snippet is in, so it should be only used on a relation that's already scoped
# for project access
def self.public_or_visible_to_user(user = nil)
if user
authorized = user
.project_authorizations
.select(1)
.where('project_authorizations.project_id = snippets.project_id')
levels = Gitlab::VisibilityLevel.levels_for_user(user)
where('EXISTS (?) OR snippets.visibility_level IN (?) or snippets.author_id = (?)', authorized, levels, user.id)
else
public_to_user
end
end
def to_reference(from = nil, full: false) def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{id}" reference = "#{self.class.reference_prefix}#{id}"
......
...@@ -13,7 +13,7 @@ class Upload < ActiveRecord::Base ...@@ -13,7 +13,7 @@ class Upload < ActiveRecord::Base
validates :model, presence: true validates :model, presence: true
validates :uploader, presence: true validates :uploader, presence: true
scope :with_files_stored_locally, -> { where(store: [nil, ObjectStorage::Store::LOCAL]) } scope :with_files_stored_locally, -> { where(store: ObjectStorage::Store::LOCAL) }
scope :with_files_stored_remotely, -> { where(store: ObjectStorage::Store::REMOTE) } scope :with_files_stored_remotely, -> { where(store: ObjectStorage::Store::REMOTE) }
before_save :calculate_checksum!, if: :foreground_checksummable? before_save :calculate_checksum!, if: :foreground_checksummable?
...@@ -71,8 +71,6 @@ class Upload < ActiveRecord::Base ...@@ -71,8 +71,6 @@ class Upload < ActiveRecord::Base
end end
def local? def local?
return true if store.nil?
store == ObjectStorage::Store::LOCAL store == ObjectStorage::Store::LOCAL
end end
......
...@@ -347,7 +347,11 @@ class User < ActiveRecord::Base ...@@ -347,7 +347,11 @@ class User < ActiveRecord::Base
# Find a User by their primary email or any associated secondary email # Find a User by their primary email or any associated secondary email
def find_by_any_email(email, confirmed: false) def find_by_any_email(email, confirmed: false)
by_any_email(email, confirmed: confirmed).take return unless email
downcased = email.downcase
find_by_private_commit_email(downcased) || by_any_email(downcased, confirmed: confirmed).take
end end
# Returns a relation containing all the users for the given Email address # Returns a relation containing all the users for the given Email address
...@@ -361,6 +365,12 @@ class User < ActiveRecord::Base ...@@ -361,6 +365,12 @@ class User < ActiveRecord::Base
from_union([users, emails]) from_union([users, emails])
end end
def find_by_private_commit_email(email)
user_id = Gitlab::PrivateCommitEmail.user_id_for_email(email)
find_by(id: user_id)
end
def filter(filter_name) def filter(filter_name)
case filter_name case filter_name
when 'admins' when 'admins'
...@@ -633,6 +643,10 @@ class User < ActiveRecord::Base ...@@ -633,6 +643,10 @@ class User < ActiveRecord::Base
def commit_email def commit_email
return self.email unless has_attribute?(:commit_email) return self.email unless has_attribute?(:commit_email)
if super == Gitlab::PrivateCommitEmail::TOKEN
return private_commit_email
end
# The commit email is the same as the primary email if undefined # The commit email is the same as the primary email if undefined
super.presence || self.email super.presence || self.email
end end
...@@ -645,6 +659,10 @@ class User < ActiveRecord::Base ...@@ -645,6 +659,10 @@ class User < ActiveRecord::Base
has_attribute?(:commit_email) && super has_attribute?(:commit_email) && super
end end
def private_commit_email
Gitlab::PrivateCommitEmail.for_user(self)
end
# see if the new email is already a verified secondary email # see if the new email is already a verified secondary email
def check_for_verified_email def check_for_verified_email
skip_reconfirmation! if emails.confirmed.where(email: self.email).any? skip_reconfirmation! if emails.confirmed.where(email: self.email).any?
...@@ -1020,13 +1038,21 @@ class User < ActiveRecord::Base ...@@ -1020,13 +1038,21 @@ class User < ActiveRecord::Base
def verified_emails def verified_emails
verified_emails = [] verified_emails = []
verified_emails << email if primary_email_verified? verified_emails << email if primary_email_verified?
verified_emails << private_commit_email
verified_emails.concat(emails.confirmed.pluck(:email)) verified_emails.concat(emails.confirmed.pluck(:email))
verified_emails verified_emails
end end
def verified_email?(check_email) def verified_email?(check_email)
downcased = check_email.downcase downcased = check_email.downcase
email == downcased ? primary_email_verified? : emails.confirmed.where(email: downcased).exists?
if email == downcased
primary_email_verified?
else
user_id = Gitlab::PrivateCommitEmail.user_id_for_email(downcased)
user_id == id || emails.confirmed.where(email: downcased).exists?
end
end end
def hook_attrs def hook_attrs
......
...@@ -2,18 +2,18 @@ ...@@ -2,18 +2,18 @@
module MergeRequests module MergeRequests
class RefreshService < MergeRequests::BaseService class RefreshService < MergeRequests::BaseService
attr_reader :push
def execute(oldrev, newrev, ref) def execute(oldrev, newrev, ref)
push = Gitlab::Git::Push.new(@project, oldrev, newrev, ref) @push = Gitlab::Git::Push.new(@project, oldrev, newrev, ref)
return true unless push.branch_push? return true unless @push.branch_push?
refresh_merge_requests!(push) refresh_merge_requests!
end end
private private
def refresh_merge_requests!(push) def refresh_merge_requests!
@push = push
Gitlab::GitalyClient.allow_n_plus_1_calls(&method(:find_new_commits)) Gitlab::GitalyClient.allow_n_plus_1_calls(&method(:find_new_commits))
# Be sure to close outstanding MRs before reloading them to avoid generating an # Be sure to close outstanding MRs before reloading them to avoid generating an
# empty diff during a manual merge # empty diff during a manual merge
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
module Projects module Projects
class AutocompleteService < BaseService class AutocompleteService < BaseService
prepend EE::Projects::AutocompleteService
include LabelsAsHash include LabelsAsHash
def issues def issues
IssuesFinder.new(current_user, project_id: project.id, state: 'opened').execute.select([:iid, :title]) IssuesFinder.new(current_user, project_id: project.id, state: 'opened').execute.select([:iid, :title])
......
...@@ -20,6 +20,12 @@ ...@@ -20,6 +20,12 @@
By default GitLab sends emails in HTML and plain text formats so mail By default GitLab sends emails in HTML and plain text formats so mail
clients can choose what format to use. Disable this option if you only clients can choose what format to use. Disable this option if you only
want to send emails in plain text format. want to send emails in plain text format.
.form-group
= f.label :commit_email_hostname, _('Custom hostname (for private commit emails)'), class: 'label-bold'
= f.text_field :commit_email_hostname, class: 'form-control'
.form-text.text-muted
- commit_email_hostname_docs_link = link_to _('Learn more'), help_page_path('user/admin_area/settings/email', anchor: 'custom-private-commit-email-hostname'), target: '_blank'
= _("This setting will update the hostname that is used to generate private commit emails. %{learn_more}").html_safe % { learn_more: commit_email_hostname_docs_link }
-# EE-specific start -# EE-specific start
- if License.feature_available?(:email_additional_text) - if License.feature_available?(:email_additional_text)
.form-group .form-group
......
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
%p %p
= _('Who will be able to see this group?') = _('Who will be able to see this group?')
= link_to _('View the documentation'), help_page_path("public_access/public_access"), target: '_blank' = link_to _('View the documentation'), help_page_path("public_access/public_access"), target: '_blank'
= render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group, with_label: false = render 'shared/visibility_level', f: f, visibility_level: default_group_visibility, can_change_visibility_level: true, form_model: @group, with_label: false
= render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled = render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment