Commit ca4b31bb authored by Sean McGivern's avatar Sean McGivern

Merge branch 'master' into 34092-remove-webpack-rails-gem

parents a5db2ce8 fa0d93e9
......@@ -96,4 +96,4 @@ apollo.config.js
/tmp/matching_foss_tests.txt
/tmp/matching_tests.txt
ee/changelogs/unreleased-ee
/sitespeed-result
......@@ -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)
......
......@@ -572,7 +572,7 @@ export class AwardsHandler {
}
findMatchingEmojiElements(query) {
const emojiMatches = this.emoji.filterEmojiNamesByAlias(query);
const emojiMatches = this.emoji.queryEmojiNames(query);
const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]');
const $matchingElements = $emojiElements.filter(
(i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0,
......
......@@ -201,7 +201,13 @@ export default {
<section v-else>
<ancestor-notice />
<gl-table :items="clusters" :fields="fields" stacked="md" class="qa-clusters-table">
<gl-table
:items="clusters"
:fields="fields"
stacked="md"
class="qa-clusters-table"
data-testid="cluster_list_table"
>
<template #cell(name)="{ item }">
<div :class="[contentAlignClasses, 'js-status']">
<img
......
<script>
/* eslint-disable vue/no-v-html */
import { GlIcon } from '@gitlab/ui';
import { GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import iconBranch from '../svg/icon_branch.svg';
import limitWarning from './limit_warning_component.vue';
......@@ -13,6 +12,9 @@ export default {
limitWarning,
GlIcon,
},
directives: {
SafeHtml,
},
props: {
items: {
type: Array,
......@@ -47,7 +49,7 @@ export default {
<a :href="build.url" class="pipeline-id"> #{{ build.id }} </a>
<gl-icon :size="16" name="fork" />
<a :href="build.branch.url" class="ref-name"> {{ build.branch.name }} </a>
<span class="icon-branch" v-html="iconBranch"> </span>
<span v-safe-html="iconBranch" class="icon-branch"> </span>
<a :href="build.commitUrl" class="commit-sha"> {{ build.shortSha }} </a>
</h5>
<span>
......
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;
};
<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>
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import {
MATCH_LINE_TYPE,
NEW_LINE_TYPE,
OLD_LINE_TYPE,
CONTEXT_LINE_TYPE,
CONTEXT_LINE_CLASS_NAME,
LINE_POSITION_LEFT,
LINE_POSITION_RIGHT,
LINE_HOVER_CLASS_NAME,
OLD_NO_NEW_LINE_TYPE,
NEW_NO_NEW_LINE_TYPE,
EMPTY_CELL_TYPE,
} from '../constants';
import { __ } from '~/locale';
import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
import { CONTEXT_LINE_CLASS_NAME } from '../constants';
import DiffGutterAvatars from './diff_gutter_avatars.vue';
import * as utils from './diff_row_utils';
export default {
components: {
......@@ -61,14 +48,11 @@ export default {
...mapGetters('diffs', ['fileLineCoverage']),
...mapState({
isHighlighted(state) {
if (this.isCommented) return true;
const lineCode = this.line.line_code;
return lineCode ? lineCode === state.diffs.highlightedRow : false;
return utils.isHighlighted(state, this.line, this.isCommented);
},
}),
isContextLine() {
return this.line.type === CONTEXT_LINE_TYPE;
return utils.isContextLine(this.line.type);
},
classNameMap() {
return [
......@@ -82,82 +66,48 @@ export default {
return this.line.line_code || `${this.fileHash}_${this.line.old_line}_${this.line.new_line}`;
},
isMatchLine() {
return this.line.type === MATCH_LINE_TYPE;
return utils.isMatchLine(this.line.type);
},
coverageState() {
return this.fileLineCoverage(this.filePath, this.line.new_line);
},
isMetaLine() {
const { type } = this.line;
return (
type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE
);
return utils.isMetaLine(this.line.type);
},
classNameMapCell() {
const { type } = this.line;
return [
type,
{
hll: this.isHighlighted,
[LINE_HOVER_CLASS_NAME]:
this.isLoggedIn && this.isHover && !this.isContextLine && !this.isMetaLine,
},
];
return utils.classNameMapCell(this.line, this.isHighlighted, this.isLoggedIn, this.isHover);
},
addCommentTooltip() {
const brokenSymlinks = this.line.commentsDisabled;
let tooltip = __('Add a comment to this line');
if (brokenSymlinks) {
if (brokenSymlinks.wasSymbolic || brokenSymlinks.isSymbolic) {
tooltip = __(
'Commenting on symbolic links that replace or are replaced by files is currently not supported.',
);
} else if (brokenSymlinks.wasReal || brokenSymlinks.isReal) {
tooltip = __(
'Commenting on files that replace or are replaced by symbolic links is currently not supported.',
);
}
}
return tooltip;
return utils.addCommentTooltip(this.line);
},
shouldRenderCommentButton() {
if (this.isLoggedIn) {
const isDiffHead = parseBoolean(getParameterByName('diff_head'));
return !isDiffHead || gon.features?.mergeRefHeadComments;
}
return false;
return utils.shouldRenderCommentButton(
this.isLoggedIn,
true,
gon.features?.mergeRefHeadComments,
);
},
shouldShowCommentButton() {
return this.isHover && !this.isContextLine && !this.isMetaLine && !this.hasDiscussions;
return utils.shouldShowCommentButton(
this.isHover,
this.isContextLine,
this.isMetaLine,
this.hasDiscussions,
);
},
hasDiscussions() {
return this.line.discussions && this.line.discussions.length > 0;
return utils.hasDiscussions(this.line);
},
lineHref() {
return `#${this.line.line_code || ''}`;
return utils.lineHref(this.line);
},
lineCode() {
return (
this.line.line_code ||
(this.line.left && this.line.left.line_code) ||
(this.line.right && this.line.right.line_code)
);
return utils.lineCode(this.line);
},
shouldShowAvatarsOnGutter() {
return this.hasDiscussions;
},
},
created() {
this.newLineType = NEW_LINE_TYPE;
this.oldLineType = OLD_LINE_TYPE;
this.linePositionLeft = LINE_POSITION_LEFT;
this.linePositionRight = LINE_POSITION_RIGHT;
},
mounted() {
this.scrollToLineIfNeededInline(this.line);
},
......@@ -242,6 +192,7 @@ export default {
class="line-coverage"
></td>
<td
:key="line.line_code"
v-safe-html="line.rich_text"
:class="[
line.type,
......
......@@ -2,21 +2,9 @@
import { mapActions, mapGetters, mapState } from 'vuex';
import $ from 'jquery';
import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import {
MATCH_LINE_TYPE,
NEW_LINE_TYPE,
OLD_LINE_TYPE,
CONTEXT_LINE_TYPE,
CONTEXT_LINE_CLASS_NAME,
OLD_NO_NEW_LINE_TYPE,
PARALLEL_DIFF_VIEW_TYPE,
NEW_NO_NEW_LINE_TYPE,
EMPTY_CELL_TYPE,
LINE_HOVER_CLASS_NAME,
} from '../constants';
import { __ } from '~/locale';
import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
import { CONTEXT_LINE_CLASS_NAME, PARALLEL_DIFF_VIEW_TYPE } from '../constants';
import DiffGutterAvatars from './diff_gutter_avatars.vue';
import * as utils from './diff_row_utils';
export default {
components: {
......@@ -63,20 +51,15 @@ export default {
...mapGetters(['isLoggedIn']),
...mapState({
isHighlighted(state) {
if (this.isCommented) return true;
const lineCode =
(this.line.left && this.line.left.line_code) ||
(this.line.right && this.line.right.line_code);
return lineCode ? lineCode === state.diffs.highlightedRow : false;
const line = this.line.left?.line_code ? this.line.left : this.line.right;
return utils.isHighlighted(state, line, this.isCommented);
},
}),
isContextLineLeft() {
return this.line.left && this.line.left.type === CONTEXT_LINE_TYPE;
return utils.isContextLine(this.line.left?.type);
},
isContextLineRight() {
return this.line.right && this.line.right.type === CONTEXT_LINE_TYPE;
return utils.isContextLine(this.line.right?.type);
},
classNameMap() {
return {
......@@ -85,156 +68,83 @@ 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);
......@@ -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,
......
import { uniq } from 'lodash';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import emojiAliases from 'emojis/aliases.json';
import axios from '../lib/utils/axios_utils';
......@@ -62,13 +63,18 @@ export function isEmojiNameValid(name) {
return validEmojiNames.indexOf(name) >= 0;
}
export function filterEmojiNames(filter) {
const match = filter.toLowerCase();
return validEmojiNames.filter(name => name.indexOf(match) >= 0);
}
export function filterEmojiNamesByAlias(filter) {
return uniq(filterEmojiNames(filter).map(name => normalizeEmojiName(name)));
/**
* Search emoji by name or alias. Returns a normalized, deduplicated list of
* names.
*
* Calling with an empty filter returns an empty array.
*
* @param {String}
* @returns {Array}
*/
export function queryEmojiNames(filter) {
const matches = fuzzaldrinPlus.filter(validEmojiNames, filter);
return uniq(matches.map(name => normalizeEmojiName(name)));
}
let emojiCategoryMap;
......
import { take } from 'lodash';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { sanitize } from 'dompurify';
import { sanitize } from '~/lib/dompurify';
import { FREQUENT_ITEMS, HOUR_IN_MS } from './constants';
export const isMobile = () => ['md', 'sm', 'xs'].includes(bp.getBreakpointSize());
......
<script>
import { GlLink } from '@gitlab/ui';
import { GlLink, GlTooltipDirective, GlSprintf } from '@gitlab/ui';
import { formatDate } from '~/lib/utils/datetime_utility';
export default {
components: {
GlLink,
GlSprintf,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
alert: {
......@@ -24,17 +28,23 @@ export default {
<div
class="gl-border-solid gl-border-1 gl-border-gray-100 gl-p-5 gl-mb-3 gl-rounded-base gl-display-flex gl-justify-content-space-between"
>
<div class="text-truncate gl-pr-3">
<div class="gl-pr-3">
<span class="gl-font-weight-bold">{{ s__('HighlightBar|Original alert:') }}</span>
<gl-link :href="alert.detailsUrl">{{ alert.title }}</gl-link>
<gl-link v-gl-tooltip :title="alert.title" :href="alert.detailsUrl">
<gl-sprintf :message="__('Alert #%{alertId}')">
<template #alertId>
<span>{{ alert.iid }}</span>
</template>
</gl-sprintf>
</gl-link>
</div>
<div class="gl-pr-3 gl-white-space-nowrap">
<div class="gl-pr-3">
<span class="gl-font-weight-bold">{{ s__('HighlightBar|Alert start time:') }}</span>
{{ startTime }}
</div>
<div class="gl-white-space-nowrap">
<div>
<span class="gl-font-weight-bold">{{ s__('HighlightBar|Alert events:') }}</span>
<span>{{ alert.eventCount }}</span>
</div>
......
import { sanitize } from 'dompurify';
import { sanitize } from '~/lib/dompurify';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import updateDescription from '../utils/update_description';
......
import { sanitize } from 'dompurify';
import { sanitize } from '~/lib/dompurify';
// We currently load + parse the data from the issue app and related merge request
let cachedParsedData;
......
import { sanitize as dompurifySanitize, addHook } from 'dompurify';
import { getBaseURL, relativePathToAbsolute } from '~/lib/utils/url_utility';
// Safely allow SVG <use> tags
const defaultConfig = {
ADD_TAGS: ['use'],
};
// Only icons urls from `gon` are allowed
const getAllowedIconUrls = (gon = window.gon) =>
[gon.sprite_file_icons, gon.sprite_icons].filter(Boolean);
const isUrlAllowed = url => getAllowedIconUrls().some(allowedUrl => url.startsWith(allowedUrl));
const isHrefSafe = url =>
isUrlAllowed(url) || isUrlAllowed(relativePathToAbsolute(url, getBaseURL()));
const removeUnsafeHref = (node, attr) => {
if (!node.hasAttribute(attr)) {
return;
}
if (!isHrefSafe(node.getAttribute(attr))) {
node.removeAttribute(attr);
}
};
/**
* Sanitize icons' <use> tag attributes, to safely include
* svgs such as in:
*
* <svg viewBox="0 0 100 100">
* <use href="/assets/icons-xxx.svg#icon_name"></use>
* </svg>
*
* @param {Object} node - Node to sanitize
*/
const sanitizeSvgIcon = node => {
removeUnsafeHref(node, 'href');
// Note: `xlink:href` is deprecated, but still in use
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href
removeUnsafeHref(node, 'xlink:href');
};
addHook('afterSanitizeAttributes', node => {
if (node.tagName.toLowerCase() === 'use') {
sanitizeSvgIcon(node);
}
});
export const sanitize = (val, config = defaultConfig) => dompurifySanitize(val, config);
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { sanitize } from 'dompurify';
import { sanitize } from '~/lib/dompurify';
/**
* Wraps substring matches with HTML `<span>` elements.
......
<script>
/* eslint-disable vue/no-v-html */
import marked from 'marked';
import { sanitize } from 'dompurify';
import katex from 'katex';
import { sanitize } from '~/lib/dompurify';
import Prompt from './prompt.vue';
const renderer = new marked.Renderer();
......
<script>
/* eslint-disable vue/no-v-html */
import { sanitize } from 'dompurify';
import { sanitize } from '~/lib/dompurify';
import Prompt from '../prompt.vue';
export default {
......
import $ from 'jquery';
import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
import { initCommitBoxInfo } from '~/projects/commit_box/info';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import { fetchCommitMergeRequests } from '~/commit_merge_requests';
document.addEventListener('DOMContentLoaded', () => {
new MiniPipelineGraph({
container: '.js-commit-pipeline-graph',
}).bindEvents();
// eslint-disable-next-line no-jquery/no-load
$('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
fetchCommitMergeRequests();
initCommitBoxInfo();
initPipelines();
});
......@@ -4,10 +4,8 @@ import $ from 'jquery';
import Diff from '~/diff';
import ZenMode from '~/zen_mode';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
import initNotes from '~/init_notes';
import initChangesDropdown from '~/init_changes_dropdown';
import { fetchCommitMergeRequests } from '~/commit_merge_requests';
import '~/sourcegraph/load';
import { handleLocationHash } from '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils';
......@@ -15,6 +13,7 @@ import syntaxHighlight from '~/syntax_highlight';
import flash from '~/flash';
import { __ } from '~/locale';
import loadAwardsHandler from '~/awards_handler';
import { initCommitBoxInfo } from '~/projects/commit_box/info';
document.addEventListener('DOMContentLoaded', () => {
const hasPerfBar = document.querySelector('.with-performance-bar');
......@@ -22,13 +21,10 @@ document.addEventListener('DOMContentLoaded', () => {
initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight + performanceHeight);
new ZenMode();
new ShortcutsNavigation();
new MiniPipelineGraph({
container: '.js-commit-pipeline-graph',
}).bindEvents();
initCommitBoxInfo();
initNotes();
// eslint-disable-next-line no-jquery/no-load
$('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
fetchCommitMergeRequests();
const filesContainer = $('.js-diffs-batch');
......
import Search from './search';
import initStateFilter from '~/search/state_filter';
import initConfidentialFilter from '~/search/confidential_filter';
document.addEventListener('DOMContentLoaded', () => {
initStateFilter();
initConfidentialFilter();
return new Search();
});
......@@ -2,7 +2,7 @@
import $ from 'jquery';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { sanitize } from 'dompurify';
import { sanitize } from '~/lib/dompurify';
import axios from '~/lib/utils/axios_utils';
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import { deprecatedCreateFlash as flash } from '~/flash';
......
import { loadBranches } from './load_branches';
import { fetchCommitMergeRequests } from '~/commit_merge_requests';
import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
export const initCommitBoxInfo = (containerSelector = '.js-commit-box-info') => {
const containerEl = document.querySelector(containerSelector);
// Display commit related branches
loadBranches(containerEl);
// Related merge requests to this commit
fetchCommitMergeRequests();
// Display pipeline info for this commit
new MiniPipelineGraph({
container: '.js-commit-pipeline-graph',
}).bindEvents();
};
import axios from 'axios';
import { sanitize } from '~/lib/dompurify';
import { __ } from '~/locale';
export const loadBranches = containerEl => {
if (!containerEl) {
return;
}
const { commitPath } = containerEl.dataset;
const branchesEl = containerEl.querySelector('.commit-info.branches');
axios
.get(commitPath)
.then(({ data }) => {
branchesEl.innerHTML = sanitize(data);
})
.catch(() => {
branchesEl.textContent = __('Failed to load branches. Please try again.');
});
};
<script>
import { GlTooltipDirective, GlLink, GlBadge, GlButton, GlIcon } from '@gitlab/ui';
import { GlTooltipDirective, GlLink, GlBadge, GlButton } from '@gitlab/ui';
import { BACK_URL_PARAM } from '~/releases/constants';
import { setUrlParams } from '~/lib/utils/url_utility';
......@@ -8,7 +8,6 @@ export default {
components: {
GlLink,
GlBadge,
GlIcon,
GlButton,
},
directives: {
......@@ -55,11 +54,10 @@ export default {
v-gl-tooltip
category="primary"
variant="default"
icon="pencil"
class="gl-mr-3 js-edit-button ml-2 pb-2"
:title="__('Edit this release')"
:href="editLink"
>
<gl-icon name="pencil" />
</gl-button>
/>
</div>
</template>
<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);
import { sprintf, s__ } from '~/locale';
export default {
name: 'StateFilter',
name: 'DropdownFilter',
components: {
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
},
props: {
scope: {
initialFilter: {
type: String,
required: false,
default: null,
},
filters: {
type: Object,
required: true,
},
filtersArray: {
type: Array,
required: true,
},
state: {
header: {
type: String,
required: false,
default: FILTER_STATES.ANY.value,
validator: v => FILTERS_ARRAY.some(({ value }) => value === v),
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 filter = FILTERS_ARRAY.find(({ value }) => value === this.selectedFilter);
if (!filter || filter === FILTER_STATES.ANY) {
return FILTER_TEXT;
const f = this.filtersArray.find(({ value }) => value === this.selectedFilter);
if (!f || f === this.filters.ANY) {
return sprintf(s__('Any %{header}'), { header: this.header });
}
return filter.label;
return f.label;
},
showDropdown() {
return Object.values(SCOPES).includes(this.scope);
return this.supportedScopes.includes(this.scope);
},
selectedFilter: {
get() {
if (FILTERS_ARRAY.some(({ value }) => value === this.state)) {
return this.state;
if (this.filtersArray.some(({ value }) => value === this.filter)) {
return this.filter;
}
return FILTER_STATES.ANY.value;
return this.filters.ANY.value;
},
set(state) {
visitUrl(setUrlParams({ state }));
set(filter) {
visitUrl(setUrlParams({ [this.param]: filter }));
},
},
},
......@@ -59,36 +73,39 @@ export default {
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,
filter === this.filters.ANY,
};
},
isFilterSelected(filter) {
return filter === this.selectedFilter;
},
handleFilterChange(state) {
this.selectedFilter = state;
handleFilterChange(filter) {
this.selectedFilter = filter;
},
},
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">
<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">
{{ $options.filterHeader }}
{{ header }}
</header>
<gl-dropdown-divider />
<gl-dropdown-item
v-for="filter in $options.filtersByScope[scope]"
:key="filter.value"
v-for="f in filtersArray"
:key="f.value"
:is-check-item="true"
:is-checked="isFilterSelected(filter.value)"
:class="dropDownItemClass(filter)"
@click="handleFilterChange(filter.value)"
>{{ filter.label }}</gl-dropdown-item
:is-checked="isFilterSelected(f.value)"
:class="dropDownItemClass(f)"
@click="handleFilterChange(f.value)"
>
{{ f.label }}
</gl-dropdown-item>
</gl-dropdown>
</template>
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';
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),
},
});
},
});
};
......@@ -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';
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),
},
});
},
......
import Vue from 'vue';
import { sanitize } from 'dompurify';
import { sanitize } from '~/lib/dompurify';
import UsersCache from './lib/utils/users_cache';
import UserPopover from './vue_shared/components/user_popover/user_popover.vue';
......
......@@ -112,8 +112,7 @@ a {
}
.dropdown-menu a,
.dropdown-menu button,
.dropdown-menu-nav a {
.dropdown-menu button {
transition: none;
}
......
......@@ -33,8 +33,7 @@
}
.show.dropdown {
.dropdown-menu,
.dropdown-menu-nav {
.dropdown-menu {
@include set-visible;
min-height: $dropdown-min-height;
max-height: $dropdown-max-height;
......@@ -258,8 +257,7 @@
}
}
.dropdown-menu,
.dropdown-menu-nav {
.dropdown-menu {
display: none;
position: absolute;
width: auto;
......@@ -393,7 +391,13 @@
pointer-events: none;
}
.dropdown-menu li {
.dropdown-menu {
display: none;
opacity: 1;
visibility: visible;
transform: translateY(0);
li {
cursor: pointer;
&.droplab-item-active button {
......@@ -439,6 +443,7 @@
}
}
}
}
.icon {
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 {
display: block;
}
.filtered-search-box-input-container {
.dropdown-menu,
.dropdown-menu-nav {
.dropdown-menu {
max-width: 280px;
}
}
......@@ -850,8 +846,7 @@
}
header.navbar-gitlab .dropdown {
.dropdown-menu,
.dropdown-menu-nav {
.dropdown-menu {
width: 100%;
min-width: 100%;
}
......
......@@ -227,6 +227,10 @@
padding-left: 40px;
}
.gl-label-scoped {
--label-inset-border: inset 0 0 0 1px currentColor;
}
@include media-breakpoint-up(lg) {
margin-right: 5px;
}
......
......@@ -9,6 +9,7 @@ class Admin::UsersController < Admin::ApplicationController
def index
@users = User.filter_items(params[:filter]).order_name_asc
@users = @users.search_with_secondary_emails(params[:search_query]) if params[:search_query].present?
@users = @users.includes(:authorized_projects) # rubocop: disable CodeReuse/ActiveRecord
@users = @users.sort_by_attribute(@sort = params[:sort])
@users = @users.page(params[:page])
end
......
......@@ -60,14 +60,8 @@ 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
end
protected
......
......@@ -11,8 +11,6 @@ module Projects
push_frontend_feature_flag(:ci_key_autocomplete, default_enabled: true)
end
helper_method :highlight_badge
def show
end
......@@ -52,10 +50,6 @@ module Projects
private
def highlight_badge(name, content, language = nil)
Gitlab::Highlight.highlight(name, content, language: language)
end
def update_params
params.require(:project).permit(*permitted_project_params)
end
......
......@@ -38,7 +38,6 @@ class SearchController < ApplicationController
@show_snippets = search_service.show_snippets?
@search_results = search_service.search_results
@search_objects = search_service.search_objects(preload_method)
@search_highlight = search_service.search_highlight
render_commits if @scope == 'commits'
eager_load_user_status if @scope == 'users'
......
# frozen_string_literal: true
module BlobHelper
def highlight(file_name, file_content, language: nil, plain: false)
highlighted = Gitlab::Highlight.highlight(file_name, file_content, plain: plain, language: language)
raw %(<pre class="code highlight"><code>#{highlighted}</code></pre>)
end
def no_highlight_files
%w(credits changelog news copying copyright license authors)
end
......
......@@ -299,13 +299,6 @@ module SearchHelper
simple_search_highlight_and_truncate(issue.description, search_term, highlighter: '<span class="gl-text-black-normal gl-font-weight-bold">\1</span>')
end
def simple_search_highlight_and_truncate(text, phrase, options = {})
truncate_length = options.delete(:length) { 200 }
text = truncate(text, length: truncate_length)
phrase = phrase.split
highlight(text, phrase, options)
end
def show_user_search_tab?
return false if Feature.disabled?(:users_search, default_enabled: true)
......
......@@ -1302,6 +1302,14 @@ class MergeRequest < ApplicationRecord
unlock_mr
end
def update_and_mark_in_progress_merge_commit_sha(commit_id)
self.update(in_progress_merge_commit_sha: commit_id)
# Since another process checks for matching merge request, we need
# to make it possible to detect whether the query should go to the
# primary.
target_project.mark_primary_write_location
end
def diverged_commits_count
cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits")
......
......@@ -2292,6 +2292,10 @@ class Project < ApplicationRecord
[]
end
def mark_primary_write_location
# Overriden in EE
end
def toggle_ci_cd_settings!(settings_attribute)
ci_cd_settings.toggle!(settings_attribute)
end
......
......@@ -853,7 +853,7 @@ class Repository
def merge(user, source_sha, merge_request, message)
with_cache_hooks do
raw_repository.merge(user, source_sha, merge_request.target_branch, message) do |commit_id|
merge_request.update(in_progress_merge_commit_sha: commit_id)
merge_request.update_and_mark_in_progress_merge_commit_sha(commit_id)
nil # Return value does not matter.
end
end
......@@ -873,7 +873,7 @@ class Repository
their_commit_id = commit(source)&.id
raise 'Invalid merge source' if their_commit_id.nil?
merge_request&.update(in_progress_merge_commit_sha: their_commit_id)
merge_request&.update_and_mark_in_progress_merge_commit_sha(their_commit_id)
with_cache_hooks { raw.ff_merge(user, their_commit_id, target_branch) }
end
......
......@@ -8,6 +8,9 @@ class UserPreference < ApplicationRecord
belongs_to :user
scope :with_user, -> { joins(:user) }
scope :gitpod_enabled, -> { where(gitpod_enabled: true) }
validates :issue_notes_filter, :merge_request_notes_filter, inclusion: { in: NOTES_FILTERS.values }, presence: true
validates :tab_width, numericality: {
only_integer: true,
......
......@@ -27,7 +27,7 @@ module MergeRequests
rescue StandardError => e
raise MergeError, "Something went wrong during merge: #{e.message}"
ensure
merge_request.update(in_progress_merge_commit_sha: nil)
merge_request.update_and_mark_in_progress_merge_commit_sha(nil)
end
end
end
......@@ -84,7 +84,7 @@ module MergeRequests
merge_request.update!(merge_commit_sha: commit_id)
ensure
merge_request.update_column(:in_progress_merge_commit_sha, nil)
merge_request.update_and_mark_in_progress_merge_commit_sha(nil)
end
def try_merge
......
......@@ -65,10 +65,6 @@ class SearchService
@search_objects ||= redact_unauthorized_results(search_results.objects(scope, page: params[:page], per_page: per_page, preload_method: preload_method))
end
def search_highlight
search_results.highlight_map(scope)
end
private
def per_page
......
......@@ -4,6 +4,9 @@ module Snippets
class BaseService < ::BaseService
include SpamCheckMethods
UPDATE_COMMIT_MSG = 'Update snippet'
INITIAL_COMMIT_MSG = 'Initial commit'
CreateRepositoryError = Class.new(StandardError)
attr_reader :uploaded_assets, :snippet_actions
......@@ -85,5 +88,20 @@ module Snippets
def restricted_files_actions
nil
end
def commit_attrs(snippet, msg)
{
branch_name: snippet.default_branch,
message: msg
}
end
def delete_repository(snippet)
snippet.repository.remove
snippet.snippet_repository&.delete
# Purge any existing value for repository_exists?
snippet.repository.expire_exists_cache
end
end
end
......@@ -59,7 +59,7 @@ module Snippets
log_error(e.message)
# If the commit action failed we need to remove the repository if exists
@snippet.repository.remove if @snippet.repository_exists?
delete_repository(@snippet) if @snippet.repository_exists?
# If the snippet was created, we need to remove it as we
# would do like if it had had any validation error
......@@ -81,12 +81,9 @@ module Snippets
end
def create_commit
commit_attrs = {
branch_name: @snippet.default_branch,
message: 'Initial commit'
}
attrs = commit_attrs(@snippet, INITIAL_COMMIT_MSG)
@snippet.snippet_repository.multi_files_action(current_user, files_to_commit(@snippet), commit_attrs)
@snippet.snippet_repository.multi_files_action(current_user, files_to_commit(@snippet), attrs)
end
def move_temporary_files
......
......@@ -37,7 +37,10 @@ module Snippets
# is implemented.
# Once we can perform different operations through this service
# we won't need to keep track of the `content` and `file_name` fields
if snippet_actions.any?
#
# If the repository does not exist we don't need to update `params`
# because we need to commit the information from the database
if snippet_actions.any? && snippet.repository_exists?
params[:content] = snippet_actions[0].content if snippet_actions[0].content
params[:file_name] = snippet_actions[0].file_path
end
......@@ -52,7 +55,11 @@ module Snippets
# the repository we can just return
return true unless committable_attributes?
unless snippet.repository_exists?
create_repository_for(snippet)
create_first_commit_using_db_data(snippet)
end
create_commit(snippet)
true
......@@ -72,13 +79,7 @@ module Snippets
# If the commit action failed we remove it because
# we don't want to leave empty repositories
# around, to allow cloning them.
if repository_empty?(snippet)
snippet.repository.remove
snippet.snippet_repository&.delete
end
# Purge any existing value for repository_exists?
snippet.repository.expire_exists_cache
delete_repository(snippet) if repository_empty?(snippet)
false
end
......@@ -89,15 +90,25 @@ module Snippets
raise CreateRepositoryError, 'Repository could not be created' unless snippet.repository_exists?
end
# If the user provides `snippet_actions` and the repository
# does not exist, we need to commit first the snippet info stored
# in the database. Mostly because the content inside `snippet_actions`
# would assume that the file is already in the repository.
def create_first_commit_using_db_data(snippet)
return if snippet_actions.empty?
attrs = commit_attrs(snippet, INITIAL_COMMIT_MSG)
actions = [{ file_path: snippet.file_name, content: snippet.content }]
snippet.snippet_repository.multi_files_action(current_user, actions, attrs)
end
def create_commit(snippet)
raise UpdateError unless snippet.snippet_repository
commit_attrs = {
branch_name: snippet.default_branch,
message: 'Update snippet'
}
attrs = commit_attrs(snippet, UPDATE_COMMIT_MSG)
snippet.snippet_repository.multi_files_action(current_user, files_to_commit(snippet), commit_attrs)
snippet.snippet_repository.multi_files_action(current_user, files_to_commit(snippet), attrs)
end
# Because we are removing repositories we don't want to remove
......
......@@ -19,7 +19,7 @@
%h3.text-center
= s_('AdminArea|Projects: %{number_of_projects}') % { number_of_projects: approximate_count_with_delimiters(@counts, Project) }
%hr
= link_to(s_('AdminArea|New project'), new_project_path, class: "btn btn-success gl-w-full")
= link_to(s_('AdminArea|New project'), new_project_path, class: "btn gl-button btn-success gl-w-full")
.col-sm-4
.info-well.dark-well
.well-segment.well-centered
......@@ -28,8 +28,8 @@
= s_('AdminArea|Users: %{number_of_users}') % { number_of_users: approximate_count_with_delimiters(@counts, User) }
%hr
.btn-group.d-flex{ role: 'group' }
= link_to s_('AdminArea|New user'), new_admin_user_path, class: "btn btn-success gl-w-full"
= link_to s_('AdminArea|Users statistics'), admin_dashboard_stats_path, class: 'btn btn-primary gl-w-full'
= link_to s_('AdminArea|New user'), new_admin_user_path, class: "btn gl-button btn-success gl-w-full"
= link_to s_('AdminArea|Users statistics'), admin_dashboard_stats_path, class: 'btn gl-button btn-info gl-w-full'
.col-sm-4
.info-well.dark-well
.well-segment.well-centered
......@@ -37,7 +37,7 @@
%h3.text-center
= s_('AdminArea|Groups: %{number_of_groups}') % { number_of_groups: approximate_count_with_delimiters(@counts, Group) }
%hr
= link_to s_('AdminArea|New group'), new_admin_group_path, class: "btn btn-success gl-w-full"
= link_to s_('AdminArea|New group'), new_admin_group_path, class: "btn gl-button btn-success gl-w-full"
.row
.col-md-4
#js-admin-statistics-container
......
......@@ -27,5 +27,5 @@
= render_suggested_colors
.form-actions
= f.submit _('Save'), class: 'btn btn-success js-save-button'
= link_to _("Cancel"), admin_labels_path, class: 'btn btn-cancel'
= f.submit _('Save'), class: 'btn gl-button btn-success js-save-button'
= link_to _("Cancel"), admin_labels_path, class: 'btn gl-button btn-cancel'
%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')
- page_title _("Labels")
%div
= link_to new_admin_label_path, class: "float-right btn btn-nr btn-success" do
= link_to new_admin_label_path, class: "float-right btn gl-button btn-nr btn-success" do
= _('New label')
%h3.page-title
= _('Labels')
......
......@@ -4,7 +4,12 @@
= _('Name')
.table-mobile-content
= render 'user_detail', user: user
.table-section.section-25
.table-section.section-10
.table-mobile-header{ role: 'rowheader' }
= _('Projects')
.table-mobile-content.gl-str-truncated{ data: { testid: "user-project-count-#{user.id}" } }
= user.authorized_projects.length
.table-section.section-15
.table-mobile-header{ role: 'rowheader' }
= _('Created on')
.table-mobile-content
......
......@@ -72,7 +72,8 @@
.table-holder
.thead-white.text-nowrap.gl-responsive-table-row.table-row-header{ role: 'row' }
.table-section.section-40{ role: 'rowheader' }= _('Name')
.table-section.section-25{ role: 'rowheader' }= _('Created on')
.table-section.section-10{ role: 'rowheader' }= _('Projects')
.table-section.section-15{ role: 'rowheader' }= _('Created on')
.table-section.section-15{ role: 'rowheader' }= _('Last activity')
= render partial: 'admin/users/user', collection: @users
......
......@@ -12,7 +12,7 @@
= s_('ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project')
= render 'clusters/clusters/buttons'
- if Feature.enabled?(:clusters_list_redesign)
- if Feature.enabled?(:clusters_list_redesign, default_enabled: true)
#js-clusters-list-app{ data: js_clusters_list_data(clusterable.index_path(format: :json)) }
- else
- if @has_ancestor_clusters
......@@ -20,7 +20,7 @@
= s_('ClusterIntegration|Clusters are utilized by selecting the nearest ancestor with a matching environment scope. For example, project clusters will override group clusters.')
%strong
= link_to _('More information'), help_page_path('user/group/clusters/index', anchor: 'cluster-precedence')
.clusters-table.js-clusters-list
.clusters-table.js-clusters-list{ data: { testid: 'cluster_list_table' } }
.gl-responsive-table-row.table-row-header{ role: "row" }
.table-section.section-60{ role: "rowheader" }
= s_("ClusterIntegration|Kubernetes cluster")
......
- if @group.labels.empty?
$('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)
- can_collaborate = can_collaborate_with_project?(@project)
.page-content-header.js-commit-box{ 'data-commit-path' => branches_project_commit_path(@project, @commit.id) }
.page-content-header
.header-main-content
= render partial: 'signature', object: @commit.signature
%strong
......@@ -58,7 +58,7 @@
%pre.commit-description<
= preserve(markdown_field(@commit, :description))
.info-well
.info-well.js-commit-box-info{ 'data-commit-path' => branches_project_commit_path(@project, @commit.id) }
.well-segment.branch-info
.icon-container.commit-icon
= custom_icon("icon_commit")
......
......@@ -28,7 +28,8 @@
= render partial: 'projects/commits/commit', collection: context_commits, locals: { project: project, ref: ref, merge_request: merge_request }
- if hidden > 0
%li.alert.alert-warning
%li.gl-alert.gl-alert-warning
= sprite_icon('warning', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
= n_('%s additional commit has been omitted to prevent performance issues.', '%s additional commits have been omitted to prevent performance issues.', hidden) % number_with_delimiter(hidden)
- if project.context_commits_enabled? && can_update_merge_request && context_commits&.empty?
......
......@@ -7,6 +7,6 @@
%h4.gl-mt-0
Request details
.col-lg-9
= link_to 'Resend Request', @hook_log.present.retry_path, method: :post, class: "btn btn-default float-right gl-ml-3"
= link_to 'Resend Request', @hook_log.present.retry_path, method: :post, class: "btn gl-button btn-default float-right gl-ml-3"
= render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log }
......@@ -15,18 +15,18 @@
.col-md-2.text-center
Markdown
.col-md-10.code.js-syntax-highlight
= highlight_badge('.md', badge.to_markdown, language: 'markdown')
= highlight('.md', badge.to_markdown, language: 'markdown')
.row
%hr
.row
.col-md-2.text-center
HTML
.col-md-10.code.js-syntax-highlight
= highlight_badge('.html', badge.to_html, language: 'html')
= highlight('.html', badge.to_html, language: 'html')
.row
%hr
.row
.col-md-2.text-center
AsciiDoc
.col-md-10.code.js-syntax-highlight
= highlight_badge('.adoc', badge.to_asciidoc)
= highlight('.adoc', badge.to_asciidoc)
- 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'
......
.d-lg-flex.align-items-end
#js-search-filter-by-state{ 'v-cloak': true, data: { scope: @scope, filter: params[:state]} }
- if Feature.enabled?(:search_filter_by_confidential, @group)
#js-search-filter-by-confidential{ 'v-cloak': true, data: { scope: @scope, filter: params[:confidential] } }
- if %w(issues merge_requests).include?(@scope)
%hr.gl-mt-4.gl-mb-4
......@@ -9,5 +9,6 @@
%span.term.str-truncated.gl-font-weight-bold.gl-ml-2= issue.title
.gl-text-gray-500.gl-my-3
= sprintf(s_(' %{project_name}#%{issue_iid} &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
= highlight_and_truncate_issue(issue, @search_term, @search_highlight)
= truncate(issue.description, length: 200)
......@@ -211,6 +211,14 @@
:weight: 1
:idempotent:
:tags: []
- :name: cronjob:member_invitation_reminder_emails
:feature_category: :subgroups
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
:tags: []
- :name: cronjob:metrics_dashboard_schedule_annotations_prune
:feature_category: :metrics
:has_external_dependencies:
......
# frozen_string_literal: true
class MemberInvitationReminderEmailsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :subgroups
urgency :low
def perform
return unless Gitlab::Experimentation.enabled?(:invitation_reminders)
# To keep this MR small, implementation will be done in another MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/42981/diffs?commit_id=8063606e0f83957b2dd38d660ee986f24dee6138
end
end
---
title: Display cluster list node information
merge_request: 42396
author:
type: added
---
title: Surface Alert number GFM reference in highlight bar
merge_request: 42832
author:
type: changed
---
title: Set hook_log css to gl-button
merge_request: 42730
author: Mike Terhar @mterhar
type: other
---
title: Fix edge case when updating snippet with no repo
merge_request: 42964
author:
type: fixed
---
title: Add Gitpod enabled user setting to Usage Data
merge_request: 42570
author:
type: changed
---
title: Global Search - Bold Issue's Search Term
merge_request: 41411
author:
type: changed
---
title: Remove duplicate index on cluster_agents
merge_request: 42902
author:
type: other
---
title: Drop Iglu registry URL column
merge_request: 42939
author:
type: removed
---
title: Display user project count on Admin Dashboard
merge_request: 42871
author:
type: added
---
title: Use fuzzy matching for issuable awards
merge_request: 42674
author: Ethan Reesor (@firelizzard)
type: added
---
title: Move shared logic into utils
merge_request: 42407
author:
type: other
---
title: Allow member mapping to map importer user on Group/Project Import
merge_request: 42882
author:
type: changed
---
title: Fix size of edit button on releases page
merge_request: 42779
author:
type: fixed
---
title: Fix profile scoped label CSS
merge_request: 43005
author:
type: changed
---
name: artifacts_management_page
introduced_by_url:
rollout_issue_url:
group:
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/16654
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/254938
group: group::continuous integration
type: development
default_enabled: false
---
name: clusters_list_redesign
introduced_by_url:
rollout_issue_url:
group:
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/220182
group: Configure
type: development
default_enabled: false
default_enabled: true
---
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
\ No newline at end of file
......@@ -517,6 +517,9 @@ Settings.cron_jobs['ci_platform_metrics_update_cron_worker']['job_class'] = 'CiP
Settings.cron_jobs['analytics_instance_statistics_count_job_trigger_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['analytics_instance_statistics_count_job_trigger_worker']['cron'] ||= '50 23 */1 * *'
Settings.cron_jobs['analytics_instance_statistics_count_job_trigger_worker']['job_class'] ||= 'Analytics::InstanceStatistics::CountJobTriggerWorker'
Settings.cron_jobs['member_invitation_reminder_emails_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['member_invitation_reminder_emails_worker']['cron'] ||= '0 0 * * *'
Settings.cron_jobs['member_invitation_reminder_emails_worker']['job_class'] = 'MemberInvitationReminderEmailsWorker'
Gitlab.ee do
Settings.cron_jobs['adjourned_group_deletion_worker'] ||= Settingslogic.new({})
......
# 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
# 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
# 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
8d14013bcb4d8302c91e331f619fb6f621ab79907aebc421d99c9484ecd7a5d8
\ No newline at end of file
7f62ce5117a16213bad6537dfeae2af4016262c533f8fa6b7a19572077bcf8d7
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment