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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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>')
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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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))
end
def search_highlight
search_results.highlight_map(scope)
end
private
def per_page

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 }
- 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?

View File

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

View File

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

View File

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

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
.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
.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)

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_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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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(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)

View File

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

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 { 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}`, () => {

View File

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

View File

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

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

View File

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

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[:packages]).to eq(4)
expect(count_data[:user_preferences_user_gitpod_enabled]).to eq(1)
end
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)
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

View File

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

View File

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

View File

@ -133,6 +133,7 @@ module UsageDataHelpers
todos
uploads
web_hooks
user_preferences_user_gitpod_enabled
).push(*SMAU_KEYS)
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 'adds original autor note'
include_examples 'does not add original autor note'
end
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')
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