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 ...@@ -96,4 +96,4 @@ apollo.config.js
/tmp/matching_foss_tests.txt /tmp/matching_foss_tests.txt
/tmp/matching_tests.txt /tmp/matching_tests.txt
ee/changelogs/unreleased-ee ee/changelogs/unreleased-ee
/sitespeed-result
...@@ -885,10 +885,10 @@ GEM ...@@ -885,10 +885,10 @@ GEM
bundler (>= 1.3.0) bundler (>= 1.3.0)
railties (= 6.0.3.1) railties (= 6.0.3.1)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.4) rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.x) actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.x) actionview (>= 5.0.1.rc1)
activesupport (>= 5.0.1.x) activesupport (>= 5.0.1.rc1)
rails-dom-testing (2.0.3) rails-dom-testing (2.0.3)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
nokogiri (>= 1.6) nokogiri (>= 1.6)
......
...@@ -572,7 +572,7 @@ export class AwardsHandler { ...@@ -572,7 +572,7 @@ export class AwardsHandler {
} }
findMatchingEmojiElements(query) { 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 $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]');
const $matchingElements = $emojiElements.filter( const $matchingElements = $emojiElements.filter(
(i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0, (i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0,
......
...@@ -201,7 +201,13 @@ export default { ...@@ -201,7 +201,13 @@ export default {
<section v-else> <section v-else>
<ancestor-notice /> <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 }"> <template #cell(name)="{ item }">
<div :class="[contentAlignClasses, 'js-status']"> <div :class="[contentAlignClasses, 'js-status']">
<img <img
......
<script> <script>
/* eslint-disable vue/no-v-html */ import { GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { GlIcon } from '@gitlab/ui';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import iconBranch from '../svg/icon_branch.svg'; import iconBranch from '../svg/icon_branch.svg';
import limitWarning from './limit_warning_component.vue'; import limitWarning from './limit_warning_component.vue';
...@@ -13,6 +12,9 @@ export default { ...@@ -13,6 +12,9 @@ export default {
limitWarning, limitWarning,
GlIcon, GlIcon,
}, },
directives: {
SafeHtml,
},
props: { props: {
items: { items: {
type: Array, type: Array,
...@@ -47,7 +49,7 @@ export default { ...@@ -47,7 +49,7 @@ export default {
<a :href="build.url" class="pipeline-id"> #{{ build.id }} </a> <a :href="build.url" class="pipeline-id"> #{{ build.id }} </a>
<gl-icon :size="16" name="fork" /> <gl-icon :size="16" name="fork" />
<a :href="build.branch.url" class="ref-name"> {{ build.branch.name }} </a> <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> <a :href="build.commitUrl" class="commit-sha"> {{ build.shortSha }} </a>
</h5> </h5>
<span> <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> <script>
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { import { CONTEXT_LINE_CLASS_NAME } from '../constants';
MATCH_LINE_TYPE,
NEW_LINE_TYPE,
OLD_LINE_TYPE,
CONTEXT_LINE_TYPE,
CONTEXT_LINE_CLASS_NAME,
LINE_POSITION_LEFT,
LINE_POSITION_RIGHT,
LINE_HOVER_CLASS_NAME,
OLD_NO_NEW_LINE_TYPE,
NEW_NO_NEW_LINE_TYPE,
EMPTY_CELL_TYPE,
} from '../constants';
import { __ } from '~/locale';
import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
import DiffGutterAvatars from './diff_gutter_avatars.vue'; import DiffGutterAvatars from './diff_gutter_avatars.vue';
import * as utils from './diff_row_utils';
export default { export default {
components: { components: {
...@@ -61,14 +48,11 @@ export default { ...@@ -61,14 +48,11 @@ export default {
...mapGetters('diffs', ['fileLineCoverage']), ...mapGetters('diffs', ['fileLineCoverage']),
...mapState({ ...mapState({
isHighlighted(state) { isHighlighted(state) {
if (this.isCommented) return true; return utils.isHighlighted(state, this.line, this.isCommented);
const lineCode = this.line.line_code;
return lineCode ? lineCode === state.diffs.highlightedRow : false;
}, },
}), }),
isContextLine() { isContextLine() {
return this.line.type === CONTEXT_LINE_TYPE; return utils.isContextLine(this.line.type);
}, },
classNameMap() { classNameMap() {
return [ return [
...@@ -82,82 +66,48 @@ export default { ...@@ -82,82 +66,48 @@ export default {
return this.line.line_code || `${this.fileHash}_${this.line.old_line}_${this.line.new_line}`; return this.line.line_code || `${this.fileHash}_${this.line.old_line}_${this.line.new_line}`;
}, },
isMatchLine() { isMatchLine() {
return this.line.type === MATCH_LINE_TYPE; return utils.isMatchLine(this.line.type);
}, },
coverageState() { coverageState() {
return this.fileLineCoverage(this.filePath, this.line.new_line); return this.fileLineCoverage(this.filePath, this.line.new_line);
}, },
isMetaLine() { isMetaLine() {
const { type } = this.line; return utils.isMetaLine(this.line.type);
return (
type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE
);
}, },
classNameMapCell() { classNameMapCell() {
const { type } = this.line; return utils.classNameMapCell(this.line, this.isHighlighted, this.isLoggedIn, this.isHover);
return [
type,
{
hll: this.isHighlighted,
[LINE_HOVER_CLASS_NAME]:
this.isLoggedIn && this.isHover && !this.isContextLine && !this.isMetaLine,
},
];
}, },
addCommentTooltip() { addCommentTooltip() {
const brokenSymlinks = this.line.commentsDisabled; return utils.addCommentTooltip(this.line);
let tooltip = __('Add a comment to this line');
if (brokenSymlinks) {
if (brokenSymlinks.wasSymbolic || brokenSymlinks.isSymbolic) {
tooltip = __(
'Commenting on symbolic links that replace or are replaced by files is currently not supported.',
);
} else if (brokenSymlinks.wasReal || brokenSymlinks.isReal) {
tooltip = __(
'Commenting on files that replace or are replaced by symbolic links is currently not supported.',
);
}
}
return tooltip;
}, },
shouldRenderCommentButton() { shouldRenderCommentButton() {
if (this.isLoggedIn) { return utils.shouldRenderCommentButton(
const isDiffHead = parseBoolean(getParameterByName('diff_head')); this.isLoggedIn,
return !isDiffHead || gon.features?.mergeRefHeadComments; true,
} gon.features?.mergeRefHeadComments,
);
return false;
}, },
shouldShowCommentButton() { shouldShowCommentButton() {
return this.isHover && !this.isContextLine && !this.isMetaLine && !this.hasDiscussions; return utils.shouldShowCommentButton(
this.isHover,
this.isContextLine,
this.isMetaLine,
this.hasDiscussions,
);
}, },
hasDiscussions() { hasDiscussions() {
return this.line.discussions && this.line.discussions.length > 0; return utils.hasDiscussions(this.line);
}, },
lineHref() { lineHref() {
return `#${this.line.line_code || ''}`; return utils.lineHref(this.line);
}, },
lineCode() { lineCode() {
return ( return utils.lineCode(this.line);
this.line.line_code ||
(this.line.left && this.line.left.line_code) ||
(this.line.right && this.line.right.line_code)
);
}, },
shouldShowAvatarsOnGutter() { shouldShowAvatarsOnGutter() {
return this.hasDiscussions; return this.hasDiscussions;
}, },
}, },
created() {
this.newLineType = NEW_LINE_TYPE;
this.oldLineType = OLD_LINE_TYPE;
this.linePositionLeft = LINE_POSITION_LEFT;
this.linePositionRight = LINE_POSITION_RIGHT;
},
mounted() { mounted() {
this.scrollToLineIfNeededInline(this.line); this.scrollToLineIfNeededInline(this.line);
}, },
...@@ -242,6 +192,7 @@ export default { ...@@ -242,6 +192,7 @@ export default {
class="line-coverage" class="line-coverage"
></td> ></td>
<td <td
:key="line.line_code"
v-safe-html="line.rich_text" v-safe-html="line.rich_text"
:class="[ :class="[
line.type, line.type,
......
...@@ -2,21 +2,9 @@ ...@@ -2,21 +2,9 @@
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import $ from 'jquery'; import $ from 'jquery';
import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { import { CONTEXT_LINE_CLASS_NAME, PARALLEL_DIFF_VIEW_TYPE } from '../constants';
MATCH_LINE_TYPE,
NEW_LINE_TYPE,
OLD_LINE_TYPE,
CONTEXT_LINE_TYPE,
CONTEXT_LINE_CLASS_NAME,
OLD_NO_NEW_LINE_TYPE,
PARALLEL_DIFF_VIEW_TYPE,
NEW_NO_NEW_LINE_TYPE,
EMPTY_CELL_TYPE,
LINE_HOVER_CLASS_NAME,
} from '../constants';
import { __ } from '~/locale';
import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
import DiffGutterAvatars from './diff_gutter_avatars.vue'; import DiffGutterAvatars from './diff_gutter_avatars.vue';
import * as utils from './diff_row_utils';
export default { export default {
components: { components: {
...@@ -63,20 +51,15 @@ export default { ...@@ -63,20 +51,15 @@ export default {
...mapGetters(['isLoggedIn']), ...mapGetters(['isLoggedIn']),
...mapState({ ...mapState({
isHighlighted(state) { isHighlighted(state) {
if (this.isCommented) return true; const line = this.line.left?.line_code ? this.line.left : this.line.right;
return utils.isHighlighted(state, line, this.isCommented);
const lineCode =
(this.line.left && this.line.left.line_code) ||
(this.line.right && this.line.right.line_code);
return lineCode ? lineCode === state.diffs.highlightedRow : false;
}, },
}), }),
isContextLineLeft() { isContextLineLeft() {
return this.line.left && this.line.left.type === CONTEXT_LINE_TYPE; return utils.isContextLine(this.line.left?.type);
}, },
isContextLineRight() { isContextLineRight() {
return this.line.right && this.line.right.type === CONTEXT_LINE_TYPE; return utils.isContextLine(this.line.right?.type);
}, },
classNameMap() { classNameMap() {
return { return {
...@@ -85,157 +68,84 @@ export default { ...@@ -85,157 +68,84 @@ export default {
}; };
}, },
parallelViewLeftLineType() { parallelViewLeftLineType() {
if (this.line.right && this.line.right.type === NEW_NO_NEW_LINE_TYPE) { return utils.parallelViewLeftLineType(this.line, this.isHighlighted);
return OLD_NO_NEW_LINE_TYPE;
}
const lineTypeClass = this.line.left ? this.line.left.type : EMPTY_CELL_TYPE;
return [
lineTypeClass,
{
hll: this.isHighlighted,
},
];
}, },
isMatchLineLeft() { isMatchLineLeft() {
return this.line.left && this.line.left.type === MATCH_LINE_TYPE; return utils.isMatchLine(this.line.left?.type);
}, },
isMatchLineRight() { isMatchLineRight() {
return this.line.right && this.line.right.type === MATCH_LINE_TYPE; return utils.isMatchLine(this.line.right?.type);
}, },
coverageState() { coverageState() {
return this.fileLineCoverage(this.filePath, this.line.right.new_line); return this.fileLineCoverage(this.filePath, this.line.right.new_line);
}, },
classNameMapCellLeft() { classNameMapCellLeft() {
const { type } = this.line.left; return utils.classNameMapCell(
this.line.left,
return [ this.isHighlighted,
type, this.isLoggedIn,
{ this.isLeftHover,
hll: this.isHighlighted, );
[LINE_HOVER_CLASS_NAME]:
this.isLoggedIn && this.isLeftHover && !this.isContextLineLeft && !this.isMetaLineLeft,
},
];
}, },
classNameMapCellRight() { classNameMapCellRight() {
const { type } = this.line.right; return utils.classNameMapCell(
this.line.right,
return [ this.isHighlighted,
type, this.isLoggedIn,
{ this.isRightHover,
hll: this.isHighlighted, );
[LINE_HOVER_CLASS_NAME]:
this.isLoggedIn &&
this.isRightHover &&
!this.isContextLineRight &&
!this.isMetaLineRight,
},
];
}, },
addCommentTooltipLeft() { addCommentTooltipLeft() {
const brokenSymlinks = this.line.left.commentsDisabled; return utils.addCommentTooltip(this.line.left);
let tooltip = __('Add a comment to this line');
if (brokenSymlinks) {
if (brokenSymlinks.wasSymbolic || brokenSymlinks.isSymbolic) {
tooltip = __(
'Commenting on symbolic links that replace or are replaced by files is currently not supported.',
);
} else if (brokenSymlinks.wasReal || brokenSymlinks.isReal) {
tooltip = __(
'Commenting on files that replace or are replaced by symbolic links is currently not supported.',
);
}
}
return tooltip;
}, },
addCommentTooltipRight() { addCommentTooltipRight() {
const brokenSymlinks = this.line.right.commentsDisabled; return utils.addCommentTooltip(this.line.right);
let tooltip = __('Add a comment to this line');
if (brokenSymlinks) {
if (brokenSymlinks.wasSymbolic || brokenSymlinks.isSymbolic) {
tooltip = __(
'Commenting on symbolic links that replace or are replaced by files is currently not supported.',
);
} else if (brokenSymlinks.wasReal || brokenSymlinks.isReal) {
tooltip = __(
'Commenting on files that replace or are replaced by symbolic links is currently not supported.',
);
}
}
return tooltip;
}, },
shouldRenderCommentButton() { shouldRenderCommentButton() {
if (!this.isCommentButtonRendered) { return utils.shouldRenderCommentButton(
return false; this.isLoggedIn,
} this.isCommentButtonRendered,
gon.features?.mergeRefHeadComments,
if (this.isLoggedIn) { );
const isDiffHead = parseBoolean(getParameterByName('diff_head'));
return !isDiffHead || gon.features?.mergeRefHeadComments;
}
return false;
}, },
shouldShowCommentButtonLeft() { shouldShowCommentButtonLeft() {
return ( return utils.shouldShowCommentButton(
this.isLeftHover && this.isLeftHover,
!this.isContextLineLeft && this.isContextLineLeft,
!this.isMetaLineLeft && this.isMetaLineLeft,
!this.hasDiscussionsLeft this.hasDiscussionsLeft,
); );
}, },
shouldShowCommentButtonRight() { shouldShowCommentButtonRight() {
return ( return utils.shouldShowCommentButton(
this.isRightHover && this.isRightHover,
!this.isContextLineRight && this.isContextLineRight,
!this.isMetaLineRight && this.isMetaLineRight,
!this.hasDiscussionsRight this.hasDiscussionsRight,
); );
}, },
hasDiscussionsLeft() { hasDiscussionsLeft() {
return this.line.left?.discussions?.length > 0; return utils.hasDiscussions(this.line.left);
}, },
hasDiscussionsRight() { hasDiscussionsRight() {
return this.line.right?.discussions?.length > 0; return utils.hasDiscussions(this.line.right);
}, },
lineHrefOld() { lineHrefOld() {
return `#${this.line.left.line_code || ''}`; return utils.lineHref(this.line.left);
}, },
lineHrefNew() { lineHrefNew() {
return `#${this.line.right.line_code || ''}`; return utils.lineHref(this.line.right);
}, },
lineCode() { lineCode() {
return ( return utils.lineCode(this.line);
(this.line.left && this.line.left.line_code) ||
(this.line.right && this.line.right.line_code)
);
}, },
isMetaLineLeft() { isMetaLineLeft() {
const type = this.line.left?.type; return utils.isMetaLine(this.line.left?.type);
return (
type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE
);
}, },
isMetaLineRight() { isMetaLineRight() {
const type = this.line.right?.type; return utils.isMetaLine(this.line.right?.type);
return (
type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE
);
}, },
}, },
created() {
this.newLineType = NEW_LINE_TYPE;
this.oldLineType = OLD_LINE_TYPE;
this.parallelDiffViewType = PARALLEL_DIFF_VIEW_TYPE;
},
mounted() { mounted() {
this.scrollToLineIfNeededParallel(this.line); this.scrollToLineIfNeededParallel(this.line);
this.unwatchShouldShowCommentButton = this.$watch( this.unwatchShouldShowCommentButton = this.$watch(
...@@ -341,6 +251,7 @@ export default { ...@@ -341,6 +251,7 @@ export default {
<td :class="parallelViewLeftLineType" class="line-coverage left-side"></td> <td :class="parallelViewLeftLineType" class="line-coverage left-side"></td>
<td <td
:id="line.left.line_code" :id="line.left.line_code"
:key="line.left.line_code"
v-safe-html="line.left.rich_text" v-safe-html="line.left.rich_text"
:class="parallelViewLeftLineType" :class="parallelViewLeftLineType"
class="line_content with-coverage parallel left-side" class="line_content with-coverage parallel left-side"
...@@ -401,6 +312,7 @@ export default { ...@@ -401,6 +312,7 @@ export default {
></td> ></td>
<td <td
:id="line.right.line_code" :id="line.right.line_code"
:key="line.right.rich_text"
v-safe-html="line.right.rich_text" v-safe-html="line.right.rich_text"
:class="[ :class="[
line.right.type, line.right.type,
......
import { uniq } from 'lodash'; import { uniq } from 'lodash';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import emojiAliases from 'emojis/aliases.json'; import emojiAliases from 'emojis/aliases.json';
import axios from '../lib/utils/axios_utils'; import axios from '../lib/utils/axios_utils';
...@@ -62,13 +63,18 @@ export function isEmojiNameValid(name) { ...@@ -62,13 +63,18 @@ export function isEmojiNameValid(name) {
return validEmojiNames.indexOf(name) >= 0; return validEmojiNames.indexOf(name) >= 0;
} }
export function filterEmojiNames(filter) { /**
const match = filter.toLowerCase(); * Search emoji by name or alias. Returns a normalized, deduplicated list of
return validEmojiNames.filter(name => name.indexOf(match) >= 0); * names.
} *
* Calling with an empty filter returns an empty array.
export function filterEmojiNamesByAlias(filter) { *
return uniq(filterEmojiNames(filter).map(name => normalizeEmojiName(name))); * @param {String}
* @returns {Array}
*/
export function queryEmojiNames(filter) {
const matches = fuzzaldrinPlus.filter(validEmojiNames, filter);
return uniq(matches.map(name => normalizeEmojiName(name)));
} }
let emojiCategoryMap; let emojiCategoryMap;
......
import { take } from 'lodash'; import { take } from 'lodash';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; 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'; import { FREQUENT_ITEMS, HOUR_IN_MS } from './constants';
export const isMobile = () => ['md', 'sm', 'xs'].includes(bp.getBreakpointSize()); export const isMobile = () => ['md', 'sm', 'xs'].includes(bp.getBreakpointSize());
......
<script> <script>
import { GlLink } from '@gitlab/ui'; import { GlLink, GlTooltipDirective, GlSprintf } from '@gitlab/ui';
import { formatDate } from '~/lib/utils/datetime_utility'; import { formatDate } from '~/lib/utils/datetime_utility';
export default { export default {
components: { components: {
GlLink, GlLink,
GlSprintf,
},
directives: {
GlTooltip: GlTooltipDirective,
}, },
props: { props: {
alert: { alert: {
...@@ -24,17 +28,23 @@ export default { ...@@ -24,17 +28,23 @@ export default {
<div <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" 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> <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>
<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> <span class="gl-font-weight-bold">{{ s__('HighlightBar|Alert start time:') }}</span>
{{ startTime }} {{ startTime }}
</div> </div>
<div class="gl-white-space-nowrap"> <div>
<span class="gl-font-weight-bold">{{ s__('HighlightBar|Alert events:') }}</span> <span class="gl-font-weight-bold">{{ s__('HighlightBar|Alert events:') }}</span>
<span>{{ alert.eventCount }}</span> <span>{{ alert.eventCount }}</span>
</div> </div>
......
import { sanitize } from 'dompurify'; import { sanitize } from '~/lib/dompurify';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import updateDescription from '../utils/update_description'; 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 // We currently load + parse the data from the issue app and related merge request
let cachedParsedData; 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 fuzzaldrinPlus from 'fuzzaldrin-plus';
import { sanitize } from 'dompurify'; import { sanitize } from '~/lib/dompurify';
/** /**
* Wraps substring matches with HTML `<span>` elements. * Wraps substring matches with HTML `<span>` elements.
......
<script> <script>
/* eslint-disable vue/no-v-html */ /* eslint-disable vue/no-v-html */
import marked from 'marked'; import marked from 'marked';
import { sanitize } from 'dompurify';
import katex from 'katex'; import katex from 'katex';
import { sanitize } from '~/lib/dompurify';
import Prompt from './prompt.vue'; import Prompt from './prompt.vue';
const renderer = new marked.Renderer(); const renderer = new marked.Renderer();
......
<script> <script>
/* eslint-disable vue/no-v-html */ /* eslint-disable vue/no-v-html */
import { sanitize } from 'dompurify'; import { sanitize } from '~/lib/dompurify';
import Prompt from '../prompt.vue'; import Prompt from '../prompt.vue';
export default { export default {
......
import $ from 'jquery'; import { initCommitBoxInfo } from '~/projects/commit_box/info';
import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
import initPipelines from '~/commit/pipelines/pipelines_bundle'; import initPipelines from '~/commit/pipelines/pipelines_bundle';
import { fetchCommitMergeRequests } from '~/commit_merge_requests';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new MiniPipelineGraph({ initCommitBoxInfo();
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();
initPipelines(); initPipelines();
}); });
...@@ -4,10 +4,8 @@ import $ from 'jquery'; ...@@ -4,10 +4,8 @@ import $ from 'jquery';
import Diff from '~/diff'; import Diff from '~/diff';
import ZenMode from '~/zen_mode'; import ZenMode from '~/zen_mode';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
import initNotes from '~/init_notes'; import initNotes from '~/init_notes';
import initChangesDropdown from '~/init_changes_dropdown'; import initChangesDropdown from '~/init_changes_dropdown';
import { fetchCommitMergeRequests } from '~/commit_merge_requests';
import '~/sourcegraph/load'; import '~/sourcegraph/load';
import { handleLocationHash } from '~/lib/utils/common_utils'; import { handleLocationHash } from '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
...@@ -15,6 +13,7 @@ import syntaxHighlight from '~/syntax_highlight'; ...@@ -15,6 +13,7 @@ import syntaxHighlight from '~/syntax_highlight';
import flash from '~/flash'; import flash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import loadAwardsHandler from '~/awards_handler'; import loadAwardsHandler from '~/awards_handler';
import { initCommitBoxInfo } from '~/projects/commit_box/info';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const hasPerfBar = document.querySelector('.with-performance-bar'); const hasPerfBar = document.querySelector('.with-performance-bar');
...@@ -22,13 +21,10 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -22,13 +21,10 @@ document.addEventListener('DOMContentLoaded', () => {
initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight + performanceHeight); initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight + performanceHeight);
new ZenMode(); new ZenMode();
new ShortcutsNavigation(); new ShortcutsNavigation();
new MiniPipelineGraph({
container: '.js-commit-pipeline-graph', initCommitBoxInfo();
}).bindEvents();
initNotes(); 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'); const filesContainer = $('.js-diffs-batch');
......
import Search from './search'; import Search from './search';
import initStateFilter from '~/search/state_filter'; import initStateFilter from '~/search/state_filter';
import initConfidentialFilter from '~/search/confidential_filter';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initStateFilter(); initStateFilter();
initConfidentialFilter();
return new Search(); return new Search();
}); });
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import $ from 'jquery'; import $ from 'jquery';
import fuzzaldrinPlus from 'fuzzaldrin-plus'; import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { sanitize } from 'dompurify'; import { sanitize } from '~/lib/dompurify';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility'; import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import { deprecatedCreateFlash as flash } from '~/flash'; 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> <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 { BACK_URL_PARAM } from '~/releases/constants';
import { setUrlParams } from '~/lib/utils/url_utility'; import { setUrlParams } from '~/lib/utils/url_utility';
...@@ -8,7 +8,6 @@ export default { ...@@ -8,7 +8,6 @@ export default {
components: { components: {
GlLink, GlLink,
GlBadge, GlBadge,
GlIcon,
GlButton, GlButton,
}, },
directives: { directives: {
...@@ -55,11 +54,10 @@ export default { ...@@ -55,11 +54,10 @@ export default {
v-gl-tooltip v-gl-tooltip
category="primary" category="primary"
variant="default" variant="default"
icon="pencil"
class="gl-mr-3 js-edit-button ml-2 pb-2" class="gl-mr-3 js-edit-button ml-2 pb-2"
:title="__('Edit this release')" :title="__('Edit this release')"
:href="editLink" :href="editLink"
> />
<gl-icon name="pencil" />
</gl-button>
</div> </div>
</template> </template>
<script> <script>
import { GlDropdown, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui'; 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'; import { setUrlParams, visitUrl } from '~/lib/utils/url_utility';
import { sprintf, s__ } from '~/locale';
const FILTERS_ARRAY = Object.values(FILTER_STATES);
export default { export default {
name: 'StateFilter', name: 'DropdownFilter',
components: { components: {
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlDropdownDivider, GlDropdownDivider,
}, },
props: { props: {
scope: { initialFilter: {
type: String, type: String,
required: false,
default: null,
},
filters: {
type: Object,
required: true, required: true,
}, },
state: { filtersArray: {
type: Array,
required: true,
},
header: {
type: String, type: String,
required: false, required: true,
default: FILTER_STATES.ANY.value, },
validator: v => FILTERS_ARRAY.some(({ value }) => value === v), param: {
type: String,
required: true,
},
scope: {
type: String,
required: true,
},
supportedScopes: {
type: Array,
required: true,
}, },
}, },
computed: { computed: {
filter() {
return this.initialFilter || this.filters.ANY.value;
},
selectedFilterText() { selectedFilterText() {
const filter = FILTERS_ARRAY.find(({ value }) => value === this.selectedFilter); const f = this.filtersArray.find(({ value }) => value === this.selectedFilter);
if (!filter || filter === FILTER_STATES.ANY) { if (!f || f === this.filters.ANY) {
return FILTER_TEXT; return sprintf(s__('Any %{header}'), { header: this.header });
} }
return filter.label; return f.label;
}, },
showDropdown() { showDropdown() {
return Object.values(SCOPES).includes(this.scope); return this.supportedScopes.includes(this.scope);
}, },
selectedFilter: { selectedFilter: {
get() { get() {
if (FILTERS_ARRAY.some(({ value }) => value === this.state)) { if (this.filtersArray.some(({ value }) => value === this.filter)) {
return this.state; return this.filter;
} }
return FILTER_STATES.ANY.value; return this.filters.ANY.value;
}, },
set(state) { set(filter) {
visitUrl(setUrlParams({ state })); visitUrl(setUrlParams({ [this.param]: filter }));
}, },
}, },
}, },
...@@ -59,36 +73,39 @@ export default { ...@@ -59,36 +73,39 @@ export default {
dropDownItemClass(filter) { dropDownItemClass(filter) {
return { return {
'gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2! gl-mb-2': '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) { isFilterSelected(filter) {
return filter === this.selectedFilter; return filter === this.selectedFilter;
}, },
handleFilterChange(state) { handleFilterChange(filter) {
this.selectedFilter = state; this.selectedFilter = filter;
}, },
}, },
filterStates: FILTER_STATES,
filterHeader: FILTER_HEADER,
filtersByScope: FILTER_STATES_BY_SCOPE,
}; };
</script> </script>
<template> <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"> <header class="gl-text-center gl-font-weight-bold gl-font-lg">
{{ $options.filterHeader }} {{ header }}
</header> </header>
<gl-dropdown-divider /> <gl-dropdown-divider />
<gl-dropdown-item <gl-dropdown-item
v-for="filter in $options.filtersByScope[scope]" v-for="f in filtersArray"
:key="filter.value" :key="f.value"
:is-check-item="true" :is-check-item="true"
:is-checked="isFilterSelected(filter.value)" :is-checked="isFilterSelected(f.value)"
:class="dropDownItemClass(filter)" :class="dropDownItemClass(f)"
@click="handleFilterChange(filter.value)" @click="handleFilterChange(f.value)"
>{{ filter.label }}</gl-dropdown-item
> >
{{ f.label }}
</gl-dropdown-item>
</gl-dropdown> </gl-dropdown>
</template> </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'; ...@@ -2,8 +2,6 @@ import { __ } from '~/locale';
export const FILTER_HEADER = __('Status'); export const FILTER_HEADER = __('Status');
export const FILTER_TEXT = __('Any Status');
export const FILTER_STATES = { export const FILTER_STATES = {
ANY: { ANY: {
label: __('Any'), label: __('Any'),
...@@ -37,3 +35,5 @@ export const FILTER_STATES_BY_SCOPE = { ...@@ -37,3 +35,5 @@ export const FILTER_STATES_BY_SCOPE = {
FILTER_STATES.CLOSED, FILTER_STATES.CLOSED,
], ],
}; };
export const FILTER_PARAM = 'state';
import Vue from 'vue'; import Vue from 'vue';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import StateFilter from './components/state_filter.vue'; import DropdownFilter from '../components/dropdown_filter.vue';
import {
FILTER_HEADER,
FILTER_PARAM,
FILTER_STATES_BY_SCOPE,
FILTER_STATES,
SCOPES,
} from './constants';
Vue.use(Translate); Vue.use(Translate);
...@@ -11,22 +18,20 @@ export default () => { ...@@ -11,22 +18,20 @@ export default () => {
return new Vue({ return new Vue({
el, el,
components: {
StateFilter,
},
data() { data() {
const { dataset } = this.$options.el; return { ...el.dataset };
return {
scope: dataset.scope,
state: dataset.state,
};
}, },
render(createElement) { render(createElement) {
return createElement('state-filter', { return createElement(DropdownFilter, {
props: { props: {
initialFilter: this.filter,
filtersArray: FILTER_STATES_BY_SCOPE[this.scope],
filters: FILTER_STATES,
header: FILTER_HEADER,
param: FILTER_PARAM,
scope: this.scope, scope: this.scope,
state: this.state, supportedScopes: Object.values(SCOPES),
}, },
}); });
}, },
......
import Vue from 'vue'; import Vue from 'vue';
import { sanitize } from 'dompurify'; import { sanitize } from '~/lib/dompurify';
import UsersCache from './lib/utils/users_cache'; import UsersCache from './lib/utils/users_cache';
import UserPopover from './vue_shared/components/user_popover/user_popover.vue'; import UserPopover from './vue_shared/components/user_popover/user_popover.vue';
......
...@@ -112,8 +112,7 @@ a { ...@@ -112,8 +112,7 @@ a {
} }
.dropdown-menu a, .dropdown-menu a,
.dropdown-menu button, .dropdown-menu button {
.dropdown-menu-nav a {
transition: none; transition: none;
} }
......
...@@ -33,8 +33,7 @@ ...@@ -33,8 +33,7 @@
} }
.show.dropdown { .show.dropdown {
.dropdown-menu, .dropdown-menu {
.dropdown-menu-nav {
@include set-visible; @include set-visible;
min-height: $dropdown-min-height; min-height: $dropdown-min-height;
max-height: $dropdown-max-height; max-height: $dropdown-max-height;
...@@ -258,8 +257,7 @@ ...@@ -258,8 +257,7 @@
} }
} }
.dropdown-menu, .dropdown-menu {
.dropdown-menu-nav {
display: none; display: none;
position: absolute; position: absolute;
width: auto; width: auto;
...@@ -393,49 +391,56 @@ ...@@ -393,49 +391,56 @@
pointer-events: none; pointer-events: none;
} }
.dropdown-menu li { .dropdown-menu {
cursor: pointer; display: none;
opacity: 1;
visibility: visible;
transform: translateY(0);
&.droplab-item-active button { li {
@include dropdown-item-hover; cursor: pointer;
}
> a, &.droplab-item-active button {
> button { @include dropdown-item-hover;
display: flex; }
margin: 0;
text-overflow: inherit;
text-align: left;
&.btn .fa:not(:last-child) { > a,
margin-left: 5px; > button {
display: flex;
margin: 0;
text-overflow: inherit;
text-align: left;
&.btn .fa:not(:last-child) {
margin-left: 5px;
}
} }
}
> button.dropdown-epic-button { > button.dropdown-epic-button {
flex-direction: column; flex-direction: column;
.reference { .reference {
color: $gray-300; color: $gray-300;
margin-top: $gl-padding-4; margin-top: $gl-padding-4;
}
} }
}
&.droplab-item-selected i { &.droplab-item-selected i {
visibility: visible; visibility: visible;
} }
.icon { .icon {
visibility: hidden; visibility: hidden;
} }
.description { .description {
display: inline-block; display: inline-block;
white-space: normal; white-space: normal;
margin-left: 5px; margin-left: 5px;
p { p {
margin-bottom: 0; margin-bottom: 0;
}
} }
} }
} }
...@@ -447,21 +452,12 @@ ...@@ -447,21 +452,12 @@
} }
} }
.droplab-dropdown .dropdown-menu,
.droplab-dropdown .dropdown-menu-nav {
display: none;
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.comment-type-dropdown.show .dropdown-menu { .comment-type-dropdown.show .dropdown-menu {
display: block; display: block;
} }
.filtered-search-box-input-container { .filtered-search-box-input-container {
.dropdown-menu, .dropdown-menu {
.dropdown-menu-nav {
max-width: 280px; max-width: 280px;
} }
} }
...@@ -850,8 +846,7 @@ ...@@ -850,8 +846,7 @@
} }
header.navbar-gitlab .dropdown { header.navbar-gitlab .dropdown {
.dropdown-menu, .dropdown-menu {
.dropdown-menu-nav {
width: 100%; width: 100%;
min-width: 100%; min-width: 100%;
} }
......
...@@ -227,6 +227,10 @@ ...@@ -227,6 +227,10 @@
padding-left: 40px; padding-left: 40px;
} }
.gl-label-scoped {
--label-inset-border: inset 0 0 0 1px currentColor;
}
@include media-breakpoint-up(lg) { @include media-breakpoint-up(lg) {
margin-right: 5px; margin-right: 5px;
} }
......
...@@ -9,6 +9,7 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -9,6 +9,7 @@ class Admin::UsersController < Admin::ApplicationController
def index def index
@users = User.filter_items(params[:filter]).order_name_asc @users = User.filter_items(params[:filter]).order_name_asc
@users = @users.search_with_secondary_emails(params[:search_query]) if params[:search_query].present? @users = @users.search_with_secondary_emails(params[:search_query]) if params[:search_query].present?
@users = @users.includes(:authorized_projects) # rubocop: disable CodeReuse/ActiveRecord
@users = @users.sort_by_attribute(@sort = params[:sort]) @users = @users.sort_by_attribute(@sort = params[:sort])
@users = @users.page(params[:page]) @users = @users.page(params[:page])
end end
......
...@@ -60,13 +60,7 @@ class Groups::LabelsController < Groups::ApplicationController ...@@ -60,13 +60,7 @@ class Groups::LabelsController < Groups::ApplicationController
def destroy def destroy
@label.destroy @label.destroy
redirect_to group_labels_path(@group), status: :found, notice: "#{@label.name} deleted permanently"
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 end
protected protected
......
...@@ -11,8 +11,6 @@ module Projects ...@@ -11,8 +11,6 @@ module Projects
push_frontend_feature_flag(:ci_key_autocomplete, default_enabled: true) push_frontend_feature_flag(:ci_key_autocomplete, default_enabled: true)
end end
helper_method :highlight_badge
def show def show
end end
...@@ -52,10 +50,6 @@ module Projects ...@@ -52,10 +50,6 @@ module Projects
private private
def highlight_badge(name, content, language = nil)
Gitlab::Highlight.highlight(name, content, language: language)
end
def update_params def update_params
params.require(:project).permit(*permitted_project_params) params.require(:project).permit(*permitted_project_params)
end end
......
...@@ -38,7 +38,6 @@ class SearchController < ApplicationController ...@@ -38,7 +38,6 @@ class SearchController < ApplicationController
@show_snippets = search_service.show_snippets? @show_snippets = search_service.show_snippets?
@search_results = search_service.search_results @search_results = search_service.search_results
@search_objects = search_service.search_objects(preload_method) @search_objects = search_service.search_objects(preload_method)
@search_highlight = search_service.search_highlight
render_commits if @scope == 'commits' render_commits if @scope == 'commits'
eager_load_user_status if @scope == 'users' eager_load_user_status if @scope == 'users'
......
# frozen_string_literal: true # frozen_string_literal: true
module BlobHelper module BlobHelper
def highlight(file_name, file_content, language: nil, plain: false)
highlighted = Gitlab::Highlight.highlight(file_name, file_content, plain: plain, language: language)
raw %(<pre class="code highlight"><code>#{highlighted}</code></pre>)
end
def no_highlight_files def no_highlight_files
%w(credits changelog news copying copyright license authors) %w(credits changelog news copying copyright license authors)
end end
......
...@@ -299,13 +299,6 @@ module SearchHelper ...@@ -299,13 +299,6 @@ module SearchHelper
simple_search_highlight_and_truncate(issue.description, search_term, highlighter: '<span class="gl-text-black-normal gl-font-weight-bold">\1</span>') simple_search_highlight_and_truncate(issue.description, search_term, highlighter: '<span class="gl-text-black-normal gl-font-weight-bold">\1</span>')
end end
def simple_search_highlight_and_truncate(text, phrase, options = {})
truncate_length = options.delete(:length) { 200 }
text = truncate(text, length: truncate_length)
phrase = phrase.split
highlight(text, phrase, options)
end
def show_user_search_tab? def show_user_search_tab?
return false if Feature.disabled?(:users_search, default_enabled: true) return false if Feature.disabled?(:users_search, default_enabled: true)
......
...@@ -1302,6 +1302,14 @@ class MergeRequest < ApplicationRecord ...@@ -1302,6 +1302,14 @@ class MergeRequest < ApplicationRecord
unlock_mr unlock_mr
end end
def update_and_mark_in_progress_merge_commit_sha(commit_id)
self.update(in_progress_merge_commit_sha: commit_id)
# Since another process checks for matching merge request, we need
# to make it possible to detect whether the query should go to the
# primary.
target_project.mark_primary_write_location
end
def diverged_commits_count def diverged_commits_count
cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits") cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits")
......
...@@ -2292,6 +2292,10 @@ class Project < ApplicationRecord ...@@ -2292,6 +2292,10 @@ class Project < ApplicationRecord
[] []
end end
def mark_primary_write_location
# Overriden in EE
end
def toggle_ci_cd_settings!(settings_attribute) def toggle_ci_cd_settings!(settings_attribute)
ci_cd_settings.toggle!(settings_attribute) ci_cd_settings.toggle!(settings_attribute)
end end
......
...@@ -853,7 +853,7 @@ class Repository ...@@ -853,7 +853,7 @@ class Repository
def merge(user, source_sha, merge_request, message) def merge(user, source_sha, merge_request, message)
with_cache_hooks do with_cache_hooks do
raw_repository.merge(user, source_sha, merge_request.target_branch, message) do |commit_id| raw_repository.merge(user, source_sha, merge_request.target_branch, message) do |commit_id|
merge_request.update(in_progress_merge_commit_sha: commit_id) merge_request.update_and_mark_in_progress_merge_commit_sha(commit_id)
nil # Return value does not matter. nil # Return value does not matter.
end end
end end
...@@ -873,7 +873,7 @@ class Repository ...@@ -873,7 +873,7 @@ class Repository
their_commit_id = commit(source)&.id their_commit_id = commit(source)&.id
raise 'Invalid merge source' if their_commit_id.nil? raise 'Invalid merge source' if their_commit_id.nil?
merge_request&.update(in_progress_merge_commit_sha: their_commit_id) merge_request&.update_and_mark_in_progress_merge_commit_sha(their_commit_id)
with_cache_hooks { raw.ff_merge(user, their_commit_id, target_branch) } with_cache_hooks { raw.ff_merge(user, their_commit_id, target_branch) }
end end
......
...@@ -8,6 +8,9 @@ class UserPreference < ApplicationRecord ...@@ -8,6 +8,9 @@ class UserPreference < ApplicationRecord
belongs_to :user belongs_to :user
scope :with_user, -> { joins(:user) }
scope :gitpod_enabled, -> { where(gitpod_enabled: true) }
validates :issue_notes_filter, :merge_request_notes_filter, inclusion: { in: NOTES_FILTERS.values }, presence: true validates :issue_notes_filter, :merge_request_notes_filter, inclusion: { in: NOTES_FILTERS.values }, presence: true
validates :tab_width, numericality: { validates :tab_width, numericality: {
only_integer: true, only_integer: true,
......
...@@ -27,7 +27,7 @@ module MergeRequests ...@@ -27,7 +27,7 @@ module MergeRequests
rescue StandardError => e rescue StandardError => e
raise MergeError, "Something went wrong during merge: #{e.message}" raise MergeError, "Something went wrong during merge: #{e.message}"
ensure ensure
merge_request.update(in_progress_merge_commit_sha: nil) merge_request.update_and_mark_in_progress_merge_commit_sha(nil)
end end
end end
end end
...@@ -84,7 +84,7 @@ module MergeRequests ...@@ -84,7 +84,7 @@ module MergeRequests
merge_request.update!(merge_commit_sha: commit_id) merge_request.update!(merge_commit_sha: commit_id)
ensure ensure
merge_request.update_column(:in_progress_merge_commit_sha, nil) merge_request.update_and_mark_in_progress_merge_commit_sha(nil)
end end
def try_merge def try_merge
......
...@@ -65,10 +65,6 @@ class SearchService ...@@ -65,10 +65,6 @@ class SearchService
@search_objects ||= redact_unauthorized_results(search_results.objects(scope, page: params[:page], per_page: per_page, preload_method: preload_method)) @search_objects ||= redact_unauthorized_results(search_results.objects(scope, page: params[:page], per_page: per_page, preload_method: preload_method))
end end
def search_highlight
search_results.highlight_map(scope)
end
private private
def per_page def per_page
......
...@@ -4,6 +4,9 @@ module Snippets ...@@ -4,6 +4,9 @@ module Snippets
class BaseService < ::BaseService class BaseService < ::BaseService
include SpamCheckMethods include SpamCheckMethods
UPDATE_COMMIT_MSG = 'Update snippet'
INITIAL_COMMIT_MSG = 'Initial commit'
CreateRepositoryError = Class.new(StandardError) CreateRepositoryError = Class.new(StandardError)
attr_reader :uploaded_assets, :snippet_actions attr_reader :uploaded_assets, :snippet_actions
...@@ -85,5 +88,20 @@ module Snippets ...@@ -85,5 +88,20 @@ module Snippets
def restricted_files_actions def restricted_files_actions
nil nil
end 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
end end
...@@ -59,7 +59,7 @@ module Snippets ...@@ -59,7 +59,7 @@ module Snippets
log_error(e.message) log_error(e.message)
# If the commit action failed we need to remove the repository if exists # 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 # If the snippet was created, we need to remove it as we
# would do like if it had had any validation error # would do like if it had had any validation error
...@@ -81,12 +81,9 @@ module Snippets ...@@ -81,12 +81,9 @@ module Snippets
end end
def create_commit def create_commit
commit_attrs = { attrs = commit_attrs(@snippet, INITIAL_COMMIT_MSG)
branch_name: @snippet.default_branch,
message: 'Initial commit'
}
@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 end
def move_temporary_files def move_temporary_files
......
...@@ -37,7 +37,10 @@ module Snippets ...@@ -37,7 +37,10 @@ module Snippets
# is implemented. # is implemented.
# Once we can perform different operations through this service # Once we can perform different operations through this service
# we won't need to keep track of the `content` and `file_name` fields # 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[:content] = snippet_actions[0].content if snippet_actions[0].content
params[:file_name] = snippet_actions[0].file_path params[:file_name] = snippet_actions[0].file_path
end end
...@@ -52,7 +55,11 @@ module Snippets ...@@ -52,7 +55,11 @@ module Snippets
# the repository we can just return # the repository we can just return
return true unless committable_attributes? return true unless committable_attributes?
create_repository_for(snippet) unless snippet.repository_exists?
create_repository_for(snippet)
create_first_commit_using_db_data(snippet)
end
create_commit(snippet) create_commit(snippet)
true true
...@@ -72,13 +79,7 @@ module Snippets ...@@ -72,13 +79,7 @@ module Snippets
# If the commit action failed we remove it because # If the commit action failed we remove it because
# we don't want to leave empty repositories # we don't want to leave empty repositories
# around, to allow cloning them. # around, to allow cloning them.
if repository_empty?(snippet) delete_repository(snippet) if repository_empty?(snippet)
snippet.repository.remove
snippet.snippet_repository&.delete
end
# Purge any existing value for repository_exists?
snippet.repository.expire_exists_cache
false false
end end
...@@ -89,15 +90,25 @@ module Snippets ...@@ -89,15 +90,25 @@ module Snippets
raise CreateRepositoryError, 'Repository could not be created' unless snippet.repository_exists? raise CreateRepositoryError, 'Repository could not be created' unless snippet.repository_exists?
end 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) def create_commit(snippet)
raise UpdateError unless snippet.snippet_repository raise UpdateError unless snippet.snippet_repository
commit_attrs = { attrs = commit_attrs(snippet, UPDATE_COMMIT_MSG)
branch_name: snippet.default_branch,
message: 'Update snippet'
}
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 end
# Because we are removing repositories we don't want to remove # Because we are removing repositories we don't want to remove
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
%h3.text-center %h3.text-center
= s_('AdminArea|Projects: %{number_of_projects}') % { number_of_projects: approximate_count_with_delimiters(@counts, Project) } = s_('AdminArea|Projects: %{number_of_projects}') % { number_of_projects: approximate_count_with_delimiters(@counts, Project) }
%hr %hr
= link_to(s_('AdminArea|New project'), new_project_path, class: "btn btn-success gl-w-full") = link_to(s_('AdminArea|New project'), new_project_path, class: "btn gl-button btn-success gl-w-full")
.col-sm-4 .col-sm-4
.info-well.dark-well .info-well.dark-well
.well-segment.well-centered .well-segment.well-centered
...@@ -28,8 +28,8 @@ ...@@ -28,8 +28,8 @@
= s_('AdminArea|Users: %{number_of_users}') % { number_of_users: approximate_count_with_delimiters(@counts, User) } = s_('AdminArea|Users: %{number_of_users}') % { number_of_users: approximate_count_with_delimiters(@counts, User) }
%hr %hr
.btn-group.d-flex{ role: 'group' } .btn-group.d-flex{ role: 'group' }
= link_to s_('AdminArea|New user'), new_admin_user_path, class: "btn btn-success gl-w-full" = link_to s_('AdminArea|New user'), new_admin_user_path, class: "btn gl-button btn-success gl-w-full"
= link_to s_('AdminArea|Users statistics'), admin_dashboard_stats_path, class: 'btn btn-primary gl-w-full' = link_to s_('AdminArea|Users statistics'), admin_dashboard_stats_path, class: 'btn gl-button btn-info gl-w-full'
.col-sm-4 .col-sm-4
.info-well.dark-well .info-well.dark-well
.well-segment.well-centered .well-segment.well-centered
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
%h3.text-center %h3.text-center
= s_('AdminArea|Groups: %{number_of_groups}') % { number_of_groups: approximate_count_with_delimiters(@counts, Group) } = s_('AdminArea|Groups: %{number_of_groups}') % { number_of_groups: approximate_count_with_delimiters(@counts, Group) }
%hr %hr
= link_to s_('AdminArea|New group'), new_admin_group_path, class: "btn btn-success gl-w-full" = link_to s_('AdminArea|New group'), new_admin_group_path, class: "btn gl-button btn-success gl-w-full"
.row .row
.col-md-4 .col-md-4
#js-admin-statistics-container #js-admin-statistics-container
......
...@@ -27,5 +27,5 @@ ...@@ -27,5 +27,5 @@
= render_suggested_colors = render_suggested_colors
.form-actions .form-actions
= f.submit _('Save'), class: 'btn btn-success js-save-button' = f.submit _('Save'), class: 'btn gl-button btn-success js-save-button'
= link_to _("Cancel"), admin_labels_path, class: 'btn btn-cancel' = link_to _("Cancel"), admin_labels_path, class: 'btn gl-button btn-cancel'
%li.label-list-item{ id: dom_id(label) } %li.label-list-item{ id: dom_id(label) }
= render "shared/label_row", label: label.present(issuable_subject: nil) = render "shared/label_row", label: label.present(issuable_subject: nil)
.label-actions-list .label-actions-list
= link_to edit_admin_label_path(label), class: 'btn btn-transparent label-action has-tooltip', title: _('Edit'), data: { placement: 'bottom' }, aria_label: _('Edit') do = link_to edit_admin_label_path(label), class: 'btn gl-button btn-transparent label-action has-tooltip', title: _('Edit'), data: { placement: 'bottom' }, aria_label: _('Edit') do
= sprite_icon('pencil') = sprite_icon('pencil')
= link_to admin_label_path(label), class: 'btn btn-transparent remove-row label-action has-tooltip', title: _('Delete'), data: { placement: 'bottom', confirm: "Delete this label? Are you sure?" }, aria_label: _('Delete'), method: :delete, remote: true do = link_to admin_label_path(label), class: 'btn gl-button btn-transparent remove-row label-action has-tooltip', title: _('Delete'), data: { placement: 'bottom', confirm: "Delete this label? Are you sure?" }, aria_label: _('Delete'), method: :delete, remote: true do
= sprite_icon('remove') = sprite_icon('remove')
- page_title _("Labels") - page_title _("Labels")
%div %div
= link_to new_admin_label_path, class: "float-right btn btn-nr btn-success" do = link_to new_admin_label_path, class: "float-right btn gl-button btn-nr btn-success" do
= _('New label') = _('New label')
%h3.page-title %h3.page-title
= _('Labels') = _('Labels')
......
...@@ -4,7 +4,12 @@ ...@@ -4,7 +4,12 @@
= _('Name') = _('Name')
.table-mobile-content .table-mobile-content
= render 'user_detail', user: user = render 'user_detail', user: user
.table-section.section-25 .table-section.section-10
.table-mobile-header{ role: 'rowheader' }
= _('Projects')
.table-mobile-content.gl-str-truncated{ data: { testid: "user-project-count-#{user.id}" } }
= user.authorized_projects.length
.table-section.section-15
.table-mobile-header{ role: 'rowheader' } .table-mobile-header{ role: 'rowheader' }
= _('Created on') = _('Created on')
.table-mobile-content .table-mobile-content
......
...@@ -72,7 +72,8 @@ ...@@ -72,7 +72,8 @@
.table-holder .table-holder
.thead-white.text-nowrap.gl-responsive-table-row.table-row-header{ role: 'row' } .thead-white.text-nowrap.gl-responsive-table-row.table-row-header{ role: 'row' }
.table-section.section-40{ role: 'rowheader' }= _('Name') .table-section.section-40{ role: 'rowheader' }= _('Name')
.table-section.section-25{ role: 'rowheader' }= _('Created on') .table-section.section-10{ role: 'rowheader' }= _('Projects')
.table-section.section-15{ role: 'rowheader' }= _('Created on')
.table-section.section-15{ role: 'rowheader' }= _('Last activity') .table-section.section-15{ role: 'rowheader' }= _('Last activity')
= render partial: 'admin/users/user', collection: @users = render partial: 'admin/users/user', collection: @users
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
= s_('ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project') = s_('ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project')
= render 'clusters/clusters/buttons' = 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)) } #js-clusters-list-app{ data: js_clusters_list_data(clusterable.index_path(format: :json)) }
- else - else
- if @has_ancestor_clusters - if @has_ancestor_clusters
...@@ -20,7 +20,7 @@ ...@@ -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.') = s_('ClusterIntegration|Clusters are utilized by selecting the nearest ancestor with a matching environment scope. For example, project clusters will override group clusters.')
%strong %strong
= link_to _('More information'), help_page_path('user/group/clusters/index', anchor: 'cluster-precedence') = 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" } .gl-responsive-table-row.table-row-header{ role: "row" }
.table-section.section-60{ role: "rowheader" } .table-section.section-60{ role: "rowheader" }
= s_("ClusterIntegration|Kubernetes cluster") = 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) - 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 .header-main-content
= render partial: 'signature', object: @commit.signature = render partial: 'signature', object: @commit.signature
%strong %strong
...@@ -58,7 +58,7 @@ ...@@ -58,7 +58,7 @@
%pre.commit-description< %pre.commit-description<
= preserve(markdown_field(@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 .well-segment.branch-info
.icon-container.commit-icon .icon-container.commit-icon
= custom_icon("icon_commit") = custom_icon("icon_commit")
......
...@@ -28,7 +28,8 @@ ...@@ -28,7 +28,8 @@
= render partial: 'projects/commits/commit', collection: context_commits, locals: { project: project, ref: ref, merge_request: merge_request } = render partial: 'projects/commits/commit', collection: context_commits, locals: { project: project, ref: ref, merge_request: merge_request }
- if hidden > 0 - if hidden > 0
%li.alert.alert-warning %li.gl-alert.gl-alert-warning
= sprite_icon('warning', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
= n_('%s additional commit has been omitted to prevent performance issues.', '%s additional commits have been omitted to prevent performance issues.', hidden) % number_with_delimiter(hidden) = n_('%s additional commit has been omitted to prevent performance issues.', '%s additional commits have been omitted to prevent performance issues.', hidden) % number_with_delimiter(hidden)
- if project.context_commits_enabled? && can_update_merge_request && context_commits&.empty? - if project.context_commits_enabled? && can_update_merge_request && context_commits&.empty?
......
...@@ -7,6 +7,6 @@ ...@@ -7,6 +7,6 @@
%h4.gl-mt-0 %h4.gl-mt-0
Request details Request details
.col-lg-9 .col-lg-9
= link_to 'Resend Request', @hook_log.present.retry_path, method: :post, class: "btn btn-default float-right gl-ml-3" = link_to 'Resend Request', @hook_log.present.retry_path, method: :post, class: "btn gl-button btn-default float-right gl-ml-3"
= render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log } = render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log }
...@@ -15,18 +15,18 @@ ...@@ -15,18 +15,18 @@
.col-md-2.text-center .col-md-2.text-center
Markdown Markdown
.col-md-10.code.js-syntax-highlight .col-md-10.code.js-syntax-highlight
= highlight_badge('.md', badge.to_markdown, language: 'markdown') = highlight('.md', badge.to_markdown, language: 'markdown')
.row .row
%hr %hr
.row .row
.col-md-2.text-center .col-md-2.text-center
HTML HTML
.col-md-10.code.js-syntax-highlight .col-md-10.code.js-syntax-highlight
= highlight_badge('.html', badge.to_html, language: 'html') = highlight('.html', badge.to_html, language: 'html')
.row .row
%hr %hr
.row .row
.col-md-2.text-center .col-md-2.text-center
AsciiDoc AsciiDoc
.col-md-10.code.js-syntax-highlight .col-md-10.code.js-syntax-highlight
= highlight_badge('.adoc', badge.to_asciidoc) = highlight('.adoc', badge.to_asciidoc)
- if @search_objects.to_a.empty? - if @search_objects.to_a.empty?
= render partial: "search/results/filters"
= render partial: "search/results/empty" = render partial: "search/results/empty"
= render_if_exists 'shared/promotions/promote_advanced_search' = render_if_exists 'shared/promotions/promote_advanced_search'
= render_if_exists 'search/form_revert_to_basic' = render_if_exists 'search/form_revert_to_basic'
...@@ -21,8 +22,7 @@ ...@@ -21,8 +22,7 @@
- link_to_group = link_to(@group.name, @group, class: 'ml-md-1') - link_to_group = link_to(@group.name, @group, class: 'ml-md-1')
= _("in group %{link_to_group}").html_safe % { link_to_group: link_to_group } = _("in group %{link_to_group}").html_safe % { link_to_group: link_to_group }
= render_if_exists 'shared/promotions/promote_advanced_search' = render_if_exists 'shared/promotions/promote_advanced_search'
= render partial: "search/results/filters"
#js-search-filter-by-state{ 'v-cloak': true, data: { scope: @scope, state: params[:state] } }
.results.gl-mt-3 .results.gl-mt-3
- if @scope == 'commits' - if @scope == 'commits'
......
.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 @@ ...@@ -9,5 +9,6 @@
%span.term.str-truncated.gl-font-weight-bold.gl-ml-2= issue.title %span.term.str-truncated.gl-font-weight-bold.gl-ml-2= issue.title
.gl-text-gray-500.gl-my-3 .gl-text-gray-500.gl-my-3
= sprintf(s_(' %{project_name}#%{issue_iid} &middot; opened %{issue_created} by %{author}'), { project_name: issue.project.full_name, issue_iid: issue.iid, issue_created: time_ago_with_tooltip(issue.created_at, placement: 'bottom'), author: link_to_member(@project, issue.author, avatar: false) }).html_safe = sprintf(s_(' %{project_name}#%{issue_iid} &middot; opened %{issue_created} by %{author}'), { project_name: issue.project.full_name, issue_iid: issue.iid, issue_created: time_ago_with_tooltip(issue.created_at, placement: 'bottom'), author: link_to_member(@project, issue.author, avatar: false) }).html_safe
.description.term.col-sm-10.gl-px-0 - if issue.description.present?
= highlight_and_truncate_issue(issue, @search_term, @search_highlight) .description.term.col-sm-10.gl-px-0
= truncate(issue.description, length: 200)
...@@ -211,6 +211,14 @@ ...@@ -211,6 +211,14 @@
:weight: 1 :weight: 1
:idempotent: :idempotent:
:tags: [] :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 - :name: cronjob:metrics_dashboard_schedule_annotations_prune
:feature_category: :metrics :feature_category: :metrics
:has_external_dependencies: :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 name: artifacts_management_page
introduced_by_url: introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/16654
rollout_issue_url: rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/254938
group: group: group::continuous integration
type: development type: development
default_enabled: false default_enabled: false
--- ---
name: clusters_list_redesign name: clusters_list_redesign
introduced_by_url: introduced_by_url:
rollout_issue_url: rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/220182
group: group: Configure
type: development 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 ...@@ -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'] ||= 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']['cron'] ||= '50 23 */1 * *'
Settings.cron_jobs['analytics_instance_statistics_count_job_trigger_worker']['job_class'] ||= 'Analytics::InstanceStatistics::CountJobTriggerWorker' 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 Gitlab.ee do
Settings.cron_jobs['adjourned_group_deletion_worker'] ||= Settingslogic.new({}) 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