Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-09-23 12:09:58 +00:00
parent 8f2b51af41
commit a071c2888d
85 changed files with 982 additions and 977 deletions

View File

@ -885,10 +885,10 @@ GEM
bundler (>= 1.3.0) bundler (>= 1.3.0)
railties (= 6.0.3.1) railties (= 6.0.3.1)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.4) rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.x) actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.x) actionview (>= 5.0.1.rc1)
activesupport (>= 5.0.1.x) activesupport (>= 5.0.1.rc1)
rails-dom-testing (2.0.3) rails-dom-testing (2.0.3)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
nokogiri (>= 1.6) nokogiri (>= 1.6)

View File

@ -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;
};

View File

@ -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>

View File

@ -1,22 +1,9 @@
<script> <script>
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { import { CONTEXT_LINE_CLASS_NAME } from '../constants';
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 DiffGutterAvatars from './diff_gutter_avatars.vue'; import DiffGutterAvatars from './diff_gutter_avatars.vue';
import * as utils from './diff_row_utils';
export default { export default {
components: { components: {
@ -61,14 +48,11 @@ export default {
...mapGetters('diffs', ['fileLineCoverage']), ...mapGetters('diffs', ['fileLineCoverage']),
...mapState({ ...mapState({
isHighlighted(state) { isHighlighted(state) {
if (this.isCommented) return true; return utils.isHighlighted(state, this.line, this.isCommented);
const lineCode = this.line.line_code;
return lineCode ? lineCode === state.diffs.highlightedRow : false;
}, },
}), }),
isContextLine() { isContextLine() {
return this.line.type === CONTEXT_LINE_TYPE; return utils.isContextLine(this.line.type);
}, },
classNameMap() { classNameMap() {
return [ return [
@ -82,82 +66,48 @@ export default {
return this.line.line_code || `${this.fileHash}_${this.line.old_line}_${this.line.new_line}`; return this.line.line_code || `${this.fileHash}_${this.line.old_line}_${this.line.new_line}`;
}, },
isMatchLine() { isMatchLine() {
return this.line.type === MATCH_LINE_TYPE; return utils.isMatchLine(this.line.type);
}, },
coverageState() { coverageState() {
return this.fileLineCoverage(this.filePath, this.line.new_line); return this.fileLineCoverage(this.filePath, this.line.new_line);
}, },
isMetaLine() { isMetaLine() {
const { type } = this.line; return utils.isMetaLine(this.line.type);
return (
type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE
);
}, },
classNameMapCell() { classNameMapCell() {
const { type } = this.line; return utils.classNameMapCell(this.line, this.isHighlighted, this.isLoggedIn, this.isHover);
return [
type,
{
hll: this.isHighlighted,
[LINE_HOVER_CLASS_NAME]:
this.isLoggedIn && this.isHover && !this.isContextLine && !this.isMetaLine,
},
];
}, },
addCommentTooltip() { addCommentTooltip() {
const brokenSymlinks = this.line.commentsDisabled; return utils.addCommentTooltip(this.line);
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;
}, },
shouldRenderCommentButton() { shouldRenderCommentButton() {
if (this.isLoggedIn) { return utils.shouldRenderCommentButton(
const isDiffHead = parseBoolean(getParameterByName('diff_head')); this.isLoggedIn,
return !isDiffHead || gon.features?.mergeRefHeadComments; true,
} gon.features?.mergeRefHeadComments,
);
return false;
}, },
shouldShowCommentButton() { shouldShowCommentButton() {
return this.isHover && !this.isContextLine && !this.isMetaLine && !this.hasDiscussions; return utils.shouldShowCommentButton(
this.isHover,
this.isContextLine,
this.isMetaLine,
this.hasDiscussions,
);
}, },
hasDiscussions() { hasDiscussions() {
return this.line.discussions && this.line.discussions.length > 0; return utils.hasDiscussions(this.line);
}, },
lineHref() { lineHref() {
return `#${this.line.line_code || ''}`; return utils.lineHref(this.line);
}, },
lineCode() { lineCode() {
return ( return utils.lineCode(this.line);
this.line.line_code ||
(this.line.left && this.line.left.line_code) ||
(this.line.right && this.line.right.line_code)
);
}, },
shouldShowAvatarsOnGutter() { shouldShowAvatarsOnGutter() {
return this.hasDiscussions; 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() { mounted() {
this.scrollToLineIfNeededInline(this.line); this.scrollToLineIfNeededInline(this.line);
}, },
@ -242,6 +192,7 @@ export default {
class="line-coverage" class="line-coverage"
></td> ></td>
<td <td
:key="line.line_code"
v-safe-html="line.rich_text" v-safe-html="line.rich_text"
:class="[ :class="[
line.type, line.type,

View File

@ -2,21 +2,9 @@
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import $ from 'jquery'; import $ from 'jquery';
import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { import { CONTEXT_LINE_CLASS_NAME, PARALLEL_DIFF_VIEW_TYPE } from '../constants';
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 DiffGutterAvatars from './diff_gutter_avatars.vue'; import DiffGutterAvatars from './diff_gutter_avatars.vue';
import * as utils from './diff_row_utils';
export default { export default {
components: { components: {
@ -63,20 +51,15 @@ export default {
...mapGetters(['isLoggedIn']), ...mapGetters(['isLoggedIn']),
...mapState({ ...mapState({
isHighlighted(state) { isHighlighted(state) {
if (this.isCommented) return true; const line = this.line.left?.line_code ? this.line.left : this.line.right;
return utils.isHighlighted(state, line, this.isCommented);
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;
}, },
}), }),
isContextLineLeft() { isContextLineLeft() {
return this.line.left && this.line.left.type === CONTEXT_LINE_TYPE; return utils.isContextLine(this.line.left?.type);
}, },
isContextLineRight() { isContextLineRight() {
return this.line.right && this.line.right.type === CONTEXT_LINE_TYPE; return utils.isContextLine(this.line.right?.type);
}, },
classNameMap() { classNameMap() {
return { return {
@ -85,157 +68,84 @@ export default {
}; };
}, },
parallelViewLeftLineType() { parallelViewLeftLineType() {
if (this.line.right && this.line.right.type === NEW_NO_NEW_LINE_TYPE) { return utils.parallelViewLeftLineType(this.line, this.isHighlighted);
return OLD_NO_NEW_LINE_TYPE;
}
const lineTypeClass = this.line.left ? this.line.left.type : EMPTY_CELL_TYPE;
return [
lineTypeClass,
{
hll: this.isHighlighted,
},
];
}, },
isMatchLineLeft() { isMatchLineLeft() {
return this.line.left && this.line.left.type === MATCH_LINE_TYPE; return utils.isMatchLine(this.line.left?.type);
}, },
isMatchLineRight() { isMatchLineRight() {
return this.line.right && this.line.right.type === MATCH_LINE_TYPE; return utils.isMatchLine(this.line.right?.type);
}, },
coverageState() { coverageState() {
return this.fileLineCoverage(this.filePath, this.line.right.new_line); return this.fileLineCoverage(this.filePath, this.line.right.new_line);
}, },
classNameMapCellLeft() { classNameMapCellLeft() {
const { type } = this.line.left; return utils.classNameMapCell(
this.line.left,
return [ this.isHighlighted,
type, this.isLoggedIn,
{ this.isLeftHover,
hll: this.isHighlighted, );
[LINE_HOVER_CLASS_NAME]:
this.isLoggedIn && this.isLeftHover && !this.isContextLineLeft && !this.isMetaLineLeft,
},
];
}, },
classNameMapCellRight() { classNameMapCellRight() {
const { type } = this.line.right; return utils.classNameMapCell(
this.line.right,
return [ this.isHighlighted,
type, this.isLoggedIn,
{ this.isRightHover,
hll: this.isHighlighted, );
[LINE_HOVER_CLASS_NAME]:
this.isLoggedIn &&
this.isRightHover &&
!this.isContextLineRight &&
!this.isMetaLineRight,
},
];
}, },
addCommentTooltipLeft() { addCommentTooltipLeft() {
const brokenSymlinks = this.line.left.commentsDisabled; return utils.addCommentTooltip(this.line.left);
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;
}, },
addCommentTooltipRight() { addCommentTooltipRight() {
const brokenSymlinks = this.line.right.commentsDisabled; return utils.addCommentTooltip(this.line.right);
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;
}, },
shouldRenderCommentButton() { shouldRenderCommentButton() {
if (!this.isCommentButtonRendered) { return utils.shouldRenderCommentButton(
return false; this.isLoggedIn,
} this.isCommentButtonRendered,
gon.features?.mergeRefHeadComments,
if (this.isLoggedIn) { );
const isDiffHead = parseBoolean(getParameterByName('diff_head'));
return !isDiffHead || gon.features?.mergeRefHeadComments;
}
return false;
}, },
shouldShowCommentButtonLeft() { shouldShowCommentButtonLeft() {
return ( return utils.shouldShowCommentButton(
this.isLeftHover && this.isLeftHover,
!this.isContextLineLeft && this.isContextLineLeft,
!this.isMetaLineLeft && this.isMetaLineLeft,
!this.hasDiscussionsLeft this.hasDiscussionsLeft,
); );
}, },
shouldShowCommentButtonRight() { shouldShowCommentButtonRight() {
return ( return utils.shouldShowCommentButton(
this.isRightHover && this.isRightHover,
!this.isContextLineRight && this.isContextLineRight,
!this.isMetaLineRight && this.isMetaLineRight,
!this.hasDiscussionsRight this.hasDiscussionsRight,
); );
}, },
hasDiscussionsLeft() { hasDiscussionsLeft() {
return this.line.left?.discussions?.length > 0; return utils.hasDiscussions(this.line.left);
}, },
hasDiscussionsRight() { hasDiscussionsRight() {
return this.line.right?.discussions?.length > 0; return utils.hasDiscussions(this.line.right);
}, },
lineHrefOld() { lineHrefOld() {
return `#${this.line.left.line_code || ''}`; return utils.lineHref(this.line.left);
}, },
lineHrefNew() { lineHrefNew() {
return `#${this.line.right.line_code || ''}`; return utils.lineHref(this.line.right);
}, },
lineCode() { lineCode() {
return ( return utils.lineCode(this.line);
(this.line.left && this.line.left.line_code) ||
(this.line.right && this.line.right.line_code)
);
}, },
isMetaLineLeft() { isMetaLineLeft() {
const type = this.line.left?.type; return utils.isMetaLine(this.line.left?.type);
return (
type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE
);
}, },
isMetaLineRight() { isMetaLineRight() {
const type = this.line.right?.type; return utils.isMetaLine(this.line.right?.type);
return (
type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE
);
}, },
}, },
created() {
this.newLineType = NEW_LINE_TYPE;
this.oldLineType = OLD_LINE_TYPE;
this.parallelDiffViewType = PARALLEL_DIFF_VIEW_TYPE;
},
mounted() { mounted() {
this.scrollToLineIfNeededParallel(this.line); this.scrollToLineIfNeededParallel(this.line);
this.unwatchShouldShowCommentButton = this.$watch( this.unwatchShouldShowCommentButton = this.$watch(
@ -341,6 +251,7 @@ export default {
<td :class="parallelViewLeftLineType" class="line-coverage left-side"></td> <td :class="parallelViewLeftLineType" class="line-coverage left-side"></td>
<td <td
:id="line.left.line_code" :id="line.left.line_code"
:key="line.left.line_code"
v-safe-html="line.left.rich_text" v-safe-html="line.left.rich_text"
:class="parallelViewLeftLineType" :class="parallelViewLeftLineType"
class="line_content with-coverage parallel left-side" class="line_content with-coverage parallel left-side"
@ -401,6 +312,7 @@ export default {
></td> ></td>
<td <td
:id="line.right.line_code" :id="line.right.line_code"
:key="line.right.rich_text"
v-safe-html="line.right.rich_text" v-safe-html="line.right.rich_text"
:class="[ :class="[
line.right.type, line.right.type,

View File

@ -1,7 +1,10 @@
import Search from './search'; import Search from './search';
import initStateFilter from '~/search/state_filter'; import initStateFilter from '~/search/state_filter';
import initConfidentialFilter from '~/search/confidential_filter';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initStateFilter(); initStateFilter();
initConfidentialFilter();
return new Search(); return new Search();
}); });

View File

@ -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>

View File

@ -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';

View File

@ -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),
},
});
},
});
};

View File

@ -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>

View File

@ -2,8 +2,6 @@ import { __ } from '~/locale';
export const FILTER_HEADER = __('Status'); export const FILTER_HEADER = __('Status');
export const FILTER_TEXT = __('Any Status');
export const FILTER_STATES = { export const FILTER_STATES = {
ANY: { ANY: {
label: __('Any'), label: __('Any'),
@ -37,3 +35,5 @@ export const FILTER_STATES_BY_SCOPE = {
FILTER_STATES.CLOSED, FILTER_STATES.CLOSED,
], ],
}; };
export const FILTER_PARAM = 'state';

View File

@ -1,6 +1,13 @@
import Vue from 'vue'; import Vue from 'vue';
import Translate from '~/vue_shared/translate'; 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); Vue.use(Translate);
@ -11,22 +18,20 @@ export default () => {
return new Vue({ return new Vue({
el, el,
components: {
StateFilter,
},
data() { data() {
const { dataset } = this.$options.el; return { ...el.dataset };
return {
scope: dataset.scope,
state: dataset.state,
};
}, },
render(createElement) { render(createElement) {
return createElement('state-filter', { return createElement(DropdownFilter, {
props: { props: {
initialFilter: this.filter,
filtersArray: FILTER_STATES_BY_SCOPE[this.scope],
filters: FILTER_STATES,
header: FILTER_HEADER,
param: FILTER_PARAM,
scope: this.scope, scope: this.scope,
state: this.state, supportedScopes: Object.values(SCOPES),
}, },
}); });
}, },

View File

@ -112,8 +112,7 @@ a {
} }
.dropdown-menu a, .dropdown-menu a,
.dropdown-menu button, .dropdown-menu button {
.dropdown-menu-nav a {
transition: none; transition: none;
} }

View File

@ -33,8 +33,7 @@
} }
.show.dropdown { .show.dropdown {
.dropdown-menu, .dropdown-menu {
.dropdown-menu-nav {
@include set-visible; @include set-visible;
min-height: $dropdown-min-height; min-height: $dropdown-min-height;
max-height: $dropdown-max-height; max-height: $dropdown-max-height;
@ -258,8 +257,7 @@
} }
} }
.dropdown-menu, .dropdown-menu {
.dropdown-menu-nav {
display: none; display: none;
position: absolute; position: absolute;
width: auto; width: auto;
@ -393,7 +391,13 @@
pointer-events: none; pointer-events: none;
} }
.dropdown-menu li { .dropdown-menu {
display: none;
opacity: 1;
visibility: visible;
transform: translateY(0);
li {
cursor: pointer; cursor: pointer;
&.droplab-item-active button { &.droplab-item-active button {
@ -439,6 +443,7 @@
} }
} }
} }
}
.icon { .icon {
display: inline-block; display: inline-block;
@ -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 { .comment-type-dropdown.show .dropdown-menu {
display: block; display: block;
} }
.filtered-search-box-input-container { .filtered-search-box-input-container {
.dropdown-menu, .dropdown-menu {
.dropdown-menu-nav {
max-width: 280px; max-width: 280px;
} }
} }
@ -850,8 +846,7 @@
} }
header.navbar-gitlab .dropdown { header.navbar-gitlab .dropdown {
.dropdown-menu, .dropdown-menu {
.dropdown-menu-nav {
width: 100%; width: 100%;
min-width: 100%; min-width: 100%;
} }

View File

@ -227,6 +227,10 @@
padding-left: 40px; padding-left: 40px;
} }
.gl-label-scoped {
--label-inset-border: inset 0 0 0 1px currentColor;
}
@include media-breakpoint-up(lg) { @include media-breakpoint-up(lg) {
margin-right: 5px; margin-right: 5px;
} }

View File

@ -9,6 +9,7 @@ class Admin::UsersController < Admin::ApplicationController
def index def index
@users = User.filter_items(params[:filter]).order_name_asc @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.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.sort_by_attribute(@sort = params[:sort])
@users = @users.page(params[:page]) @users = @users.page(params[:page])
end end

View File

@ -60,14 +60,8 @@ class Groups::LabelsController < Groups::ApplicationController
def destroy def destroy
@label.destroy @label.destroy
respond_to do |format|
format.html do
redirect_to group_labels_path(@group), status: :found, notice: "#{@label.name} deleted permanently" redirect_to group_labels_path(@group), status: :found, notice: "#{@label.name} deleted permanently"
end end
format.js
end
end
protected protected

View File

@ -11,8 +11,6 @@ module Projects
push_frontend_feature_flag(:ci_key_autocomplete, default_enabled: true) push_frontend_feature_flag(:ci_key_autocomplete, default_enabled: true)
end end
helper_method :highlight_badge
def show def show
end end
@ -52,10 +50,6 @@ module Projects
private private
def highlight_badge(name, content, language = nil)
Gitlab::Highlight.highlight(name, content, language: language)
end
def update_params def update_params
params.require(:project).permit(*permitted_project_params) params.require(:project).permit(*permitted_project_params)
end end

View File

@ -38,7 +38,6 @@ class SearchController < ApplicationController
@show_snippets = search_service.show_snippets? @show_snippets = search_service.show_snippets?
@search_results = search_service.search_results @search_results = search_service.search_results
@search_objects = search_service.search_objects(preload_method) @search_objects = search_service.search_objects(preload_method)
@search_highlight = search_service.search_highlight
render_commits if @scope == 'commits' render_commits if @scope == 'commits'
eager_load_user_status if @scope == 'users' eager_load_user_status if @scope == 'users'

View File

@ -1,6 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true
module BlobHelper 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 def no_highlight_files
%w(credits changelog news copying copyright license authors) %w(credits changelog news copying copyright license authors)
end end

View File

@ -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>') simple_search_highlight_and_truncate(issue.description, search_term, highlighter: '<span class="gl-text-black-normal gl-font-weight-bold">\1</span>')
end 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? def show_user_search_tab?
return false if Feature.disabled?(:users_search, default_enabled: true) return false if Feature.disabled?(:users_search, default_enabled: true)

View File

@ -1302,6 +1302,14 @@ class MergeRequest < ApplicationRecord
unlock_mr unlock_mr
end 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 def diverged_commits_count
cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits") cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits")

View File

@ -2292,6 +2292,10 @@ class Project < ApplicationRecord
[] []
end end
def mark_primary_write_location
# Overriden in EE
end
def toggle_ci_cd_settings!(settings_attribute) def toggle_ci_cd_settings!(settings_attribute)
ci_cd_settings.toggle!(settings_attribute) ci_cd_settings.toggle!(settings_attribute)
end end

View File

@ -853,7 +853,7 @@ class Repository
def merge(user, source_sha, merge_request, message) def merge(user, source_sha, merge_request, message)
with_cache_hooks do with_cache_hooks do
raw_repository.merge(user, source_sha, merge_request.target_branch, message) do |commit_id| 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. nil # Return value does not matter.
end end
end end
@ -873,7 +873,7 @@ class Repository
their_commit_id = commit(source)&.id their_commit_id = commit(source)&.id
raise 'Invalid merge source' if their_commit_id.nil? 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) } with_cache_hooks { raw.ff_merge(user, their_commit_id, target_branch) }
end end

View File

@ -8,6 +8,9 @@ class UserPreference < ApplicationRecord
belongs_to :user 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 :issue_notes_filter, :merge_request_notes_filter, inclusion: { in: NOTES_FILTERS.values }, presence: true
validates :tab_width, numericality: { validates :tab_width, numericality: {
only_integer: true, only_integer: true,

View File

@ -27,7 +27,7 @@ module MergeRequests
rescue StandardError => e rescue StandardError => e
raise MergeError, "Something went wrong during merge: #{e.message}" raise MergeError, "Something went wrong during merge: #{e.message}"
ensure ensure
merge_request.update(in_progress_merge_commit_sha: nil) merge_request.update_and_mark_in_progress_merge_commit_sha(nil)
end end
end end
end end

View File

@ -84,7 +84,7 @@ module MergeRequests
merge_request.update!(merge_commit_sha: commit_id) merge_request.update!(merge_commit_sha: commit_id)
ensure ensure
merge_request.update_column(:in_progress_merge_commit_sha, nil) merge_request.update_and_mark_in_progress_merge_commit_sha(nil)
end end
def try_merge def try_merge

View File

@ -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)) @search_objects ||= redact_unauthorized_results(search_results.objects(scope, page: params[:page], per_page: per_page, preload_method: preload_method))
end end
def search_highlight
search_results.highlight_map(scope)
end
private private
def per_page def per_page

View File

@ -19,7 +19,7 @@
%h3.text-center %h3.text-center
= s_('AdminArea|Projects: %{number_of_projects}') % { number_of_projects: approximate_count_with_delimiters(@counts, Project) } = s_('AdminArea|Projects: %{number_of_projects}') % { number_of_projects: approximate_count_with_delimiters(@counts, Project) }
%hr %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 .col-sm-4
.info-well.dark-well .info-well.dark-well
.well-segment.well-centered .well-segment.well-centered
@ -28,8 +28,8 @@
= s_('AdminArea|Users: %{number_of_users}') % { number_of_users: approximate_count_with_delimiters(@counts, User) } = s_('AdminArea|Users: %{number_of_users}') % { number_of_users: approximate_count_with_delimiters(@counts, User) }
%hr %hr
.btn-group.d-flex{ role: 'group' } .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|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 btn-primary 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 .col-sm-4
.info-well.dark-well .info-well.dark-well
.well-segment.well-centered .well-segment.well-centered
@ -37,7 +37,7 @@
%h3.text-center %h3.text-center
= s_('AdminArea|Groups: %{number_of_groups}') % { number_of_groups: approximate_count_with_delimiters(@counts, Group) } = s_('AdminArea|Groups: %{number_of_groups}') % { number_of_groups: approximate_count_with_delimiters(@counts, Group) }
%hr %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 .row
.col-md-4 .col-md-4
#js-admin-statistics-container #js-admin-statistics-container

View File

@ -27,5 +27,5 @@
= render_suggested_colors = render_suggested_colors
.form-actions .form-actions
= f.submit _('Save'), class: 'btn btn-success js-save-button' = f.submit _('Save'), class: 'btn gl-button btn-success js-save-button'
= link_to _("Cancel"), admin_labels_path, class: 'btn btn-cancel' = link_to _("Cancel"), admin_labels_path, class: 'btn gl-button btn-cancel'

View File

@ -1,7 +1,7 @@
%li.label-list-item{ id: dom_id(label) } %li.label-list-item{ id: dom_id(label) }
= render "shared/label_row", label: label.present(issuable_subject: nil) = render "shared/label_row", label: label.present(issuable_subject: nil)
.label-actions-list .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') = 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') = sprite_icon('remove')

View File

@ -1,7 +1,7 @@
- page_title _("Labels") - page_title _("Labels")
%div %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') = _('New label')
%h3.page-title %h3.page-title
= _('Labels') = _('Labels')

View File

@ -4,7 +4,12 @@
= _('Name') = _('Name')
.table-mobile-content .table-mobile-content
= render 'user_detail', user: user = 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' } .table-mobile-header{ role: 'rowheader' }
= _('Created on') = _('Created on')
.table-mobile-content .table-mobile-content

View File

@ -72,7 +72,8 @@
.table-holder .table-holder
.thead-white.text-nowrap.gl-responsive-table-row.table-row-header{ role: 'row' } .thead-white.text-nowrap.gl-responsive-table-row.table-row-header{ role: 'row' }
.table-section.section-40{ role: 'rowheader' }= _('Name') .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') .table-section.section-15{ role: 'rowheader' }= _('Last activity')
= render partial: 'admin/users/user', collection: @users = render partial: 'admin/users/user', collection: @users

View File

@ -1,2 +0,0 @@
- if @group.labels.empty?
$('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)

View File

@ -28,7 +28,8 @@
= render partial: 'projects/commits/commit', collection: context_commits, locals: { project: project, ref: ref, merge_request: merge_request } = render partial: 'projects/commits/commit', collection: context_commits, locals: { project: project, ref: ref, merge_request: merge_request }
- if hidden > 0 - 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) = 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? - if project.context_commits_enabled? && can_update_merge_request && context_commits&.empty?

View File

@ -7,6 +7,6 @@
%h4.gl-mt-0 %h4.gl-mt-0
Request details Request details
.col-lg-9 .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 } = render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log }

View File

@ -15,18 +15,18 @@
.col-md-2.text-center .col-md-2.text-center
Markdown Markdown
.col-md-10.code.js-syntax-highlight .col-md-10.code.js-syntax-highlight
= highlight_badge('.md', badge.to_markdown, language: 'markdown') = highlight('.md', badge.to_markdown, language: 'markdown')
.row .row
%hr %hr
.row .row
.col-md-2.text-center .col-md-2.text-center
HTML HTML
.col-md-10.code.js-syntax-highlight .col-md-10.code.js-syntax-highlight
= highlight_badge('.html', badge.to_html, language: 'html') = highlight('.html', badge.to_html, language: 'html')
.row .row
%hr %hr
.row .row
.col-md-2.text-center .col-md-2.text-center
AsciiDoc AsciiDoc
.col-md-10.code.js-syntax-highlight .col-md-10.code.js-syntax-highlight
= highlight_badge('.adoc', badge.to_asciidoc) = highlight('.adoc', badge.to_asciidoc)

View File

@ -1,4 +1,5 @@
- if @search_objects.to_a.empty? - if @search_objects.to_a.empty?
= render partial: "search/results/filters"
= render partial: "search/results/empty" = render partial: "search/results/empty"
= render_if_exists 'shared/promotions/promote_advanced_search' = render_if_exists 'shared/promotions/promote_advanced_search'
= render_if_exists 'search/form_revert_to_basic' = render_if_exists 'search/form_revert_to_basic'
@ -21,8 +22,7 @@
- link_to_group = link_to(@group.name, @group, class: 'ml-md-1') - 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 } = _("in group %{link_to_group}").html_safe % { link_to_group: link_to_group }
= render_if_exists 'shared/promotions/promote_advanced_search' = render_if_exists 'shared/promotions/promote_advanced_search'
= render partial: "search/results/filters"
#js-search-filter-by-state{ 'v-cloak': true, data: { scope: @scope, state: params[:state] } }
.results.gl-mt-3 .results.gl-mt-3
- if @scope == 'commits' - if @scope == 'commits'

View File

@ -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

View File

@ -9,5 +9,6 @@
%span.term.str-truncated.gl-font-weight-bold.gl-ml-2= issue.title %span.term.str-truncated.gl-font-weight-bold.gl-ml-2= issue.title
.gl-text-gray-500.gl-my-3 .gl-text-gray-500.gl-my-3
= sprintf(s_(' %{project_name}#%{issue_iid} &middot; 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 = sprintf(s_(' %{project_name}#%{issue_iid} &middot; 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
- if issue.description.present?
.description.term.col-sm-10.gl-px-0 .description.term.col-sm-10.gl-px-0
= highlight_and_truncate_issue(issue, @search_term, @search_highlight) = truncate(issue.description, length: 200)

View File

@ -0,0 +1,5 @@
---
title: Set hook_log css to gl-button
merge_request: 42730
author: Mike Terhar @mterhar
type: other

View File

@ -0,0 +1,5 @@
---
title: Add Gitpod enabled user setting to Usage Data
merge_request: 42570
author:
type: changed

View File

@ -1,5 +0,0 @@
---
title: Global Search - Bold Issue's Search Term
merge_request: 41411
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Remove duplicate index on cluster_agents
merge_request: 42902
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Drop Iglu registry URL column
merge_request: 42939
author:
type: removed

View File

@ -0,0 +1,5 @@
---
title: Display user project count on Admin Dashboard
merge_request: 42871
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Move shared logic into utils
merge_request: 42407
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Allow member mapping to map importer user on Group/Project Import
merge_request: 42882
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Fix profile scoped label CSS
merge_request: 43005
author:
type: changed

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
8d14013bcb4d8302c91e331f619fb6f621ab79907aebc421d99c9484ecd7a5d8

View File

@ -0,0 +1 @@
7f62ce5117a16213bad6537dfeae2af4016262c533f8fa6b7a19572077bcf8d7

View File

@ -0,0 +1 @@
ad63096e49440f7f2a15ea2747689ca39f52fdcebc1949a1feed82a22f432e9e

View File

@ -9196,7 +9196,6 @@ CREATE TABLE application_settings (
throttle_incident_management_notification_enabled boolean DEFAULT false NOT NULL, throttle_incident_management_notification_enabled boolean DEFAULT false NOT NULL,
throttle_incident_management_notification_period_in_seconds integer DEFAULT 3600, throttle_incident_management_notification_period_in_seconds integer DEFAULT 3600,
throttle_incident_management_notification_per_period 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_hooks_limit integer DEFAULT 3 NOT NULL,
push_event_activities_limit integer DEFAULT 3 NOT NULL, push_event_activities_limit integer DEFAULT 3 NOT NULL,
custom_http_clone_url_root character varying(511), 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 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_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); 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_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 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); CREATE INDEX index_user_statuses_on_user_id ON user_statuses USING btree (user_id);

View File

@ -82,6 +82,10 @@ local machine, this is a simple way to get started:
-backend-config="retry_wait_min=5" -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. You can now run `terraform plan` and `terraform apply` as you normally would.
## Get started using GitLab CI ## Get started using GitLab CI

View File

@ -20,3 +20,5 @@ module Gitlab
end end
end end
end end
Gitlab::Checks::MatchingMergeRequest.prepend_if_ee('EE::Gitlab::Checks::MatchingMergeRequest')

View File

@ -72,7 +72,7 @@ module Gitlab
end end
def with_lock_retries(&block) def with_lock_retries(&block)
Gitlab::Database::WithLockRetries.new({ Gitlab::Database::WithLockRetries.new(**{
klass: self.class, klass: self.class,
logger: Gitlab::AppLogger logger: Gitlab::AppLogger
}).run(&block) }).run(&block)

View File

@ -35,7 +35,7 @@ module Gitlab
end end
def include?(old_user_id) 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 end
private private
@ -63,6 +63,8 @@ module Gitlab
end end
def add_team_member(member, existing_user = nil) 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['user'] = existing_user
member_hash = member_hash(member) member_hash = member_hash(member)

View File

@ -116,11 +116,6 @@ module Gitlab
UsersFinder.new(current_user, search: query).execute UsersFinder.new(current_user, search: query).execute
end end
# highlighting is only performed by Elasticsearch backed results
def highlight_map(scope)
{}
end
private private
def projects def projects

View File

@ -445,8 +445,11 @@ module Gitlab
# rubocop: enable UsageData/LargeTable # rubocop: enable UsageData/LargeTable
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
# augmented in EE
def user_preferences_usage def user_preferences_usage
{} # augmented in EE {
user_preferences_user_gitpod_enabled: count(UserPreference.with_user.gitpod_enabled.merge(User.active))
}
end end
def merge_requests_users(time_period) def merge_requests_users(time_period)

View File

@ -3037,10 +3037,10 @@ msgstr ""
msgid "Any" msgid "Any"
msgstr "" msgstr ""
msgid "Any Author" msgid "Any %{header}"
msgstr "" msgstr ""
msgid "Any Status" msgid "Any Author"
msgstr "" msgstr ""
msgid "Any branch" msgid "Any branch"

View File

@ -23,6 +23,12 @@ RSpec.describe Admin::UsersController do
expect(assigns(:users)).to eq([admin]) expect(assigns(:users)).to eq([admin])
end end
it 'eager loads authorized projects association' do
get :index
expect(assigns(:users).first.association(:authorized_projects)).to be_loaded
end
end end
describe 'GET :id' do describe 'GET :id' do

View File

@ -56,4 +56,43 @@ RSpec.describe Groups::LabelsController do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end end
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 end

View File

@ -102,6 +102,9 @@ FactoryBot.define do
create(:package, project: projects[1]) create(:package, project: projects[1])
create(:package, created_at: 2.months.ago, 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) ProjectFeature.first.update_attribute('repository_access_level', 0)
# Create fresh & a month (28-days SMAU) old data # Create fresh & a month (28-days SMAU) old data

View File

@ -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(current_user.last_activity_on.strftime("%e %b, %Y"))
expect(page).to have_content(user.email) expect(page).to have_content(user.email)
expect(page).to have_content(user.name) expect(page).to have_content(user.name)
expect(page).to have_content('Projects')
expect(page).to have_button('Block') expect(page).to have_button('Block')
expect(page).to have_button('Deactivate') expect(page).to have_button('Deactivate')
expect(page).to have_button('Delete user') expect(page).to have_button('Delete user')
@ -48,6 +49,19 @@ RSpec.describe "Admin::Users" do
end end
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 describe 'search and sort' do
before do before do
create(:user, name: 'Foo Bar', last_activity_on: 3.days.ago) create(:user, name: 'Foo Bar', last_activity_on: 3.days.ago)

View File

@ -17,10 +17,10 @@ RSpec.describe 'list of badges' do
expect(page).to have_content 'Markdown' expect(page).to have_content 'Markdown'
expect(page).to have_content 'HTML' expect(page).to have_content 'HTML'
expect(page).to have_content 'AsciiDoc' 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']") 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' expect(page).to have_content 'badges/master/pipeline.svg'
end end
end end
@ -32,10 +32,10 @@ RSpec.describe 'list of badges' do
expect(page).to have_content 'Markdown' expect(page).to have_content 'Markdown'
expect(page).to have_content 'HTML' expect(page).to have_content 'HTML'
expect(page).to have_content 'AsciiDoc' 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']") 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' expect(page).to have_content 'badges/master/coverage.svg'
end end
end end

View File

@ -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);
},
);
});

View File

@ -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);
},
);
});
});

View File

@ -1,11 +1,11 @@
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; 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 { import {
FILTER_STATES, FILTER_STATES,
SCOPES,
FILTER_STATES_BY_SCOPE, FILTER_STATES_BY_SCOPE,
FILTER_TEXT, FILTER_HEADER,
SCOPES,
} from '~/search/state_filter/constants'; } from '~/search/state_filter/constants';
import * as urlUtils from '~/lib/utils/url_utility'; import * as urlUtils from '~/lib/utils/url_utility';
@ -15,14 +15,19 @@ jest.mock('~/lib/utils/url_utility', () => ({
})); }));
function createComponent(props = { scope: 'issues' }) { function createComponent(props = { scope: 'issues' }) {
return shallowMount(StateFilter, { return shallowMount(DropdownFilter, {
propsData: { propsData: {
filtersArray: FILTER_STATES_BY_SCOPE.issues,
filters: FILTER_STATES,
header: FILTER_HEADER,
param: 'state',
supportedScopes: Object.values(SCOPES),
...props, ...props,
}, },
}); });
} }
describe('StateFilter', () => { describe('DropdownFilter', () => {
let wrapper; let wrapper;
beforeEach(() => { beforeEach(() => {
@ -41,7 +46,7 @@ describe('StateFilter', () => {
describe('template', () => { describe('template', () => {
describe.each` describe.each`
scope | showStateDropdown scope | showDropdown
${'issues'} | ${true} ${'issues'} | ${true}
${'merge_requests'} | ${true} ${'merge_requests'} | ${true}
${'projects'} | ${false} ${'projects'} | ${false}
@ -50,26 +55,25 @@ describe('StateFilter', () => {
${'notes'} | ${false} ${'notes'} | ${false}
${'wiki_blobs'} | ${false} ${'wiki_blobs'} | ${false}
${'blobs'} | ${false} ${'blobs'} | ${false}
`(`state dropdown`, ({ scope, showStateDropdown }) => { `(`dropdown`, ({ scope, showDropdown }) => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ scope }); wrapper = createComponent({ scope });
}); });
it(`does${showStateDropdown ? '' : ' not'} render when scope is ${scope}`, () => { it(`does${showDropdown ? '' : ' not'} render when scope is ${scope}`, () => {
expect(findGlDropdown().exists()).toBe(showStateDropdown); expect(findGlDropdown().exists()).toBe(showDropdown);
}); });
}); });
describe.each` describe.each`
state | label initialFilter | label
${FILTER_STATES.ANY.value} | ${FILTER_TEXT} ${FILTER_STATES.ANY.value} | ${`Any ${FILTER_HEADER}`}
${FILTER_STATES.OPEN.value} | ${FILTER_STATES.OPEN.label} ${FILTER_STATES.OPEN.value} | ${FILTER_STATES.OPEN.label}
${FILTER_STATES.CLOSED.value} | ${FILTER_STATES.CLOSED.label} ${FILTER_STATES.CLOSED.value} | ${FILTER_STATES.CLOSED.label}
${FILTER_STATES.MERGED.value} | ${FILTER_STATES.MERGED.label} `(`filter text`, ({ initialFilter, label }) => {
`(`filter text`, ({ state, label }) => { describe(`when initialFilter is ${initialFilter}`, () => {
describe(`when state is ${state}`, () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ scope: 'issues', state }); wrapper = createComponent({ scope: 'issues', initialFilter });
}); });
it(`sets dropdown label to ${label}`, () => { it(`sets dropdown label to ${label}`, () => {

View File

@ -5,6 +5,16 @@ require 'spec_helper'
RSpec.describe BlobHelper do RSpec.describe BlobHelper do
include TreeHelper 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 describe "#sanitize_svg_data" do
let(:input_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'unsanitized.svg') } let(:input_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'unsanitized.svg') }
let(:data) { File.read(input_svg_path) } let(:data) { File.read(input_svg_path) }

View File

@ -5,6 +5,8 @@ require 'spec_helper'
RSpec.describe Banzai::Filter::ExternalIssueReferenceFilter do RSpec.describe Banzai::Filter::ExternalIssueReferenceFilter do
include FilterSpecHelper include FilterSpecHelper
let_it_be_with_refind(:project) { create(:project) }
shared_examples_for "external issue tracker" do shared_examples_for "external issue tracker" do
it_behaves_like 'a reference containing an element node' it_behaves_like 'a reference containing an element node'
@ -116,7 +118,7 @@ RSpec.describe Banzai::Filter::ExternalIssueReferenceFilter do
end end
context "redmine project" do context "redmine project" do
let(:project) { create(:redmine_project) } let_it_be(:service) { create(:redmine_service, project: project) }
before do before do
project.update!(issues_enabled: false) project.update!(issues_enabled: false)
@ -138,7 +140,7 @@ RSpec.describe Banzai::Filter::ExternalIssueReferenceFilter do
end end
context "youtrack project" do context "youtrack project" do
let(:project) { create(:youtrack_project) } let_it_be(:service) { create(:youtrack_service, project: project) }
before do before do
project.update!(issues_enabled: false) project.update!(issues_enabled: false)
@ -181,7 +183,7 @@ RSpec.describe Banzai::Filter::ExternalIssueReferenceFilter do
end end
context "jira project" do context "jira project" do
let(:project) { create(:jira_project) } let_it_be(:service) { create(:jira_service, project: project) }
let(:reference) { issue.to_reference } let(:reference) { issue.to_reference }
context "with right markdown" do context "with right markdown" do
@ -210,7 +212,7 @@ RSpec.describe Banzai::Filter::ExternalIssueReferenceFilter do
end end
context "ewm project" do context "ewm project" do
let_it_be(:project) { create(:ewm_project) } let_it_be(:service) { create(:ewm_service, project: project) }
before do before do
project.update!(issues_enabled: false) project.update!(issues_enabled: false)

View File

@ -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

View File

@ -58,25 +58,6 @@ RSpec.describe Gitlab::SearchResults do
end end
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 describe '#formatted_limited_count' do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax

View File

@ -21,12 +21,6 @@ RSpec.describe Gitlab::SnippetSearchResults do
end end
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 describe '#objects' do
it 'uses page and per_page to paginate results' do it 'uses page and per_page to paginate results' do
snippet2 = create(:snippet, :public, content: 'foo', file_name: 'foo') snippet2 = create(:snippet, :public, content: 'foo', file_name: 'foo')

View File

@ -499,6 +499,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
expect(count_data[:projects_with_packages]).to eq(2) expect(count_data[:projects_with_packages]).to eq(2)
expect(count_data[:packages]).to eq(4) expect(count_data[:packages]).to eq(4)
expect(count_data[:user_preferences_user_gitpod_enabled]).to eq(1)
end end
it 'gathers object store usage correctly' do it 'gathers object store usage correctly' do

View File

@ -4299,4 +4299,18 @@ RSpec.describe MergeRequest, factory_default: :keep do
expect(merge_request.allows_reviewers?).to be(true) expect(merge_request.allows_reviewers?).to be(true)
end end
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 end

View File

@ -72,15 +72,22 @@ RSpec.describe MergeRequests::FfMergeService do
end end
it 'does not update squash_commit_sha if it is not a squash' do 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 { execute_ff_merge }.not_to change { merge_request.squash_commit_sha }
expect(merge_request.in_progress_merge_commit_sha).to be_nil
end end
it 'updates squash_commit_sha if it is a squash' do 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) merge_request.update!(squash: true)
expect { execute_ff_merge } expect { execute_ff_merge }
.to change { merge_request.squash_commit_sha } .to change { merge_request.squash_commit_sha }
.from(nil) .from(nil)
expect(merge_request.in_progress_merge_commit_sha).to be_nil
end end
end end

View File

@ -22,6 +22,7 @@ RSpec.describe MergeRequests::MergeService do
context 'valid params' do context 'valid params' do
before do before do
allow(service).to receive(:execute_hooks) 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 perform_enqueued_jobs do
service.execute(merge_request) service.execute(merge_request)

View File

@ -133,6 +133,7 @@ module UsageDataHelpers
todos todos
uploads uploads
web_hooks web_hooks
user_preferences_user_gitpod_enabled
).push(*SMAU_KEYS) ).push(*SMAU_KEYS)
USAGE_DATA_KEYS = %i( USAGE_DATA_KEYS = %i(

View File

@ -65,7 +65,7 @@ RSpec.shared_examples 'Notes user references' do
include_examples 'sets the note author to the mapped user' 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 end
context 'and the note author exists in the target instance' do context 'and the note author exists in the target instance' do

View File

@ -60,6 +60,28 @@ RSpec.describe 'search/_results' do
expect(rendered).to have_selector('#js-search-filter-by-state') expect(rendered).to have_selector('#js-search-filter-by-state')
end 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 end
end end