Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
8f2b51af41
commit
a071c2888d
|
@ -885,10 +885,10 @@ GEM
|
|||
bundler (>= 1.3.0)
|
||||
railties (= 6.0.3.1)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-controller-testing (1.0.4)
|
||||
actionpack (>= 5.0.1.x)
|
||||
actionview (>= 5.0.1.x)
|
||||
activesupport (>= 5.0.1.x)
|
||||
rails-controller-testing (1.0.5)
|
||||
actionpack (>= 5.0.1.rc1)
|
||||
actionview (>= 5.0.1.rc1)
|
||||
activesupport (>= 5.0.1.rc1)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
|
||||
import { __ } from '~/locale';
|
||||
import {
|
||||
MATCH_LINE_TYPE,
|
||||
CONTEXT_LINE_TYPE,
|
||||
LINE_HOVER_CLASS_NAME,
|
||||
OLD_NO_NEW_LINE_TYPE,
|
||||
NEW_NO_NEW_LINE_TYPE,
|
||||
EMPTY_CELL_TYPE,
|
||||
} from '../constants';
|
||||
|
||||
export const isHighlighted = (state, line, isCommented) => {
|
||||
if (isCommented) return true;
|
||||
|
||||
const lineCode = line?.line_code;
|
||||
return lineCode ? lineCode === state.diffs.highlightedRow : false;
|
||||
};
|
||||
|
||||
export const isContextLine = type => type === CONTEXT_LINE_TYPE;
|
||||
|
||||
export const isMatchLine = type => type === MATCH_LINE_TYPE;
|
||||
|
||||
export const isMetaLine = type =>
|
||||
[OLD_NO_NEW_LINE_TYPE, NEW_NO_NEW_LINE_TYPE, EMPTY_CELL_TYPE].includes(type);
|
||||
|
||||
export const shouldRenderCommentButton = (
|
||||
isLoggedIn,
|
||||
isCommentButtonRendered,
|
||||
featureMergeRefHeadComments = false,
|
||||
) => {
|
||||
if (!isCommentButtonRendered) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isLoggedIn) {
|
||||
const isDiffHead = parseBoolean(getParameterByName('diff_head'));
|
||||
return !isDiffHead || featureMergeRefHeadComments;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const hasDiscussions = line => line?.discussions?.length > 0;
|
||||
|
||||
export const lineHref = line => `#${line?.line_code || ''}`;
|
||||
|
||||
export const lineCode = line => {
|
||||
if (!line) return undefined;
|
||||
return line.line_code || line.left?.line_code || line.right?.line_code;
|
||||
};
|
||||
|
||||
export const classNameMapCell = (line, hll, isLoggedIn, isHover) => {
|
||||
if (!line) return [];
|
||||
const { type } = line;
|
||||
|
||||
return [
|
||||
type,
|
||||
{
|
||||
hll,
|
||||
[LINE_HOVER_CLASS_NAME]: isLoggedIn && isHover && !isContextLine(type) && !isMetaLine(type),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const addCommentTooltip = line => {
|
||||
let tooltip;
|
||||
if (!line) return tooltip;
|
||||
|
||||
tooltip = __('Add a comment to this line');
|
||||
const brokenSymlinks = line.commentsDisabled;
|
||||
|
||||
if (brokenSymlinks) {
|
||||
if (brokenSymlinks.wasSymbolic || brokenSymlinks.isSymbolic) {
|
||||
tooltip = __(
|
||||
'Commenting on symbolic links that replace or are replaced by files is currently not supported.',
|
||||
);
|
||||
} else if (brokenSymlinks.wasReal || brokenSymlinks.isReal) {
|
||||
tooltip = __(
|
||||
'Commenting on files that replace or are replaced by symbolic links is currently not supported.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return tooltip;
|
||||
};
|
||||
|
||||
export const parallelViewLeftLineType = (line, hll) => {
|
||||
if (line?.right?.type === NEW_NO_NEW_LINE_TYPE) {
|
||||
return OLD_NO_NEW_LINE_TYPE;
|
||||
}
|
||||
|
||||
const lineTypeClass = line?.left ? line.left.type : EMPTY_CELL_TYPE;
|
||||
|
||||
return [lineTypeClass, { hll }];
|
||||
};
|
||||
|
||||
export const shouldShowCommentButton = (hover, context, meta, discussions) => {
|
||||
return hover && !context && !meta && !discussions;
|
||||
};
|
|
@ -1,206 +0,0 @@
|
|||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
|
||||
import DiffGutterAvatars from './diff_gutter_avatars.vue';
|
||||
import { __ } from '~/locale';
|
||||
import {
|
||||
CONTEXT_LINE_TYPE,
|
||||
LINE_POSITION_RIGHT,
|
||||
EMPTY_CELL_TYPE,
|
||||
OLD_NO_NEW_LINE_TYPE,
|
||||
OLD_LINE_TYPE,
|
||||
NEW_NO_NEW_LINE_TYPE,
|
||||
LINE_HOVER_CLASS_NAME,
|
||||
} from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DiffGutterAvatars,
|
||||
GlIcon,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
line: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
fileHash: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isHighlighted: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
showCommentButton: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
linePosition: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
lineType: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
isBottom: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isHover: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isCommentButtonRendered: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['isLoggedIn']),
|
||||
lineCode() {
|
||||
return (
|
||||
this.line.line_code ||
|
||||
(this.line.left && this.line.left.line_code) ||
|
||||
(this.line.right && this.line.right.line_code)
|
||||
);
|
||||
},
|
||||
lineHref() {
|
||||
return `#${this.line.line_code || ''}`;
|
||||
},
|
||||
shouldShowCommentButton() {
|
||||
return this.isHover && !this.isContextLine && !this.isMetaLine && !this.hasDiscussions;
|
||||
},
|
||||
hasDiscussions() {
|
||||
return this.line.discussions && this.line.discussions.length > 0;
|
||||
},
|
||||
shouldShowAvatarsOnGutter() {
|
||||
if (!this.line.type && this.linePosition === LINE_POSITION_RIGHT) {
|
||||
return false;
|
||||
}
|
||||
return this.showCommentButton && this.hasDiscussions;
|
||||
},
|
||||
shouldRenderCommentButton() {
|
||||
if (!this.isCommentButtonRendered) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.isLoggedIn && this.showCommentButton) {
|
||||
const isDiffHead = parseBoolean(getParameterByName('diff_head'));
|
||||
return !isDiffHead || gon.features?.mergeRefHeadComments;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
isContextLine() {
|
||||
return this.line.type === CONTEXT_LINE_TYPE;
|
||||
},
|
||||
isMetaLine() {
|
||||
const { type } = this.line;
|
||||
|
||||
return (
|
||||
type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE
|
||||
);
|
||||
},
|
||||
classNameMap() {
|
||||
const { type } = this.line;
|
||||
|
||||
return [
|
||||
type,
|
||||
{
|
||||
hll: this.isHighlighted,
|
||||
[LINE_HOVER_CLASS_NAME]:
|
||||
this.isLoggedIn && this.isHover && !this.isContextLine && !this.isMetaLine,
|
||||
},
|
||||
];
|
||||
},
|
||||
lineNumber() {
|
||||
return this.lineType === OLD_LINE_TYPE ? this.line.old_line : this.line.new_line;
|
||||
},
|
||||
addCommentTooltip() {
|
||||
const brokenSymlinks = this.line.commentsDisabled;
|
||||
let tooltip = __('Add a comment to this line');
|
||||
|
||||
if (brokenSymlinks) {
|
||||
if (brokenSymlinks.wasSymbolic || brokenSymlinks.isSymbolic) {
|
||||
tooltip = __(
|
||||
'Commenting on symbolic links that replace or are replaced by files is currently not supported.',
|
||||
);
|
||||
} else if (brokenSymlinks.wasReal || brokenSymlinks.isReal) {
|
||||
tooltip = __(
|
||||
'Commenting on files that replace or are replaced by symbolic links is currently not supported.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return tooltip;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.unwatchShouldShowCommentButton = this.$watch('shouldShowCommentButton', newVal => {
|
||||
if (newVal) {
|
||||
this.isCommentButtonRendered = true;
|
||||
this.unwatchShouldShowCommentButton();
|
||||
}
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.unwatchShouldShowCommentButton();
|
||||
},
|
||||
methods: {
|
||||
...mapActions('diffs', ['showCommentForm', 'setHighlightedRow', 'toggleLineDiscussions']),
|
||||
handleCommentButton() {
|
||||
this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.fileHash });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<td ref="td" :class="classNameMap">
|
||||
<span
|
||||
ref="addNoteTooltip"
|
||||
v-gl-tooltip
|
||||
class="add-diff-note tooltip-wrapper"
|
||||
:title="addCommentTooltip"
|
||||
>
|
||||
<button
|
||||
v-if="shouldRenderCommentButton"
|
||||
v-show="shouldShowCommentButton"
|
||||
ref="addDiffNoteButton"
|
||||
type="button"
|
||||
class="add-diff-note note-button js-add-diff-note-button qa-diff-comment"
|
||||
:disabled="line.commentsDisabled"
|
||||
@click="handleCommentButton"
|
||||
>
|
||||
<gl-icon :size="12" name="comment" />
|
||||
</button>
|
||||
</span>
|
||||
<a
|
||||
v-if="lineNumber"
|
||||
ref="lineNumberRef"
|
||||
:data-linenumber="lineNumber"
|
||||
:href="lineHref"
|
||||
@click="setHighlightedRow(lineCode)"
|
||||
>
|
||||
</a>
|
||||
<diff-gutter-avatars
|
||||
v-if="shouldShowAvatarsOnGutter"
|
||||
:discussions="line.discussions"
|
||||
:discussions-expanded="line.discussionsExpanded"
|
||||
@toggleLineDiscussions="
|
||||
toggleLineDiscussions({ lineCode, fileHash, expanded: !line.discussionsExpanded })
|
||||
"
|
||||
/>
|
||||
</td>
|
||||
</template>
|
|
@ -1,22 +1,9 @@
|
|||
<script>
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
|
||||
import {
|
||||
MATCH_LINE_TYPE,
|
||||
NEW_LINE_TYPE,
|
||||
OLD_LINE_TYPE,
|
||||
CONTEXT_LINE_TYPE,
|
||||
CONTEXT_LINE_CLASS_NAME,
|
||||
LINE_POSITION_LEFT,
|
||||
LINE_POSITION_RIGHT,
|
||||
LINE_HOVER_CLASS_NAME,
|
||||
OLD_NO_NEW_LINE_TYPE,
|
||||
NEW_NO_NEW_LINE_TYPE,
|
||||
EMPTY_CELL_TYPE,
|
||||
} from '../constants';
|
||||
import { __ } from '~/locale';
|
||||
import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
|
||||
import { CONTEXT_LINE_CLASS_NAME } from '../constants';
|
||||
import DiffGutterAvatars from './diff_gutter_avatars.vue';
|
||||
import * as utils from './diff_row_utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -61,14 +48,11 @@ export default {
|
|||
...mapGetters('diffs', ['fileLineCoverage']),
|
||||
...mapState({
|
||||
isHighlighted(state) {
|
||||
if (this.isCommented) return true;
|
||||
|
||||
const lineCode = this.line.line_code;
|
||||
return lineCode ? lineCode === state.diffs.highlightedRow : false;
|
||||
return utils.isHighlighted(state, this.line, this.isCommented);
|
||||
},
|
||||
}),
|
||||
isContextLine() {
|
||||
return this.line.type === CONTEXT_LINE_TYPE;
|
||||
return utils.isContextLine(this.line.type);
|
||||
},
|
||||
classNameMap() {
|
||||
return [
|
||||
|
@ -82,82 +66,48 @@ export default {
|
|||
return this.line.line_code || `${this.fileHash}_${this.line.old_line}_${this.line.new_line}`;
|
||||
},
|
||||
isMatchLine() {
|
||||
return this.line.type === MATCH_LINE_TYPE;
|
||||
return utils.isMatchLine(this.line.type);
|
||||
},
|
||||
coverageState() {
|
||||
return this.fileLineCoverage(this.filePath, this.line.new_line);
|
||||
},
|
||||
isMetaLine() {
|
||||
const { type } = this.line;
|
||||
|
||||
return (
|
||||
type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE
|
||||
);
|
||||
return utils.isMetaLine(this.line.type);
|
||||
},
|
||||
classNameMapCell() {
|
||||
const { type } = this.line;
|
||||
|
||||
return [
|
||||
type,
|
||||
{
|
||||
hll: this.isHighlighted,
|
||||
[LINE_HOVER_CLASS_NAME]:
|
||||
this.isLoggedIn && this.isHover && !this.isContextLine && !this.isMetaLine,
|
||||
},
|
||||
];
|
||||
return utils.classNameMapCell(this.line, this.isHighlighted, this.isLoggedIn, this.isHover);
|
||||
},
|
||||
addCommentTooltip() {
|
||||
const brokenSymlinks = this.line.commentsDisabled;
|
||||
let tooltip = __('Add a comment to this line');
|
||||
|
||||
if (brokenSymlinks) {
|
||||
if (brokenSymlinks.wasSymbolic || brokenSymlinks.isSymbolic) {
|
||||
tooltip = __(
|
||||
'Commenting on symbolic links that replace or are replaced by files is currently not supported.',
|
||||
);
|
||||
} else if (brokenSymlinks.wasReal || brokenSymlinks.isReal) {
|
||||
tooltip = __(
|
||||
'Commenting on files that replace or are replaced by symbolic links is currently not supported.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return tooltip;
|
||||
return utils.addCommentTooltip(this.line);
|
||||
},
|
||||
shouldRenderCommentButton() {
|
||||
if (this.isLoggedIn) {
|
||||
const isDiffHead = parseBoolean(getParameterByName('diff_head'));
|
||||
return !isDiffHead || gon.features?.mergeRefHeadComments;
|
||||
}
|
||||
|
||||
return false;
|
||||
return utils.shouldRenderCommentButton(
|
||||
this.isLoggedIn,
|
||||
true,
|
||||
gon.features?.mergeRefHeadComments,
|
||||
);
|
||||
},
|
||||
shouldShowCommentButton() {
|
||||
return this.isHover && !this.isContextLine && !this.isMetaLine && !this.hasDiscussions;
|
||||
return utils.shouldShowCommentButton(
|
||||
this.isHover,
|
||||
this.isContextLine,
|
||||
this.isMetaLine,
|
||||
this.hasDiscussions,
|
||||
);
|
||||
},
|
||||
hasDiscussions() {
|
||||
return this.line.discussions && this.line.discussions.length > 0;
|
||||
return utils.hasDiscussions(this.line);
|
||||
},
|
||||
lineHref() {
|
||||
return `#${this.line.line_code || ''}`;
|
||||
return utils.lineHref(this.line);
|
||||
},
|
||||
lineCode() {
|
||||
return (
|
||||
this.line.line_code ||
|
||||
(this.line.left && this.line.left.line_code) ||
|
||||
(this.line.right && this.line.right.line_code)
|
||||
);
|
||||
return utils.lineCode(this.line);
|
||||
},
|
||||
shouldShowAvatarsOnGutter() {
|
||||
return this.hasDiscussions;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.newLineType = NEW_LINE_TYPE;
|
||||
this.oldLineType = OLD_LINE_TYPE;
|
||||
this.linePositionLeft = LINE_POSITION_LEFT;
|
||||
this.linePositionRight = LINE_POSITION_RIGHT;
|
||||
},
|
||||
mounted() {
|
||||
this.scrollToLineIfNeededInline(this.line);
|
||||
},
|
||||
|
@ -242,6 +192,7 @@ export default {
|
|||
class="line-coverage"
|
||||
></td>
|
||||
<td
|
||||
:key="line.line_code"
|
||||
v-safe-html="line.rich_text"
|
||||
:class="[
|
||||
line.type,
|
||||
|
|
|
@ -2,21 +2,9 @@
|
|||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import $ from 'jquery';
|
||||
import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
|
||||
import {
|
||||
MATCH_LINE_TYPE,
|
||||
NEW_LINE_TYPE,
|
||||
OLD_LINE_TYPE,
|
||||
CONTEXT_LINE_TYPE,
|
||||
CONTEXT_LINE_CLASS_NAME,
|
||||
OLD_NO_NEW_LINE_TYPE,
|
||||
PARALLEL_DIFF_VIEW_TYPE,
|
||||
NEW_NO_NEW_LINE_TYPE,
|
||||
EMPTY_CELL_TYPE,
|
||||
LINE_HOVER_CLASS_NAME,
|
||||
} from '../constants';
|
||||
import { __ } from '~/locale';
|
||||
import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
|
||||
import { CONTEXT_LINE_CLASS_NAME, PARALLEL_DIFF_VIEW_TYPE } from '../constants';
|
||||
import DiffGutterAvatars from './diff_gutter_avatars.vue';
|
||||
import * as utils from './diff_row_utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -63,20 +51,15 @@ export default {
|
|||
...mapGetters(['isLoggedIn']),
|
||||
...mapState({
|
||||
isHighlighted(state) {
|
||||
if (this.isCommented) return true;
|
||||
|
||||
const lineCode =
|
||||
(this.line.left && this.line.left.line_code) ||
|
||||
(this.line.right && this.line.right.line_code);
|
||||
|
||||
return lineCode ? lineCode === state.diffs.highlightedRow : false;
|
||||
const line = this.line.left?.line_code ? this.line.left : this.line.right;
|
||||
return utils.isHighlighted(state, line, this.isCommented);
|
||||
},
|
||||
}),
|
||||
isContextLineLeft() {
|
||||
return this.line.left && this.line.left.type === CONTEXT_LINE_TYPE;
|
||||
return utils.isContextLine(this.line.left?.type);
|
||||
},
|
||||
isContextLineRight() {
|
||||
return this.line.right && this.line.right.type === CONTEXT_LINE_TYPE;
|
||||
return utils.isContextLine(this.line.right?.type);
|
||||
},
|
||||
classNameMap() {
|
||||
return {
|
||||
|
@ -85,157 +68,84 @@ export default {
|
|||
};
|
||||
},
|
||||
parallelViewLeftLineType() {
|
||||
if (this.line.right && this.line.right.type === NEW_NO_NEW_LINE_TYPE) {
|
||||
return OLD_NO_NEW_LINE_TYPE;
|
||||
}
|
||||
|
||||
const lineTypeClass = this.line.left ? this.line.left.type : EMPTY_CELL_TYPE;
|
||||
|
||||
return [
|
||||
lineTypeClass,
|
||||
{
|
||||
hll: this.isHighlighted,
|
||||
},
|
||||
];
|
||||
return utils.parallelViewLeftLineType(this.line, this.isHighlighted);
|
||||
},
|
||||
isMatchLineLeft() {
|
||||
return this.line.left && this.line.left.type === MATCH_LINE_TYPE;
|
||||
return utils.isMatchLine(this.line.left?.type);
|
||||
},
|
||||
isMatchLineRight() {
|
||||
return this.line.right && this.line.right.type === MATCH_LINE_TYPE;
|
||||
return utils.isMatchLine(this.line.right?.type);
|
||||
},
|
||||
coverageState() {
|
||||
return this.fileLineCoverage(this.filePath, this.line.right.new_line);
|
||||
},
|
||||
classNameMapCellLeft() {
|
||||
const { type } = this.line.left;
|
||||
|
||||
return [
|
||||
type,
|
||||
{
|
||||
hll: this.isHighlighted,
|
||||
[LINE_HOVER_CLASS_NAME]:
|
||||
this.isLoggedIn && this.isLeftHover && !this.isContextLineLeft && !this.isMetaLineLeft,
|
||||
},
|
||||
];
|
||||
return utils.classNameMapCell(
|
||||
this.line.left,
|
||||
this.isHighlighted,
|
||||
this.isLoggedIn,
|
||||
this.isLeftHover,
|
||||
);
|
||||
},
|
||||
classNameMapCellRight() {
|
||||
const { type } = this.line.right;
|
||||
|
||||
return [
|
||||
type,
|
||||
{
|
||||
hll: this.isHighlighted,
|
||||
[LINE_HOVER_CLASS_NAME]:
|
||||
this.isLoggedIn &&
|
||||
this.isRightHover &&
|
||||
!this.isContextLineRight &&
|
||||
!this.isMetaLineRight,
|
||||
},
|
||||
];
|
||||
return utils.classNameMapCell(
|
||||
this.line.right,
|
||||
this.isHighlighted,
|
||||
this.isLoggedIn,
|
||||
this.isRightHover,
|
||||
);
|
||||
},
|
||||
addCommentTooltipLeft() {
|
||||
const brokenSymlinks = this.line.left.commentsDisabled;
|
||||
let tooltip = __('Add a comment to this line');
|
||||
|
||||
if (brokenSymlinks) {
|
||||
if (brokenSymlinks.wasSymbolic || brokenSymlinks.isSymbolic) {
|
||||
tooltip = __(
|
||||
'Commenting on symbolic links that replace or are replaced by files is currently not supported.',
|
||||
);
|
||||
} else if (brokenSymlinks.wasReal || brokenSymlinks.isReal) {
|
||||
tooltip = __(
|
||||
'Commenting on files that replace or are replaced by symbolic links is currently not supported.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return tooltip;
|
||||
return utils.addCommentTooltip(this.line.left);
|
||||
},
|
||||
addCommentTooltipRight() {
|
||||
const brokenSymlinks = this.line.right.commentsDisabled;
|
||||
let tooltip = __('Add a comment to this line');
|
||||
|
||||
if (brokenSymlinks) {
|
||||
if (brokenSymlinks.wasSymbolic || brokenSymlinks.isSymbolic) {
|
||||
tooltip = __(
|
||||
'Commenting on symbolic links that replace or are replaced by files is currently not supported.',
|
||||
);
|
||||
} else if (brokenSymlinks.wasReal || brokenSymlinks.isReal) {
|
||||
tooltip = __(
|
||||
'Commenting on files that replace or are replaced by symbolic links is currently not supported.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return tooltip;
|
||||
return utils.addCommentTooltip(this.line.right);
|
||||
},
|
||||
shouldRenderCommentButton() {
|
||||
if (!this.isCommentButtonRendered) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.isLoggedIn) {
|
||||
const isDiffHead = parseBoolean(getParameterByName('diff_head'));
|
||||
return !isDiffHead || gon.features?.mergeRefHeadComments;
|
||||
}
|
||||
|
||||
return false;
|
||||
return utils.shouldRenderCommentButton(
|
||||
this.isLoggedIn,
|
||||
this.isCommentButtonRendered,
|
||||
gon.features?.mergeRefHeadComments,
|
||||
);
|
||||
},
|
||||
shouldShowCommentButtonLeft() {
|
||||
return (
|
||||
this.isLeftHover &&
|
||||
!this.isContextLineLeft &&
|
||||
!this.isMetaLineLeft &&
|
||||
!this.hasDiscussionsLeft
|
||||
return utils.shouldShowCommentButton(
|
||||
this.isLeftHover,
|
||||
this.isContextLineLeft,
|
||||
this.isMetaLineLeft,
|
||||
this.hasDiscussionsLeft,
|
||||
);
|
||||
},
|
||||
shouldShowCommentButtonRight() {
|
||||
return (
|
||||
this.isRightHover &&
|
||||
!this.isContextLineRight &&
|
||||
!this.isMetaLineRight &&
|
||||
!this.hasDiscussionsRight
|
||||
return utils.shouldShowCommentButton(
|
||||
this.isRightHover,
|
||||
this.isContextLineRight,
|
||||
this.isMetaLineRight,
|
||||
this.hasDiscussionsRight,
|
||||
);
|
||||
},
|
||||
hasDiscussionsLeft() {
|
||||
return this.line.left?.discussions?.length > 0;
|
||||
return utils.hasDiscussions(this.line.left);
|
||||
},
|
||||
hasDiscussionsRight() {
|
||||
return this.line.right?.discussions?.length > 0;
|
||||
return utils.hasDiscussions(this.line.right);
|
||||
},
|
||||
lineHrefOld() {
|
||||
return `#${this.line.left.line_code || ''}`;
|
||||
return utils.lineHref(this.line.left);
|
||||
},
|
||||
lineHrefNew() {
|
||||
return `#${this.line.right.line_code || ''}`;
|
||||
return utils.lineHref(this.line.right);
|
||||
},
|
||||
lineCode() {
|
||||
return (
|
||||
(this.line.left && this.line.left.line_code) ||
|
||||
(this.line.right && this.line.right.line_code)
|
||||
);
|
||||
return utils.lineCode(this.line);
|
||||
},
|
||||
isMetaLineLeft() {
|
||||
const type = this.line.left?.type;
|
||||
|
||||
return (
|
||||
type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE
|
||||
);
|
||||
return utils.isMetaLine(this.line.left?.type);
|
||||
},
|
||||
isMetaLineRight() {
|
||||
const type = this.line.right?.type;
|
||||
|
||||
return (
|
||||
type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE
|
||||
);
|
||||
return utils.isMetaLine(this.line.right?.type);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.newLineType = NEW_LINE_TYPE;
|
||||
this.oldLineType = OLD_LINE_TYPE;
|
||||
this.parallelDiffViewType = PARALLEL_DIFF_VIEW_TYPE;
|
||||
},
|
||||
mounted() {
|
||||
this.scrollToLineIfNeededParallel(this.line);
|
||||
this.unwatchShouldShowCommentButton = this.$watch(
|
||||
|
@ -341,6 +251,7 @@ export default {
|
|||
<td :class="parallelViewLeftLineType" class="line-coverage left-side"></td>
|
||||
<td
|
||||
:id="line.left.line_code"
|
||||
:key="line.left.line_code"
|
||||
v-safe-html="line.left.rich_text"
|
||||
:class="parallelViewLeftLineType"
|
||||
class="line_content with-coverage parallel left-side"
|
||||
|
@ -401,6 +312,7 @@ export default {
|
|||
></td>
|
||||
<td
|
||||
:id="line.right.line_code"
|
||||
:key="line.right.rich_text"
|
||||
v-safe-html="line.right.rich_text"
|
||||
:class="[
|
||||
line.right.type,
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import Search from './search';
|
||||
import initStateFilter from '~/search/state_filter';
|
||||
import initConfidentialFilter from '~/search/confidential_filter';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initStateFilter();
|
||||
initConfidentialFilter();
|
||||
|
||||
return new Search();
|
||||
});
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
<script>
|
||||
import { GlDropdown, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui';
|
||||
import { setUrlParams, visitUrl } from '~/lib/utils/url_utility';
|
||||
import { sprintf, s__ } from '~/locale';
|
||||
|
||||
export default {
|
||||
name: 'DropdownFilter',
|
||||
components: {
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlDropdownDivider,
|
||||
},
|
||||
props: {
|
||||
initialFilter: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
filters: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
filtersArray: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
header: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
param: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
scope: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
supportedScopes: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
filter() {
|
||||
return this.initialFilter || this.filters.ANY.value;
|
||||
},
|
||||
selectedFilterText() {
|
||||
const f = this.filtersArray.find(({ value }) => value === this.selectedFilter);
|
||||
if (!f || f === this.filters.ANY) {
|
||||
return sprintf(s__('Any %{header}'), { header: this.header });
|
||||
}
|
||||
|
||||
return f.label;
|
||||
},
|
||||
showDropdown() {
|
||||
return this.supportedScopes.includes(this.scope);
|
||||
},
|
||||
selectedFilter: {
|
||||
get() {
|
||||
if (this.filtersArray.some(({ value }) => value === this.filter)) {
|
||||
return this.filter;
|
||||
}
|
||||
|
||||
return this.filters.ANY.value;
|
||||
},
|
||||
set(filter) {
|
||||
visitUrl(setUrlParams({ [this.param]: filter }));
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dropDownItemClass(filter) {
|
||||
return {
|
||||
'gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2! gl-mb-2':
|
||||
filter === this.filters.ANY,
|
||||
};
|
||||
},
|
||||
isFilterSelected(filter) {
|
||||
return filter === this.selectedFilter;
|
||||
},
|
||||
handleFilterChange(filter) {
|
||||
this.selectedFilter = filter;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-dropdown
|
||||
v-if="showDropdown"
|
||||
:text="selectedFilterText"
|
||||
class="col-3 gl-pt-4 gl-pl-0 gl-pr-0 gl-mr-4"
|
||||
menu-class="gl-w-full! gl-pl-0"
|
||||
>
|
||||
<header class="gl-text-center gl-font-weight-bold gl-font-lg">
|
||||
{{ header }}
|
||||
</header>
|
||||
<gl-dropdown-divider />
|
||||
<gl-dropdown-item
|
||||
v-for="f in filtersArray"
|
||||
:key="f.value"
|
||||
:is-check-item="true"
|
||||
:is-checked="isFilterSelected(f.value)"
|
||||
:class="dropDownItemClass(f)"
|
||||
@click="handleFilterChange(f.value)"
|
||||
>
|
||||
{{ f.label }}
|
||||
</gl-dropdown-item>
|
||||
</gl-dropdown>
|
||||
</template>
|
|
@ -0,0 +1,28 @@
|
|||
import { __ } from '~/locale';
|
||||
|
||||
export const FILTER_HEADER = __('Confidentiality');
|
||||
|
||||
export const FILTER_STATES = {
|
||||
ANY: {
|
||||
label: __('Any'),
|
||||
value: null,
|
||||
},
|
||||
CONFIDENTIAL: {
|
||||
label: __('Confidential'),
|
||||
value: 'yes',
|
||||
},
|
||||
NOT_CONFIDENTIAL: {
|
||||
label: __('Not confidential'),
|
||||
value: 'no',
|
||||
},
|
||||
};
|
||||
|
||||
export const SCOPES = {
|
||||
ISSUES: 'issues',
|
||||
};
|
||||
|
||||
export const FILTER_STATES_BY_SCOPE = {
|
||||
[SCOPES.ISSUES]: [FILTER_STATES.ANY, FILTER_STATES.CONFIDENTIAL, FILTER_STATES.NOT_CONFIDENTIAL],
|
||||
};
|
||||
|
||||
export const FILTER_PARAM = 'confidential';
|
|
@ -0,0 +1,39 @@
|
|||
import Vue from 'vue';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
import DropdownFilter from '../components/dropdown_filter.vue';
|
||||
import {
|
||||
FILTER_HEADER,
|
||||
FILTER_PARAM,
|
||||
FILTER_STATES_BY_SCOPE,
|
||||
FILTER_STATES,
|
||||
SCOPES,
|
||||
} from './constants';
|
||||
|
||||
Vue.use(Translate);
|
||||
|
||||
export default () => {
|
||||
const el = document.getElementById('js-search-filter-by-confidential');
|
||||
|
||||
if (!el) return false;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
data() {
|
||||
return { ...el.dataset };
|
||||
},
|
||||
|
||||
render(createElement) {
|
||||
return createElement(DropdownFilter, {
|
||||
props: {
|
||||
initialFilter: this.filter,
|
||||
filtersArray: FILTER_STATES_BY_SCOPE[this.scope],
|
||||
filters: FILTER_STATES,
|
||||
header: FILTER_HEADER,
|
||||
param: FILTER_PARAM,
|
||||
scope: this.scope,
|
||||
supportedScopes: Object.values(SCOPES),
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
|
@ -1,94 +0,0 @@
|
|||
<script>
|
||||
import { GlDropdown, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui';
|
||||
import {
|
||||
FILTER_STATES,
|
||||
SCOPES,
|
||||
FILTER_STATES_BY_SCOPE,
|
||||
FILTER_HEADER,
|
||||
FILTER_TEXT,
|
||||
} from '../constants';
|
||||
import { setUrlParams, visitUrl } from '~/lib/utils/url_utility';
|
||||
|
||||
const FILTERS_ARRAY = Object.values(FILTER_STATES);
|
||||
|
||||
export default {
|
||||
name: 'StateFilter',
|
||||
components: {
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlDropdownDivider,
|
||||
},
|
||||
props: {
|
||||
scope: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
state: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: FILTER_STATES.ANY.value,
|
||||
validator: v => FILTERS_ARRAY.some(({ value }) => value === v),
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
selectedFilterText() {
|
||||
const filter = FILTERS_ARRAY.find(({ value }) => value === this.selectedFilter);
|
||||
if (!filter || filter === FILTER_STATES.ANY) {
|
||||
return FILTER_TEXT;
|
||||
}
|
||||
|
||||
return filter.label;
|
||||
},
|
||||
showDropdown() {
|
||||
return Object.values(SCOPES).includes(this.scope);
|
||||
},
|
||||
selectedFilter: {
|
||||
get() {
|
||||
if (FILTERS_ARRAY.some(({ value }) => value === this.state)) {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
return FILTER_STATES.ANY.value;
|
||||
},
|
||||
set(state) {
|
||||
visitUrl(setUrlParams({ state }));
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dropDownItemClass(filter) {
|
||||
return {
|
||||
'gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2! gl-mb-2':
|
||||
filter === FILTER_STATES.ANY,
|
||||
};
|
||||
},
|
||||
isFilterSelected(filter) {
|
||||
return filter === this.selectedFilter;
|
||||
},
|
||||
handleFilterChange(state) {
|
||||
this.selectedFilter = state;
|
||||
},
|
||||
},
|
||||
filterStates: FILTER_STATES,
|
||||
filterHeader: FILTER_HEADER,
|
||||
filtersByScope: FILTER_STATES_BY_SCOPE,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-dropdown v-if="showDropdown" :text="selectedFilterText" class="col-sm-3 gl-pt-4 gl-pl-0">
|
||||
<header class="gl-text-center gl-font-weight-bold gl-font-lg">
|
||||
{{ $options.filterHeader }}
|
||||
</header>
|
||||
<gl-dropdown-divider />
|
||||
<gl-dropdown-item
|
||||
v-for="filter in $options.filtersByScope[scope]"
|
||||
:key="filter.value"
|
||||
:is-check-item="true"
|
||||
:is-checked="isFilterSelected(filter.value)"
|
||||
:class="dropDownItemClass(filter)"
|
||||
@click="handleFilterChange(filter.value)"
|
||||
>{{ filter.label }}</gl-dropdown-item
|
||||
>
|
||||
</gl-dropdown>
|
||||
</template>
|
|
@ -2,8 +2,6 @@ import { __ } from '~/locale';
|
|||
|
||||
export const FILTER_HEADER = __('Status');
|
||||
|
||||
export const FILTER_TEXT = __('Any Status');
|
||||
|
||||
export const FILTER_STATES = {
|
||||
ANY: {
|
||||
label: __('Any'),
|
||||
|
@ -37,3 +35,5 @@ export const FILTER_STATES_BY_SCOPE = {
|
|||
FILTER_STATES.CLOSED,
|
||||
],
|
||||
};
|
||||
|
||||
export const FILTER_PARAM = 'state';
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
import Vue from 'vue';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
import StateFilter from './components/state_filter.vue';
|
||||
import DropdownFilter from '../components/dropdown_filter.vue';
|
||||
import {
|
||||
FILTER_HEADER,
|
||||
FILTER_PARAM,
|
||||
FILTER_STATES_BY_SCOPE,
|
||||
FILTER_STATES,
|
||||
SCOPES,
|
||||
} from './constants';
|
||||
|
||||
Vue.use(Translate);
|
||||
|
||||
|
@ -11,22 +18,20 @@ export default () => {
|
|||
|
||||
return new Vue({
|
||||
el,
|
||||
components: {
|
||||
StateFilter,
|
||||
},
|
||||
data() {
|
||||
const { dataset } = this.$options.el;
|
||||
return {
|
||||
scope: dataset.scope,
|
||||
state: dataset.state,
|
||||
};
|
||||
return { ...el.dataset };
|
||||
},
|
||||
|
||||
render(createElement) {
|
||||
return createElement('state-filter', {
|
||||
return createElement(DropdownFilter, {
|
||||
props: {
|
||||
initialFilter: this.filter,
|
||||
filtersArray: FILTER_STATES_BY_SCOPE[this.scope],
|
||||
filters: FILTER_STATES,
|
||||
header: FILTER_HEADER,
|
||||
param: FILTER_PARAM,
|
||||
scope: this.scope,
|
||||
state: this.state,
|
||||
supportedScopes: Object.values(SCOPES),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
@ -112,8 +112,7 @@ a {
|
|||
}
|
||||
|
||||
.dropdown-menu a,
|
||||
.dropdown-menu button,
|
||||
.dropdown-menu-nav a {
|
||||
.dropdown-menu button {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
|
|
|
@ -33,8 +33,7 @@
|
|||
}
|
||||
|
||||
.show.dropdown {
|
||||
.dropdown-menu,
|
||||
.dropdown-menu-nav {
|
||||
.dropdown-menu {
|
||||
@include set-visible;
|
||||
min-height: $dropdown-min-height;
|
||||
max-height: $dropdown-max-height;
|
||||
|
@ -258,8 +257,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.dropdown-menu,
|
||||
.dropdown-menu-nav {
|
||||
.dropdown-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
width: auto;
|
||||
|
@ -393,49 +391,56 @@
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dropdown-menu li {
|
||||
cursor: pointer;
|
||||
.dropdown-menu {
|
||||
display: none;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
|
||||
&.droplab-item-active button {
|
||||
@include dropdown-item-hover;
|
||||
}
|
||||
li {
|
||||
cursor: pointer;
|
||||
|
||||
> a,
|
||||
> button {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
text-overflow: inherit;
|
||||
text-align: left;
|
||||
&.droplab-item-active button {
|
||||
@include dropdown-item-hover;
|
||||
}
|
||||
|
||||
&.btn .fa:not(:last-child) {
|
||||
> a,
|
||||
> button {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
text-overflow: inherit;
|
||||
text-align: left;
|
||||
|
||||
&.btn .fa:not(:last-child) {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
> button.dropdown-epic-button {
|
||||
flex-direction: column;
|
||||
|
||||
.reference {
|
||||
color: $gray-300;
|
||||
margin-top: $gl-padding-4;
|
||||
}
|
||||
}
|
||||
|
||||
&.droplab-item-selected i {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.icon {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.description {
|
||||
display: inline-block;
|
||||
white-space: normal;
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
> button.dropdown-epic-button {
|
||||
flex-direction: column;
|
||||
|
||||
.reference {
|
||||
color: $gray-300;
|
||||
margin-top: $gl-padding-4;
|
||||
}
|
||||
}
|
||||
|
||||
&.droplab-item-selected i {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.icon {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.description {
|
||||
display: inline-block;
|
||||
white-space: normal;
|
||||
margin-left: 5px;
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -447,21 +452,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.droplab-dropdown .dropdown-menu,
|
||||
.droplab-dropdown .dropdown-menu-nav {
|
||||
display: none;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.comment-type-dropdown.show .dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.filtered-search-box-input-container {
|
||||
.dropdown-menu,
|
||||
.dropdown-menu-nav {
|
||||
.dropdown-menu {
|
||||
max-width: 280px;
|
||||
}
|
||||
}
|
||||
|
@ -850,8 +846,7 @@
|
|||
}
|
||||
|
||||
header.navbar-gitlab .dropdown {
|
||||
.dropdown-menu,
|
||||
.dropdown-menu-nav {
|
||||
.dropdown-menu {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
|
|
@ -227,6 +227,10 @@
|
|||
padding-left: 40px;
|
||||
}
|
||||
|
||||
.gl-label-scoped {
|
||||
--label-inset-border: inset 0 0 0 1px currentColor;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ class Admin::UsersController < Admin::ApplicationController
|
|||
def index
|
||||
@users = User.filter_items(params[:filter]).order_name_asc
|
||||
@users = @users.search_with_secondary_emails(params[:search_query]) if params[:search_query].present?
|
||||
@users = @users.includes(:authorized_projects) # rubocop: disable CodeReuse/ActiveRecord
|
||||
@users = @users.sort_by_attribute(@sort = params[:sort])
|
||||
@users = @users.page(params[:page])
|
||||
end
|
||||
|
|
|
@ -60,13 +60,7 @@ class Groups::LabelsController < Groups::ApplicationController
|
|||
|
||||
def destroy
|
||||
@label.destroy
|
||||
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
redirect_to group_labels_path(@group), status: :found, notice: "#{@label.name} deleted permanently"
|
||||
end
|
||||
format.js
|
||||
end
|
||||
redirect_to group_labels_path(@group), status: :found, notice: "#{@label.name} deleted permanently"
|
||||
end
|
||||
|
||||
protected
|
||||
|
|
|
@ -11,8 +11,6 @@ module Projects
|
|||
push_frontend_feature_flag(:ci_key_autocomplete, default_enabled: true)
|
||||
end
|
||||
|
||||
helper_method :highlight_badge
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
|
@ -52,10 +50,6 @@ module Projects
|
|||
|
||||
private
|
||||
|
||||
def highlight_badge(name, content, language = nil)
|
||||
Gitlab::Highlight.highlight(name, content, language: language)
|
||||
end
|
||||
|
||||
def update_params
|
||||
params.require(:project).permit(*permitted_project_params)
|
||||
end
|
||||
|
|
|
@ -38,7 +38,6 @@ class SearchController < ApplicationController
|
|||
@show_snippets = search_service.show_snippets?
|
||||
@search_results = search_service.search_results
|
||||
@search_objects = search_service.search_objects(preload_method)
|
||||
@search_highlight = search_service.search_highlight
|
||||
|
||||
render_commits if @scope == 'commits'
|
||||
eager_load_user_status if @scope == 'users'
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module BlobHelper
|
||||
def highlight(file_name, file_content, language: nil, plain: false)
|
||||
highlighted = Gitlab::Highlight.highlight(file_name, file_content, plain: plain, language: language)
|
||||
|
||||
raw %(<pre class="code highlight"><code>#{highlighted}</code></pre>)
|
||||
end
|
||||
|
||||
def no_highlight_files
|
||||
%w(credits changelog news copying copyright license authors)
|
||||
end
|
||||
|
|
|
@ -299,13 +299,6 @@ module SearchHelper
|
|||
simple_search_highlight_and_truncate(issue.description, search_term, highlighter: '<span class="gl-text-black-normal gl-font-weight-bold">\1</span>')
|
||||
end
|
||||
|
||||
def simple_search_highlight_and_truncate(text, phrase, options = {})
|
||||
truncate_length = options.delete(:length) { 200 }
|
||||
text = truncate(text, length: truncate_length)
|
||||
phrase = phrase.split
|
||||
highlight(text, phrase, options)
|
||||
end
|
||||
|
||||
def show_user_search_tab?
|
||||
return false if Feature.disabled?(:users_search, default_enabled: true)
|
||||
|
||||
|
|
|
@ -1302,6 +1302,14 @@ class MergeRequest < ApplicationRecord
|
|||
unlock_mr
|
||||
end
|
||||
|
||||
def update_and_mark_in_progress_merge_commit_sha(commit_id)
|
||||
self.update(in_progress_merge_commit_sha: commit_id)
|
||||
# Since another process checks for matching merge request, we need
|
||||
# to make it possible to detect whether the query should go to the
|
||||
# primary.
|
||||
target_project.mark_primary_write_location
|
||||
end
|
||||
|
||||
def diverged_commits_count
|
||||
cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits")
|
||||
|
||||
|
|
|
@ -2292,6 +2292,10 @@ class Project < ApplicationRecord
|
|||
[]
|
||||
end
|
||||
|
||||
def mark_primary_write_location
|
||||
# Overriden in EE
|
||||
end
|
||||
|
||||
def toggle_ci_cd_settings!(settings_attribute)
|
||||
ci_cd_settings.toggle!(settings_attribute)
|
||||
end
|
||||
|
|
|
@ -853,7 +853,7 @@ class Repository
|
|||
def merge(user, source_sha, merge_request, message)
|
||||
with_cache_hooks do
|
||||
raw_repository.merge(user, source_sha, merge_request.target_branch, message) do |commit_id|
|
||||
merge_request.update(in_progress_merge_commit_sha: commit_id)
|
||||
merge_request.update_and_mark_in_progress_merge_commit_sha(commit_id)
|
||||
nil # Return value does not matter.
|
||||
end
|
||||
end
|
||||
|
@ -873,7 +873,7 @@ class Repository
|
|||
their_commit_id = commit(source)&.id
|
||||
raise 'Invalid merge source' if their_commit_id.nil?
|
||||
|
||||
merge_request&.update(in_progress_merge_commit_sha: their_commit_id)
|
||||
merge_request&.update_and_mark_in_progress_merge_commit_sha(their_commit_id)
|
||||
|
||||
with_cache_hooks { raw.ff_merge(user, their_commit_id, target_branch) }
|
||||
end
|
||||
|
|
|
@ -8,6 +8,9 @@ class UserPreference < ApplicationRecord
|
|||
|
||||
belongs_to :user
|
||||
|
||||
scope :with_user, -> { joins(:user) }
|
||||
scope :gitpod_enabled, -> { where(gitpod_enabled: true) }
|
||||
|
||||
validates :issue_notes_filter, :merge_request_notes_filter, inclusion: { in: NOTES_FILTERS.values }, presence: true
|
||||
validates :tab_width, numericality: {
|
||||
only_integer: true,
|
||||
|
|
|
@ -27,7 +27,7 @@ module MergeRequests
|
|||
rescue StandardError => e
|
||||
raise MergeError, "Something went wrong during merge: #{e.message}"
|
||||
ensure
|
||||
merge_request.update(in_progress_merge_commit_sha: nil)
|
||||
merge_request.update_and_mark_in_progress_merge_commit_sha(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -84,7 +84,7 @@ module MergeRequests
|
|||
|
||||
merge_request.update!(merge_commit_sha: commit_id)
|
||||
ensure
|
||||
merge_request.update_column(:in_progress_merge_commit_sha, nil)
|
||||
merge_request.update_and_mark_in_progress_merge_commit_sha(nil)
|
||||
end
|
||||
|
||||
def try_merge
|
||||
|
|
|
@ -65,10 +65,6 @@ class SearchService
|
|||
@search_objects ||= redact_unauthorized_results(search_results.objects(scope, page: params[:page], per_page: per_page, preload_method: preload_method))
|
||||
end
|
||||
|
||||
def search_highlight
|
||||
search_results.highlight_map(scope)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def per_page
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
%h3.text-center
|
||||
= s_('AdminArea|Projects: %{number_of_projects}') % { number_of_projects: approximate_count_with_delimiters(@counts, Project) }
|
||||
%hr
|
||||
= link_to(s_('AdminArea|New project'), new_project_path, class: "btn btn-success gl-w-full")
|
||||
= link_to(s_('AdminArea|New project'), new_project_path, class: "btn gl-button btn-success gl-w-full")
|
||||
.col-sm-4
|
||||
.info-well.dark-well
|
||||
.well-segment.well-centered
|
||||
|
@ -28,8 +28,8 @@
|
|||
= s_('AdminArea|Users: %{number_of_users}') % { number_of_users: approximate_count_with_delimiters(@counts, User) }
|
||||
%hr
|
||||
.btn-group.d-flex{ role: 'group' }
|
||||
= link_to s_('AdminArea|New user'), new_admin_user_path, class: "btn btn-success gl-w-full"
|
||||
= link_to s_('AdminArea|Users statistics'), admin_dashboard_stats_path, class: 'btn btn-primary gl-w-full'
|
||||
= link_to s_('AdminArea|New user'), new_admin_user_path, class: "btn gl-button btn-success gl-w-full"
|
||||
= link_to s_('AdminArea|Users statistics'), admin_dashboard_stats_path, class: 'btn gl-button btn-info gl-w-full'
|
||||
.col-sm-4
|
||||
.info-well.dark-well
|
||||
.well-segment.well-centered
|
||||
|
@ -37,7 +37,7 @@
|
|||
%h3.text-center
|
||||
= s_('AdminArea|Groups: %{number_of_groups}') % { number_of_groups: approximate_count_with_delimiters(@counts, Group) }
|
||||
%hr
|
||||
= link_to s_('AdminArea|New group'), new_admin_group_path, class: "btn btn-success gl-w-full"
|
||||
= link_to s_('AdminArea|New group'), new_admin_group_path, class: "btn gl-button btn-success gl-w-full"
|
||||
.row
|
||||
.col-md-4
|
||||
#js-admin-statistics-container
|
||||
|
|
|
@ -27,5 +27,5 @@
|
|||
= render_suggested_colors
|
||||
|
||||
.form-actions
|
||||
= f.submit _('Save'), class: 'btn btn-success js-save-button'
|
||||
= link_to _("Cancel"), admin_labels_path, class: 'btn btn-cancel'
|
||||
= f.submit _('Save'), class: 'btn gl-button btn-success js-save-button'
|
||||
= link_to _("Cancel"), admin_labels_path, class: 'btn gl-button btn-cancel'
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
%li.label-list-item{ id: dom_id(label) }
|
||||
= render "shared/label_row", label: label.present(issuable_subject: nil)
|
||||
.label-actions-list
|
||||
= link_to edit_admin_label_path(label), class: 'btn btn-transparent label-action has-tooltip', title: _('Edit'), data: { placement: 'bottom' }, aria_label: _('Edit') do
|
||||
= link_to edit_admin_label_path(label), class: 'btn gl-button btn-transparent label-action has-tooltip', title: _('Edit'), data: { placement: 'bottom' }, aria_label: _('Edit') do
|
||||
= sprite_icon('pencil')
|
||||
= link_to admin_label_path(label), class: 'btn btn-transparent remove-row label-action has-tooltip', title: _('Delete'), data: { placement: 'bottom', confirm: "Delete this label? Are you sure?" }, aria_label: _('Delete'), method: :delete, remote: true do
|
||||
= link_to admin_label_path(label), class: 'btn gl-button btn-transparent remove-row label-action has-tooltip', title: _('Delete'), data: { placement: 'bottom', confirm: "Delete this label? Are you sure?" }, aria_label: _('Delete'), method: :delete, remote: true do
|
||||
= sprite_icon('remove')
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
- page_title _("Labels")
|
||||
|
||||
%div
|
||||
= link_to new_admin_label_path, class: "float-right btn btn-nr btn-success" do
|
||||
= link_to new_admin_label_path, class: "float-right btn gl-button btn-nr btn-success" do
|
||||
= _('New label')
|
||||
%h3.page-title
|
||||
= _('Labels')
|
||||
|
|
|
@ -4,7 +4,12 @@
|
|||
= _('Name')
|
||||
.table-mobile-content
|
||||
= render 'user_detail', user: user
|
||||
.table-section.section-25
|
||||
.table-section.section-10
|
||||
.table-mobile-header{ role: 'rowheader' }
|
||||
= _('Projects')
|
||||
.table-mobile-content.gl-str-truncated{ data: { testid: "user-project-count-#{user.id}" } }
|
||||
= user.authorized_projects.length
|
||||
.table-section.section-15
|
||||
.table-mobile-header{ role: 'rowheader' }
|
||||
= _('Created on')
|
||||
.table-mobile-content
|
||||
|
|
|
@ -72,7 +72,8 @@
|
|||
.table-holder
|
||||
.thead-white.text-nowrap.gl-responsive-table-row.table-row-header{ role: 'row' }
|
||||
.table-section.section-40{ role: 'rowheader' }= _('Name')
|
||||
.table-section.section-25{ role: 'rowheader' }= _('Created on')
|
||||
.table-section.section-10{ role: 'rowheader' }= _('Projects')
|
||||
.table-section.section-15{ role: 'rowheader' }= _('Created on')
|
||||
.table-section.section-15{ role: 'rowheader' }= _('Last activity')
|
||||
|
||||
= render partial: 'admin/users/user', collection: @users
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
- if @group.labels.empty?
|
||||
$('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)
|
|
@ -28,7 +28,8 @@
|
|||
= render partial: 'projects/commits/commit', collection: context_commits, locals: { project: project, ref: ref, merge_request: merge_request }
|
||||
|
||||
- if hidden > 0
|
||||
%li.alert.alert-warning
|
||||
%li.gl-alert.gl-alert-warning
|
||||
= sprite_icon('warning', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
|
||||
= n_('%s additional commit has been omitted to prevent performance issues.', '%s additional commits have been omitted to prevent performance issues.', hidden) % number_with_delimiter(hidden)
|
||||
|
||||
- if project.context_commits_enabled? && can_update_merge_request && context_commits&.empty?
|
||||
|
|
|
@ -7,6 +7,6 @@
|
|||
%h4.gl-mt-0
|
||||
Request details
|
||||
.col-lg-9
|
||||
= link_to 'Resend Request', @hook_log.present.retry_path, method: :post, class: "btn btn-default float-right gl-ml-3"
|
||||
= link_to 'Resend Request', @hook_log.present.retry_path, method: :post, class: "btn gl-button btn-default float-right gl-ml-3"
|
||||
|
||||
= render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log }
|
||||
|
|
|
@ -15,18 +15,18 @@
|
|||
.col-md-2.text-center
|
||||
Markdown
|
||||
.col-md-10.code.js-syntax-highlight
|
||||
= highlight_badge('.md', badge.to_markdown, language: 'markdown')
|
||||
= highlight('.md', badge.to_markdown, language: 'markdown')
|
||||
.row
|
||||
%hr
|
||||
.row
|
||||
.col-md-2.text-center
|
||||
HTML
|
||||
.col-md-10.code.js-syntax-highlight
|
||||
= highlight_badge('.html', badge.to_html, language: 'html')
|
||||
= highlight('.html', badge.to_html, language: 'html')
|
||||
.row
|
||||
%hr
|
||||
.row
|
||||
.col-md-2.text-center
|
||||
AsciiDoc
|
||||
.col-md-10.code.js-syntax-highlight
|
||||
= highlight_badge('.adoc', badge.to_asciidoc)
|
||||
= highlight('.adoc', badge.to_asciidoc)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
- if @search_objects.to_a.empty?
|
||||
= render partial: "search/results/filters"
|
||||
= render partial: "search/results/empty"
|
||||
= render_if_exists 'shared/promotions/promote_advanced_search'
|
||||
= render_if_exists 'search/form_revert_to_basic'
|
||||
|
@ -21,8 +22,7 @@
|
|||
- link_to_group = link_to(@group.name, @group, class: 'ml-md-1')
|
||||
= _("in group %{link_to_group}").html_safe % { link_to_group: link_to_group }
|
||||
= render_if_exists 'shared/promotions/promote_advanced_search'
|
||||
|
||||
#js-search-filter-by-state{ 'v-cloak': true, data: { scope: @scope, state: params[:state] } }
|
||||
= render partial: "search/results/filters"
|
||||
|
||||
.results.gl-mt-3
|
||||
- if @scope == 'commits'
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
.d-lg-flex.align-items-end
|
||||
#js-search-filter-by-state{ 'v-cloak': true, data: { scope: @scope, filter: params[:state]} }
|
||||
- if Feature.enabled?(:search_filter_by_confidential, @group)
|
||||
#js-search-filter-by-confidential{ 'v-cloak': true, data: { scope: @scope, filter: params[:confidential] } }
|
||||
|
||||
- if %w(issues merge_requests).include?(@scope)
|
||||
%hr.gl-mt-4.gl-mb-4
|
|
@ -9,5 +9,6 @@
|
|||
%span.term.str-truncated.gl-font-weight-bold.gl-ml-2= issue.title
|
||||
.gl-text-gray-500.gl-my-3
|
||||
= sprintf(s_(' %{project_name}#%{issue_iid} · opened %{issue_created} by %{author}'), { project_name: issue.project.full_name, issue_iid: issue.iid, issue_created: time_ago_with_tooltip(issue.created_at, placement: 'bottom'), author: link_to_member(@project, issue.author, avatar: false) }).html_safe
|
||||
.description.term.col-sm-10.gl-px-0
|
||||
= highlight_and_truncate_issue(issue, @search_term, @search_highlight)
|
||||
- if issue.description.present?
|
||||
.description.term.col-sm-10.gl-px-0
|
||||
= truncate(issue.description, length: 200)
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Set hook_log css to gl-button
|
||||
merge_request: 42730
|
||||
author: Mike Terhar @mterhar
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add Gitpod enabled user setting to Usage Data
|
||||
merge_request: 42570
|
||||
author:
|
||||
type: changed
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Global Search - Bold Issue's Search Term
|
||||
merge_request: 41411
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove duplicate index on cluster_agents
|
||||
merge_request: 42902
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Drop Iglu registry URL column
|
||||
merge_request: 42939
|
||||
author:
|
||||
type: removed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Display user project count on Admin Dashboard
|
||||
merge_request: 42871
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Move shared logic into utils
|
||||
merge_request: 42407
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allow member mapping to map importer user on Group/Project Import
|
||||
merge_request: 42882
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix profile scoped label CSS
|
||||
merge_request: 43005
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: search_filter_by_confidential
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40793
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/244923
|
||||
group: group::global search
|
||||
type: development
|
||||
default_enabled: false
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexToUserPreferences < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :user_preferences, :gitpod_enabled, name: :index_user_preferences_on_gitpod_enabled
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index :user_preferences, :gitpod_enabled, name: :index_user_preferences_on_gitpod_enabled
|
||||
end
|
||||
end
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveDuplicateClusterAgentsIndex < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
INDEX = 'index_cluster_agents_on_project_id'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
remove_concurrent_index_by_name :cluster_agents, INDEX
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_index :cluster_agents, :project_id, name: INDEX
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DropSnowplowIgluRegistryUrlFromApplicationSettings < ActiveRecord::Migration[6.0]
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
remove_column :application_settings, :snowplow_iglu_registry_url, :string, limit: 255
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
8d14013bcb4d8302c91e331f619fb6f621ab79907aebc421d99c9484ecd7a5d8
|
|
@ -0,0 +1 @@
|
|||
7f62ce5117a16213bad6537dfeae2af4016262c533f8fa6b7a19572077bcf8d7
|
|
@ -0,0 +1 @@
|
|||
ad63096e49440f7f2a15ea2747689ca39f52fdcebc1949a1feed82a22f432e9e
|
|
@ -9196,7 +9196,6 @@ CREATE TABLE application_settings (
|
|||
throttle_incident_management_notification_enabled boolean DEFAULT false NOT NULL,
|
||||
throttle_incident_management_notification_period_in_seconds integer DEFAULT 3600,
|
||||
throttle_incident_management_notification_per_period integer DEFAULT 3600,
|
||||
snowplow_iglu_registry_url character varying(255),
|
||||
push_event_hooks_limit integer DEFAULT 3 NOT NULL,
|
||||
push_event_activities_limit integer DEFAULT 3 NOT NULL,
|
||||
custom_http_clone_url_root character varying(511),
|
||||
|
@ -19789,8 +19788,6 @@ CREATE INDEX index_cluster_agent_tokens_on_agent_id ON cluster_agent_tokens USIN
|
|||
|
||||
CREATE UNIQUE INDEX index_cluster_agent_tokens_on_token_encrypted ON cluster_agent_tokens USING btree (token_encrypted);
|
||||
|
||||
CREATE INDEX index_cluster_agents_on_project_id ON cluster_agents USING btree (project_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_cluster_agents_on_project_id_and_name ON cluster_agents USING btree (project_id, name);
|
||||
|
||||
CREATE UNIQUE INDEX index_cluster_groups_on_cluster_id_and_group_id ON cluster_groups USING btree (cluster_id, group_id);
|
||||
|
@ -21335,6 +21332,8 @@ CREATE UNIQUE INDEX index_user_interacted_projects_on_project_id_and_user_id ON
|
|||
|
||||
CREATE INDEX index_user_interacted_projects_on_user_id ON user_interacted_projects USING btree (user_id);
|
||||
|
||||
CREATE INDEX index_user_preferences_on_gitpod_enabled ON user_preferences USING btree (gitpod_enabled);
|
||||
|
||||
CREATE UNIQUE INDEX index_user_preferences_on_user_id ON user_preferences USING btree (user_id);
|
||||
|
||||
CREATE INDEX index_user_statuses_on_user_id ON user_statuses USING btree (user_id);
|
||||
|
|
|
@ -82,6 +82,10 @@ local machine, this is a simple way to get started:
|
|||
-backend-config="retry_wait_min=5"
|
||||
```
|
||||
|
||||
NOTE: **Note:**
|
||||
The name of your state can contain only uppercase and lowercase letters,
|
||||
decimal digits, hyphens and underscores.
|
||||
|
||||
You can now run `terraform plan` and `terraform apply` as you normally would.
|
||||
|
||||
## Get started using GitLab CI
|
||||
|
|
|
@ -20,3 +20,5 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
Gitlab::Checks::MatchingMergeRequest.prepend_if_ee('EE::Gitlab::Checks::MatchingMergeRequest')
|
||||
|
|
|
@ -72,7 +72,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def with_lock_retries(&block)
|
||||
Gitlab::Database::WithLockRetries.new({
|
||||
Gitlab::Database::WithLockRetries.new(**{
|
||||
klass: self.class,
|
||||
logger: Gitlab::AppLogger
|
||||
}).run(&block)
|
||||
|
|
|
@ -35,7 +35,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def include?(old_user_id)
|
||||
map.has_key?(old_user_id) && map[old_user_id] != default_user_id
|
||||
map.has_key?(old_user_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -63,6 +63,8 @@ module Gitlab
|
|||
end
|
||||
|
||||
def add_team_member(member, existing_user = nil)
|
||||
return true if existing_user && @importable.members.exists?(user_id: existing_user.id)
|
||||
|
||||
member['user'] = existing_user
|
||||
member_hash = member_hash(member)
|
||||
|
||||
|
|
|
@ -116,11 +116,6 @@ module Gitlab
|
|||
UsersFinder.new(current_user, search: query).execute
|
||||
end
|
||||
|
||||
# highlighting is only performed by Elasticsearch backed results
|
||||
def highlight_map(scope)
|
||||
{}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def projects
|
||||
|
|
|
@ -445,8 +445,11 @@ module Gitlab
|
|||
# rubocop: enable UsageData/LargeTable
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
# augmented in EE
|
||||
def user_preferences_usage
|
||||
{} # augmented in EE
|
||||
{
|
||||
user_preferences_user_gitpod_enabled: count(UserPreference.with_user.gitpod_enabled.merge(User.active))
|
||||
}
|
||||
end
|
||||
|
||||
def merge_requests_users(time_period)
|
||||
|
|
|
@ -3037,10 +3037,10 @@ msgstr ""
|
|||
msgid "Any"
|
||||
msgstr ""
|
||||
|
||||
msgid "Any Author"
|
||||
msgid "Any %{header}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Any Status"
|
||||
msgid "Any Author"
|
||||
msgstr ""
|
||||
|
||||
msgid "Any branch"
|
||||
|
|
|
@ -23,6 +23,12 @@ RSpec.describe Admin::UsersController do
|
|||
|
||||
expect(assigns(:users)).to eq([admin])
|
||||
end
|
||||
|
||||
it 'eager loads authorized projects association' do
|
||||
get :index
|
||||
|
||||
expect(assigns(:users).first.association(:authorized_projects)).to be_loaded
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET :id' do
|
||||
|
|
|
@ -56,4 +56,43 @@ RSpec.describe Groups::LabelsController do
|
|||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE #destroy' do
|
||||
context 'when current user has ability to destroy the label' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'removes the label' do
|
||||
label = create(:group_label, group: group)
|
||||
delete :destroy, params: { group_id: group.to_param, id: label.to_param }
|
||||
|
||||
expect { label.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
context 'when label is succesfuly destroyed' do
|
||||
it 'redirects to the group labels page' do
|
||||
label = create(:group_label, group: group)
|
||||
delete :destroy, params: { group_id: group.to_param, id: label.to_param }
|
||||
|
||||
expect(response).to redirect_to(group_labels_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when current_user does not have ability to destroy the label' do
|
||||
let(:another_user) { create(:user) }
|
||||
|
||||
before do
|
||||
sign_in(another_user)
|
||||
end
|
||||
|
||||
it 'responds with status 404' do
|
||||
label = create(:group_label, group: group)
|
||||
delete :destroy, params: { group_id: group.to_param, id: label.to_param }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -102,6 +102,9 @@ FactoryBot.define do
|
|||
create(:package, project: projects[1])
|
||||
create(:package, created_at: 2.months.ago, project: projects[1])
|
||||
|
||||
# User Preferences
|
||||
create(:user_preference, gitpod_enabled: true)
|
||||
|
||||
ProjectFeature.first.update_attribute('repository_access_level', 0)
|
||||
|
||||
# Create fresh & a month (28-days SMAU) old data
|
||||
|
|
|
@ -31,6 +31,7 @@ RSpec.describe "Admin::Users" do
|
|||
expect(page).to have_content(current_user.last_activity_on.strftime("%e %b, %Y"))
|
||||
expect(page).to have_content(user.email)
|
||||
expect(page).to have_content(user.name)
|
||||
expect(page).to have_content('Projects')
|
||||
expect(page).to have_button('Block')
|
||||
expect(page).to have_button('Deactivate')
|
||||
expect(page).to have_button('Delete user')
|
||||
|
@ -48,6 +49,19 @@ RSpec.describe "Admin::Users" do
|
|||
end
|
||||
end
|
||||
|
||||
context 'user project count' do
|
||||
before do
|
||||
project = create(:project)
|
||||
project.add_maintainer(current_user)
|
||||
end
|
||||
|
||||
it 'displays count of users projects' do
|
||||
visit admin_users_path
|
||||
|
||||
expect(page.find("[data-testid='user-project-count-#{current_user.id}']").text).to eq("1")
|
||||
end
|
||||
end
|
||||
|
||||
describe 'search and sort' do
|
||||
before do
|
||||
create(:user, name: 'Foo Bar', last_activity_on: 3.days.ago)
|
||||
|
|
|
@ -17,10 +17,10 @@ RSpec.describe 'list of badges' do
|
|||
expect(page).to have_content 'Markdown'
|
||||
expect(page).to have_content 'HTML'
|
||||
expect(page).to have_content 'AsciiDoc'
|
||||
expect(page).to have_css('.js-syntax-highlight', count: 3)
|
||||
expect(page).to have_css('.highlight', count: 3)
|
||||
expect(page).to have_xpath("//img[@alt='pipeline status']")
|
||||
|
||||
page.within('.js-syntax-highlight', match: :first) do
|
||||
page.within('.highlight', match: :first) do
|
||||
expect(page).to have_content 'badges/master/pipeline.svg'
|
||||
end
|
||||
end
|
||||
|
@ -32,10 +32,10 @@ RSpec.describe 'list of badges' do
|
|||
expect(page).to have_content 'Markdown'
|
||||
expect(page).to have_content 'HTML'
|
||||
expect(page).to have_content 'AsciiDoc'
|
||||
expect(page).to have_css('.js-syntax-highlight', count: 3)
|
||||
expect(page).to have_css('.highlight', count: 3)
|
||||
expect(page).to have_xpath("//img[@alt='coverage report']")
|
||||
|
||||
page.within('.js-syntax-highlight', match: :first) do
|
||||
page.within('.highlight', match: :first) do
|
||||
expect(page).to have_content 'badges/master/coverage.svg'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,203 @@
|
|||
import * as utils from '~/diffs/components/diff_row_utils';
|
||||
import {
|
||||
MATCH_LINE_TYPE,
|
||||
CONTEXT_LINE_TYPE,
|
||||
OLD_NO_NEW_LINE_TYPE,
|
||||
NEW_NO_NEW_LINE_TYPE,
|
||||
EMPTY_CELL_TYPE,
|
||||
} from '~/diffs/constants';
|
||||
|
||||
const LINE_CODE = 'abc123';
|
||||
|
||||
describe('isHighlighted', () => {
|
||||
it('should return true if line is highlighted', () => {
|
||||
const state = { diffs: { highlightedRow: LINE_CODE } };
|
||||
const line = { line_code: LINE_CODE };
|
||||
const isCommented = false;
|
||||
expect(utils.isHighlighted(state, line, isCommented)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if line is not highlighted', () => {
|
||||
const state = { diffs: { highlightedRow: 'xxx' } };
|
||||
const line = { line_code: LINE_CODE };
|
||||
const isCommented = false;
|
||||
expect(utils.isHighlighted(state, line, isCommented)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if isCommented is true', () => {
|
||||
const state = { diffs: { highlightedRow: 'xxx' } };
|
||||
const line = { line_code: LINE_CODE };
|
||||
const isCommented = true;
|
||||
expect(utils.isHighlighted(state, line, isCommented)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isContextLine', () => {
|
||||
it('return true if line type is context', () => {
|
||||
expect(utils.isContextLine(CONTEXT_LINE_TYPE)).toBe(true);
|
||||
});
|
||||
|
||||
it('return false if line type is not context', () => {
|
||||
expect(utils.isContextLine('xxx')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isMatchLine', () => {
|
||||
it('return true if line type is match', () => {
|
||||
expect(utils.isMatchLine(MATCH_LINE_TYPE)).toBe(true);
|
||||
});
|
||||
|
||||
it('return false if line type is not match', () => {
|
||||
expect(utils.isMatchLine('xxx')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isMetaLine', () => {
|
||||
it.each`
|
||||
type | expectation
|
||||
${OLD_NO_NEW_LINE_TYPE} | ${true}
|
||||
${NEW_NO_NEW_LINE_TYPE} | ${true}
|
||||
${EMPTY_CELL_TYPE} | ${true}
|
||||
${'xxx'} | ${false}
|
||||
`('should return $expectation if type is $type', ({ type, expectation }) => {
|
||||
expect(utils.isMetaLine(type)).toBe(expectation);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldRenderCommentButton', () => {
|
||||
it('should return false if comment button is not rendered', () => {
|
||||
expect(utils.shouldRenderCommentButton(true, false)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if not logged in', () => {
|
||||
expect(utils.shouldRenderCommentButton(false, true)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true logged in and rendered', () => {
|
||||
expect(utils.shouldRenderCommentButton(true, true)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasDiscussions', () => {
|
||||
it('should return false if line is undefined', () => {
|
||||
expect(utils.hasDiscussions()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if discussions is undefined', () => {
|
||||
expect(utils.hasDiscussions({})).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if discussions has legnth of 0', () => {
|
||||
expect(utils.hasDiscussions({ discussions: [] })).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if discussions has legnth > 0', () => {
|
||||
expect(utils.hasDiscussions({ discussions: [1] })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('lineHref', () => {
|
||||
it(`should return #${LINE_CODE}`, () => {
|
||||
expect(utils.lineHref({ line_code: LINE_CODE })).toEqual(`#${LINE_CODE}`);
|
||||
});
|
||||
|
||||
it(`should return '#' if line is undefined`, () => {
|
||||
expect(utils.lineHref()).toEqual('#');
|
||||
});
|
||||
|
||||
it(`should return '#' if line_code is undefined`, () => {
|
||||
expect(utils.lineHref({})).toEqual('#');
|
||||
});
|
||||
});
|
||||
|
||||
describe('lineCode', () => {
|
||||
it(`should return undefined if line_code is undefined`, () => {
|
||||
expect(utils.lineCode()).toEqual(undefined);
|
||||
expect(utils.lineCode({ left: {} })).toEqual(undefined);
|
||||
expect(utils.lineCode({ right: {} })).toEqual(undefined);
|
||||
});
|
||||
|
||||
it(`should return ${LINE_CODE}`, () => {
|
||||
expect(utils.lineCode({ line_code: LINE_CODE })).toEqual(LINE_CODE);
|
||||
expect(utils.lineCode({ left: { line_code: LINE_CODE } })).toEqual(LINE_CODE);
|
||||
expect(utils.lineCode({ right: { line_code: LINE_CODE } })).toEqual(LINE_CODE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('classNameMapCell', () => {
|
||||
it.each`
|
||||
line | hll | loggedIn | hovered | expectation
|
||||
${undefined} | ${true} | ${true} | ${true} | ${[]}
|
||||
${{ type: 'new' }} | ${false} | ${false} | ${false} | ${['new', { hll: false, 'is-over': false }]}
|
||||
${{ type: 'new' }} | ${true} | ${true} | ${false} | ${['new', { hll: true, 'is-over': false }]}
|
||||
${{ type: 'new' }} | ${true} | ${false} | ${true} | ${['new', { hll: true, 'is-over': false }]}
|
||||
${{ type: 'new' }} | ${true} | ${true} | ${true} | ${['new', { hll: true, 'is-over': true }]}
|
||||
`('should return $expectation', ({ line, hll, loggedIn, hovered, expectation }) => {
|
||||
const classes = utils.classNameMapCell(line, hll, loggedIn, hovered);
|
||||
expect(classes).toEqual(expectation);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addCommentTooltip', () => {
|
||||
const brokenSymLinkTooltip =
|
||||
'Commenting on symbolic links that replace or are replaced by files is currently not supported.';
|
||||
const brokenRealTooltip =
|
||||
'Commenting on files that replace or are replaced by symbolic links is currently not supported.';
|
||||
it('should return default tooltip', () => {
|
||||
expect(utils.addCommentTooltip()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return broken symlink tooltip', () => {
|
||||
expect(utils.addCommentTooltip({ commentsDisabled: { wasSymbolic: true } })).toEqual(
|
||||
brokenSymLinkTooltip,
|
||||
);
|
||||
expect(utils.addCommentTooltip({ commentsDisabled: { isSymbolic: true } })).toEqual(
|
||||
brokenSymLinkTooltip,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return broken real tooltip', () => {
|
||||
expect(utils.addCommentTooltip({ commentsDisabled: { wasReal: true } })).toEqual(
|
||||
brokenRealTooltip,
|
||||
);
|
||||
expect(utils.addCommentTooltip({ commentsDisabled: { isReal: true } })).toEqual(
|
||||
brokenRealTooltip,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parallelViewLeftLineType', () => {
|
||||
it(`should return ${OLD_NO_NEW_LINE_TYPE}`, () => {
|
||||
expect(utils.parallelViewLeftLineType({ right: { type: NEW_NO_NEW_LINE_TYPE } })).toEqual(
|
||||
OLD_NO_NEW_LINE_TYPE,
|
||||
);
|
||||
});
|
||||
|
||||
it(`should return 'new'`, () => {
|
||||
expect(utils.parallelViewLeftLineType({ left: { type: 'new' } })).toContain('new');
|
||||
});
|
||||
|
||||
it(`should return ${EMPTY_CELL_TYPE}`, () => {
|
||||
expect(utils.parallelViewLeftLineType({})).toContain(EMPTY_CELL_TYPE);
|
||||
});
|
||||
|
||||
it(`should return hll:true`, () => {
|
||||
expect(utils.parallelViewLeftLineType({}, true)[1]).toEqual({ hll: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldShowCommentButton', () => {
|
||||
it.each`
|
||||
hover | context | meta | discussions | expectation
|
||||
${true} | ${false} | ${false} | ${false} | ${true}
|
||||
${false} | ${false} | ${false} | ${false} | ${false}
|
||||
${true} | ${true} | ${false} | ${false} | ${false}
|
||||
${true} | ${true} | ${true} | ${false} | ${false}
|
||||
${true} | ${true} | ${true} | ${true} | ${false}
|
||||
`(
|
||||
'should return $expectation when hover is $hover',
|
||||
({ hover, context, meta, discussions, expectation }) => {
|
||||
expect(utils.shouldShowCommentButton(hover, context, meta, discussions)).toBe(expectation);
|
||||
},
|
||||
);
|
||||
});
|
|
@ -1,279 +0,0 @@
|
|||
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import DiffTableCell from '~/diffs/components/diff_table_cell.vue';
|
||||
import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
|
||||
import { LINE_POSITION_RIGHT } from '~/diffs/constants';
|
||||
import { createStore } from '~/mr_notes/stores';
|
||||
import discussionsMockData from '../mock_data/diff_discussions';
|
||||
import diffFileMockData from '../mock_data/diff_file';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
const TEST_USER_ID = 'abc123';
|
||||
const TEST_USER = { id: TEST_USER_ID };
|
||||
const TEST_LINE_NUMBER = 1;
|
||||
const TEST_LINE_CODE = 'LC_42';
|
||||
const TEST_FILE_HASH = diffFileMockData.file_hash;
|
||||
|
||||
describe('DiffTableCell', () => {
|
||||
const symlinkishFileTooltip =
|
||||
'Commenting on symbolic links that replace or are replaced by files is currently not supported.';
|
||||
const realishFileTooltip =
|
||||
'Commenting on files that replace or are replaced by symbolic links is currently not supported.';
|
||||
const otherFileTooltip = 'Add a comment to this line';
|
||||
|
||||
let wrapper;
|
||||
let line;
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
store.state.notes.userData = TEST_USER;
|
||||
|
||||
line = {
|
||||
line_code: TEST_LINE_CODE,
|
||||
type: 'new',
|
||||
old_line: null,
|
||||
new_line: 1,
|
||||
discussions: [{ ...discussionsMockData }],
|
||||
discussionsExpanded: true,
|
||||
text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
|
||||
rich_text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
|
||||
meta_data: null,
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
const setWindowLocation = value => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
writable: true,
|
||||
value,
|
||||
});
|
||||
};
|
||||
|
||||
const createComponent = (props = {}) => {
|
||||
wrapper = shallowMount(DiffTableCell, {
|
||||
localVue,
|
||||
store,
|
||||
propsData: {
|
||||
line,
|
||||
fileHash: TEST_FILE_HASH,
|
||||
contextLinesPath: '/context/lines/path',
|
||||
isHighlighted: false,
|
||||
...props,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findTd = () => wrapper.find({ ref: 'td' });
|
||||
const findNoteButton = () => wrapper.find({ ref: 'addDiffNoteButton' });
|
||||
const findLineNumber = () => wrapper.find({ ref: 'lineNumberRef' });
|
||||
const findTooltip = () => wrapper.find({ ref: 'addNoteTooltip' });
|
||||
const findAvatars = () => wrapper.find(DiffGutterAvatars);
|
||||
|
||||
describe('td', () => {
|
||||
it('highlights when isHighlighted true', () => {
|
||||
createComponent({ isHighlighted: true });
|
||||
|
||||
expect(findTd().classes()).toContain('hll');
|
||||
});
|
||||
|
||||
it('does not highlight when isHighlighted false', () => {
|
||||
createComponent({ isHighlighted: false });
|
||||
|
||||
expect(findTd().classes()).not.toContain('hll');
|
||||
});
|
||||
});
|
||||
|
||||
describe('comment button', () => {
|
||||
it.each`
|
||||
showCommentButton | userData | query | mergeRefHeadComments | expectation
|
||||
${true} | ${TEST_USER} | ${'diff_head=false'} | ${false} | ${true}
|
||||
${true} | ${TEST_USER} | ${'diff_head=true'} | ${true} | ${true}
|
||||
${true} | ${TEST_USER} | ${'diff_head=true'} | ${false} | ${false}
|
||||
${false} | ${TEST_USER} | ${'diff_head=true'} | ${true} | ${false}
|
||||
${false} | ${TEST_USER} | ${'bogus'} | ${true} | ${false}
|
||||
${true} | ${null} | ${''} | ${true} | ${false}
|
||||
`(
|
||||
'exists is $expectation - with showCommentButton ($showCommentButton) userData ($userData) query ($query)',
|
||||
({ showCommentButton, userData, query, mergeRefHeadComments, expectation }) => {
|
||||
store.state.notes.userData = userData;
|
||||
gon.features = { mergeRefHeadComments };
|
||||
setWindowLocation({ href: `${TEST_HOST}?${query}` });
|
||||
createComponent({ showCommentButton });
|
||||
|
||||
wrapper.setData({ isCommentButtonRendered: showCommentButton });
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findNoteButton().exists()).toBe(expectation);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each`
|
||||
isHover | otherProps | discussions | expectation
|
||||
${true} | ${{}} | ${[]} | ${true}
|
||||
${false} | ${{}} | ${[]} | ${false}
|
||||
${true} | ${{ line: { ...line, type: 'context' } }} | ${[]} | ${false}
|
||||
${true} | ${{ line: { ...line, type: 'old-nonewline' } }} | ${[]} | ${false}
|
||||
${true} | ${{}} | ${[{}]} | ${false}
|
||||
`(
|
||||
'visible is $expectation - with isHover ($isHover), discussions ($discussions), otherProps ($otherProps)',
|
||||
({ isHover, otherProps, discussions, expectation }) => {
|
||||
line.discussions = discussions;
|
||||
createComponent({
|
||||
showCommentButton: true,
|
||||
isHover,
|
||||
...otherProps,
|
||||
});
|
||||
|
||||
wrapper.setData({
|
||||
isCommentButtonRendered: true,
|
||||
});
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findNoteButton().isVisible()).toBe(expectation);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each`
|
||||
disabled | commentsDisabled
|
||||
${'disabled'} | ${true}
|
||||
${undefined} | ${false}
|
||||
`(
|
||||
'has attribute disabled=$disabled when the outer component has prop commentsDisabled=$commentsDisabled',
|
||||
({ disabled, commentsDisabled }) => {
|
||||
line.commentsDisabled = commentsDisabled;
|
||||
|
||||
createComponent({
|
||||
showCommentButton: true,
|
||||
isHover: true,
|
||||
});
|
||||
|
||||
wrapper.setData({ isCommentButtonRendered: true });
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findNoteButton().attributes('disabled')).toBe(disabled);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each`
|
||||
tooltip | commentsDisabled
|
||||
${symlinkishFileTooltip} | ${{ wasSymbolic: true }}
|
||||
${symlinkishFileTooltip} | ${{ isSymbolic: true }}
|
||||
${realishFileTooltip} | ${{ wasReal: true }}
|
||||
${realishFileTooltip} | ${{ isReal: true }}
|
||||
${otherFileTooltip} | ${false}
|
||||
`(
|
||||
'has the correct tooltip when commentsDisabled=$commentsDisabled',
|
||||
({ tooltip, commentsDisabled }) => {
|
||||
line.commentsDisabled = commentsDisabled;
|
||||
|
||||
createComponent({
|
||||
showCommentButton: true,
|
||||
isHover: true,
|
||||
});
|
||||
|
||||
wrapper.setData({ isCommentButtonRendered: true });
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findTooltip().attributes('title')).toBe(tooltip);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('line number', () => {
|
||||
describe('without lineNumber prop', () => {
|
||||
it('does not render', () => {
|
||||
createComponent({ lineType: 'old' });
|
||||
|
||||
expect(findLineNumber().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with lineNumber prop', () => {
|
||||
describe.each`
|
||||
lineProps | expectedHref | expectedClickArg
|
||||
${{ line_code: TEST_LINE_CODE }} | ${`#${TEST_LINE_CODE}`} | ${TEST_LINE_CODE}
|
||||
${{ line_code: undefined }} | ${'#'} | ${undefined}
|
||||
${{ line_code: undefined, left: { line_code: TEST_LINE_CODE } }} | ${'#'} | ${TEST_LINE_CODE}
|
||||
${{ line_code: undefined, right: { line_code: TEST_LINE_CODE } }} | ${'#'} | ${TEST_LINE_CODE}
|
||||
`('with line ($lineProps)', ({ lineProps, expectedHref, expectedClickArg }) => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(store, 'dispatch').mockImplementation();
|
||||
Object.assign(line, lineProps);
|
||||
createComponent({ lineNumber: TEST_LINE_NUMBER });
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
expect(findLineNumber().exists()).toBe(true);
|
||||
expect(findLineNumber().attributes()).toEqual({
|
||||
href: expectedHref,
|
||||
'data-linenumber': TEST_LINE_NUMBER.toString(),
|
||||
});
|
||||
});
|
||||
|
||||
it('on click, dispatches setHighlightedRow', () => {
|
||||
expect(store.dispatch).not.toHaveBeenCalled();
|
||||
|
||||
findLineNumber().trigger('click');
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith('diffs/setHighlightedRow', expectedClickArg);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('diff-gutter-avatars', () => {
|
||||
describe('with showCommentButton', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(store, 'dispatch').mockImplementation();
|
||||
|
||||
createComponent({ showCommentButton: true });
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
expect(findAvatars().props()).toEqual({
|
||||
discussions: line.discussions,
|
||||
discussionsExpanded: line.discussionsExpanded,
|
||||
});
|
||||
});
|
||||
|
||||
it('toggles line discussion', () => {
|
||||
expect(store.dispatch).not.toHaveBeenCalled();
|
||||
|
||||
findAvatars().vm.$emit('toggleLineDiscussions');
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith('diffs/toggleLineDiscussions', {
|
||||
lineCode: TEST_LINE_CODE,
|
||||
fileHash: TEST_FILE_HASH,
|
||||
expanded: !line.discussionsExpanded,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it.each`
|
||||
props | lineProps | expectation
|
||||
${{ showCommentButton: true }} | ${{}} | ${true}
|
||||
${{ showCommentButton: false }} | ${{}} | ${false}
|
||||
${{ showCommentButton: true, linePosition: LINE_POSITION_RIGHT }} | ${{ type: null }} | ${false}
|
||||
${{ showCommentButton: true }} | ${{ discussions: [] }} | ${false}
|
||||
`(
|
||||
'exists is $expectation - with props ($props), line ($lineProps)',
|
||||
({ props, lineProps, expectation }) => {
|
||||
Object.assign(line, lineProps);
|
||||
createComponent(props);
|
||||
|
||||
expect(findAvatars().exists()).toBe(expectation);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,11 +1,11 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
|
||||
import StateFilter from '~/search/state_filter/components/state_filter.vue';
|
||||
import DropdownFilter from '~/search/components/dropdown_filter.vue';
|
||||
import {
|
||||
FILTER_STATES,
|
||||
SCOPES,
|
||||
FILTER_STATES_BY_SCOPE,
|
||||
FILTER_TEXT,
|
||||
FILTER_HEADER,
|
||||
SCOPES,
|
||||
} from '~/search/state_filter/constants';
|
||||
import * as urlUtils from '~/lib/utils/url_utility';
|
||||
|
||||
|
@ -15,14 +15,19 @@ jest.mock('~/lib/utils/url_utility', () => ({
|
|||
}));
|
||||
|
||||
function createComponent(props = { scope: 'issues' }) {
|
||||
return shallowMount(StateFilter, {
|
||||
return shallowMount(DropdownFilter, {
|
||||
propsData: {
|
||||
filtersArray: FILTER_STATES_BY_SCOPE.issues,
|
||||
filters: FILTER_STATES,
|
||||
header: FILTER_HEADER,
|
||||
param: 'state',
|
||||
supportedScopes: Object.values(SCOPES),
|
||||
...props,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe('StateFilter', () => {
|
||||
describe('DropdownFilter', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -41,7 +46,7 @@ describe('StateFilter', () => {
|
|||
|
||||
describe('template', () => {
|
||||
describe.each`
|
||||
scope | showStateDropdown
|
||||
scope | showDropdown
|
||||
${'issues'} | ${true}
|
||||
${'merge_requests'} | ${true}
|
||||
${'projects'} | ${false}
|
||||
|
@ -50,26 +55,25 @@ describe('StateFilter', () => {
|
|||
${'notes'} | ${false}
|
||||
${'wiki_blobs'} | ${false}
|
||||
${'blobs'} | ${false}
|
||||
`(`state dropdown`, ({ scope, showStateDropdown }) => {
|
||||
`(`dropdown`, ({ scope, showDropdown }) => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent({ scope });
|
||||
});
|
||||
|
||||
it(`does${showStateDropdown ? '' : ' not'} render when scope is ${scope}`, () => {
|
||||
expect(findGlDropdown().exists()).toBe(showStateDropdown);
|
||||
it(`does${showDropdown ? '' : ' not'} render when scope is ${scope}`, () => {
|
||||
expect(findGlDropdown().exists()).toBe(showDropdown);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
state | label
|
||||
${FILTER_STATES.ANY.value} | ${FILTER_TEXT}
|
||||
initialFilter | label
|
||||
${FILTER_STATES.ANY.value} | ${`Any ${FILTER_HEADER}`}
|
||||
${FILTER_STATES.OPEN.value} | ${FILTER_STATES.OPEN.label}
|
||||
${FILTER_STATES.CLOSED.value} | ${FILTER_STATES.CLOSED.label}
|
||||
${FILTER_STATES.MERGED.value} | ${FILTER_STATES.MERGED.label}
|
||||
`(`filter text`, ({ state, label }) => {
|
||||
describe(`when state is ${state}`, () => {
|
||||
`(`filter text`, ({ initialFilter, label }) => {
|
||||
describe(`when initialFilter is ${initialFilter}`, () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent({ scope: 'issues', state });
|
||||
wrapper = createComponent({ scope: 'issues', initialFilter });
|
||||
});
|
||||
|
||||
it(`sets dropdown label to ${label}`, () => {
|
|
@ -5,6 +5,16 @@ require 'spec_helper'
|
|||
RSpec.describe BlobHelper do
|
||||
include TreeHelper
|
||||
|
||||
describe '#highlight' do
|
||||
it 'wraps highlighted content' do
|
||||
expect(helper.highlight('test.rb', '52')).to eq(%q[<pre class="code highlight"><code><span id="LC1" class="line" lang="ruby"><span class="mi">52</span></span></code></pre>])
|
||||
end
|
||||
|
||||
it 'handles plain version' do
|
||||
expect(helper.highlight('test.rb', '52', plain: true)).to eq(%q[<pre class="code highlight"><code><span id="LC1" class="line" lang="">52</span></code></pre>])
|
||||
end
|
||||
end
|
||||
|
||||
describe "#sanitize_svg_data" do
|
||||
let(:input_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'unsanitized.svg') }
|
||||
let(:data) { File.read(input_svg_path) }
|
||||
|
|
|
@ -5,6 +5,8 @@ require 'spec_helper'
|
|||
RSpec.describe Banzai::Filter::ExternalIssueReferenceFilter do
|
||||
include FilterSpecHelper
|
||||
|
||||
let_it_be_with_refind(:project) { create(:project) }
|
||||
|
||||
shared_examples_for "external issue tracker" do
|
||||
it_behaves_like 'a reference containing an element node'
|
||||
|
||||
|
@ -116,7 +118,7 @@ RSpec.describe Banzai::Filter::ExternalIssueReferenceFilter do
|
|||
end
|
||||
|
||||
context "redmine project" do
|
||||
let(:project) { create(:redmine_project) }
|
||||
let_it_be(:service) { create(:redmine_service, project: project) }
|
||||
|
||||
before do
|
||||
project.update!(issues_enabled: false)
|
||||
|
@ -138,7 +140,7 @@ RSpec.describe Banzai::Filter::ExternalIssueReferenceFilter do
|
|||
end
|
||||
|
||||
context "youtrack project" do
|
||||
let(:project) { create(:youtrack_project) }
|
||||
let_it_be(:service) { create(:youtrack_service, project: project) }
|
||||
|
||||
before do
|
||||
project.update!(issues_enabled: false)
|
||||
|
@ -181,7 +183,7 @@ RSpec.describe Banzai::Filter::ExternalIssueReferenceFilter do
|
|||
end
|
||||
|
||||
context "jira project" do
|
||||
let(:project) { create(:jira_project) }
|
||||
let_it_be(:service) { create(:jira_service, project: project) }
|
||||
let(:reference) { issue.to_reference }
|
||||
|
||||
context "with right markdown" do
|
||||
|
@ -210,7 +212,7 @@ RSpec.describe Banzai::Filter::ExternalIssueReferenceFilter do
|
|||
end
|
||||
|
||||
context "ewm project" do
|
||||
let_it_be(:project) { create(:ewm_project) }
|
||||
let_it_be(:service) { create(:ewm_service, project: project) }
|
||||
|
||||
before do
|
||||
project.update!(issues_enabled: false)
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Checks::MatchingMergeRequest do
|
||||
describe '#match?' do
|
||||
let_it_be(:newrev) { '012345678' }
|
||||
let_it_be(:target_branch) { 'feature' }
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:locked_merge_request) do
|
||||
create(:merge_request,
|
||||
:locked,
|
||||
source_project: project,
|
||||
target_project: project,
|
||||
target_branch: target_branch,
|
||||
in_progress_merge_commit_sha: newrev)
|
||||
end
|
||||
|
||||
subject { described_class.new(newrev, target_branch, project) }
|
||||
|
||||
it 'matches a merge request' do
|
||||
expect(subject.match?).to be true
|
||||
end
|
||||
|
||||
it 'does not match any merge request' do
|
||||
matcher = described_class.new(newrev, 'test', project)
|
||||
|
||||
expect(matcher.match?).to be false
|
||||
end
|
||||
end
|
||||
end
|
|
@ -58,25 +58,6 @@ RSpec.describe Gitlab::SearchResults do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#highlight_map' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:scope, :expected) do
|
||||
'projects' | {}
|
||||
'issues' | {}
|
||||
'merge_requests' | {}
|
||||
'milestones' | {}
|
||||
'users' | {}
|
||||
'unknown' | {}
|
||||
end
|
||||
|
||||
with_them do
|
||||
it 'returns the expected highlight_map' do
|
||||
expect(results.highlight_map(scope)).to eq(expected)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#formatted_limited_count' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
|
|
|
@ -21,12 +21,6 @@ RSpec.describe Gitlab::SnippetSearchResults do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#highlight_map' do
|
||||
it 'returns the expected highlight map' do
|
||||
expect(results.highlight_map('snippet_titles')).to eq({})
|
||||
end
|
||||
end
|
||||
|
||||
describe '#objects' do
|
||||
it 'uses page and per_page to paginate results' do
|
||||
snippet2 = create(:snippet, :public, content: 'foo', file_name: 'foo')
|
||||
|
|
|
@ -499,6 +499,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
|
|||
|
||||
expect(count_data[:projects_with_packages]).to eq(2)
|
||||
expect(count_data[:packages]).to eq(4)
|
||||
expect(count_data[:user_preferences_user_gitpod_enabled]).to eq(1)
|
||||
end
|
||||
|
||||
it 'gathers object store usage correctly' do
|
||||
|
|
|
@ -4299,4 +4299,18 @@ RSpec.describe MergeRequest, factory_default: :keep do
|
|||
expect(merge_request.allows_reviewers?).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#update_and_mark_in_progress_merge_commit_sha' do
|
||||
let(:ref) { subject.target_project.repository.commit.id }
|
||||
|
||||
before do
|
||||
expect(subject.target_project).to receive(:mark_primary_write_location)
|
||||
end
|
||||
|
||||
it 'updates commit ID' do
|
||||
expect { subject.update_and_mark_in_progress_merge_commit_sha(ref) }
|
||||
.to change { subject.in_progress_merge_commit_sha }
|
||||
.from(nil).to(ref)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -72,15 +72,22 @@ RSpec.describe MergeRequests::FfMergeService do
|
|||
end
|
||||
|
||||
it 'does not update squash_commit_sha if it is not a squash' do
|
||||
expect(merge_request).to receive(:update_and_mark_in_progress_merge_commit_sha).twice.and_call_original
|
||||
|
||||
expect { execute_ff_merge }.not_to change { merge_request.squash_commit_sha }
|
||||
expect(merge_request.in_progress_merge_commit_sha).to be_nil
|
||||
end
|
||||
|
||||
it 'updates squash_commit_sha if it is a squash' do
|
||||
expect(merge_request).to receive(:update_and_mark_in_progress_merge_commit_sha).twice.and_call_original
|
||||
|
||||
merge_request.update!(squash: true)
|
||||
|
||||
expect { execute_ff_merge }
|
||||
.to change { merge_request.squash_commit_sha }
|
||||
.from(nil)
|
||||
|
||||
expect(merge_request.in_progress_merge_commit_sha).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ RSpec.describe MergeRequests::MergeService do
|
|||
context 'valid params' do
|
||||
before do
|
||||
allow(service).to receive(:execute_hooks)
|
||||
expect(merge_request).to receive(:update_and_mark_in_progress_merge_commit_sha).twice.and_call_original
|
||||
|
||||
perform_enqueued_jobs do
|
||||
service.execute(merge_request)
|
||||
|
|
|
@ -133,6 +133,7 @@ module UsageDataHelpers
|
|||
todos
|
||||
uploads
|
||||
web_hooks
|
||||
user_preferences_user_gitpod_enabled
|
||||
).push(*SMAU_KEYS)
|
||||
|
||||
USAGE_DATA_KEYS = %i(
|
||||
|
|
|
@ -65,7 +65,7 @@ RSpec.shared_examples 'Notes user references' do
|
|||
|
||||
include_examples 'sets the note author to the mapped user'
|
||||
|
||||
include_examples 'adds original autor note'
|
||||
include_examples 'does not add original autor note'
|
||||
end
|
||||
|
||||
context 'and the note author exists in the target instance' do
|
||||
|
|
|
@ -60,6 +60,28 @@ RSpec.describe 'search/_results' do
|
|||
|
||||
expect(rendered).to have_selector('#js-search-filter-by-state')
|
||||
end
|
||||
|
||||
context 'Feature search_filter_by_confidential' do
|
||||
context 'when disabled' do
|
||||
before do
|
||||
stub_feature_flags(search_filter_by_confidential: false)
|
||||
end
|
||||
|
||||
it 'does not render the confidential drop down' do
|
||||
render
|
||||
|
||||
expect(rendered).not_to have_selector('#js-search-filter-by-confidential')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when enabled' do
|
||||
it 'renders the confidential drop down' do
|
||||
render
|
||||
|
||||
expect(rendered).to have_selector('#js-search-filter-by-confidential')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue