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

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

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