Add latest changes from gitlab-org/gitlab@master
|
@ -94,7 +94,8 @@
|
|||
- name: postgres:11.6
|
||||
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
|
||||
- name: redis:4.0-alpine
|
||||
- name: elasticsearch:6.4.2
|
||||
- name: elasticsearch:7.9.2
|
||||
command: ["elasticsearch", "-E", "discovery.type=single-node"]
|
||||
variables:
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
|
||||
|
@ -104,7 +105,8 @@
|
|||
- name: postgres:12
|
||||
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
|
||||
- name: redis:4.0-alpine
|
||||
- name: elasticsearch:6.4.2
|
||||
- name: elasticsearch:7.9.2
|
||||
command: ["elasticsearch", "-E", "discovery.type=single-node"]
|
||||
variables:
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
|
||||
|
|
|
@ -1165,11 +1165,6 @@ Rails/SaveBang:
|
|||
- 'spec/services/users/repair_ldap_blocked_service_spec.rb'
|
||||
- 'spec/services/verify_pages_domain_service_spec.rb'
|
||||
- 'spec/sidekiq/cron/job_gem_dependency_spec.rb'
|
||||
- 'spec/support/migrations_helpers/cluster_helpers.rb'
|
||||
- 'spec/support/migrations_helpers/namespaces_helper.rb'
|
||||
- 'spec/support/shared_contexts/email_shared_context.rb'
|
||||
- 'spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb'
|
||||
- 'spec/support/shared_contexts/mailers/notify_shared_context.rb'
|
||||
|
||||
# Offense count: 187
|
||||
# Cop supports --auto-correct.
|
||||
|
|
4
Gemfile
|
@ -290,7 +290,7 @@ gem 'gitlab_chronic_duration', '~> 0.10.6.2'
|
|||
gem 'rack-proxy', '~> 0.6.0'
|
||||
|
||||
gem 'sassc-rails', '~> 2.1.0'
|
||||
gem 'uglifier', '~> 2.7.2'
|
||||
gem 'terser', '~> 1.0'
|
||||
|
||||
gem 'addressable', '~> 2.7'
|
||||
gem 'font-awesome-rails', '~> 4.7'
|
||||
|
@ -430,7 +430,7 @@ end
|
|||
gem 'octokit', '~> 4.15'
|
||||
|
||||
# https://gitlab.com/gitlab-org/gitlab/issues/207207
|
||||
gem 'gitlab-mail_room', '~> 0.0.6', require: 'mail_room'
|
||||
gem 'gitlab-mail_room', '~> 0.0.7', require: 'mail_room'
|
||||
|
||||
gem 'email_reply_trimmer', '~> 0.1'
|
||||
gem 'html2text'
|
||||
|
|
13
Gemfile.lock
|
@ -312,7 +312,7 @@ GEM
|
|||
tzinfo
|
||||
eventmachine (1.2.7)
|
||||
excon (0.71.1)
|
||||
execjs (2.6.0)
|
||||
execjs (2.7.0)
|
||||
expression_parser (0.9.0)
|
||||
extended-markdown-filter (0.6.0)
|
||||
html-pipeline (~> 2.0)
|
||||
|
@ -436,7 +436,7 @@ GEM
|
|||
opentracing (~> 0.4)
|
||||
redis (> 3.0.0, < 5.0.0)
|
||||
gitlab-license (1.0.0)
|
||||
gitlab-mail_room (0.0.6)
|
||||
gitlab-mail_room (0.0.7)
|
||||
gitlab-markup (1.7.1)
|
||||
gitlab-net-dns (0.9.1)
|
||||
gitlab-puma (4.3.5.gitlab.3)
|
||||
|
@ -1130,6 +1130,8 @@ GEM
|
|||
temple (0.8.2)
|
||||
terminal-table (1.8.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
terser (1.0.1)
|
||||
execjs (>= 0.3.0, < 3)
|
||||
test-prof (0.12.0)
|
||||
text (1.3.1)
|
||||
thin (1.7.2)
|
||||
|
@ -1157,9 +1159,6 @@ GEM
|
|||
thread_safe (~> 0.1)
|
||||
u2f (0.2.1)
|
||||
uber (0.1.0)
|
||||
uglifier (2.7.2)
|
||||
execjs (>= 0.3.0)
|
||||
json (>= 1.8.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.5)
|
||||
|
@ -1327,7 +1326,7 @@ DEPENDENCIES
|
|||
gitlab-fog-azure-rm (~> 1.0)
|
||||
gitlab-labkit (= 0.12.1)
|
||||
gitlab-license (~> 1.0)
|
||||
gitlab-mail_room (~> 0.0.6)
|
||||
gitlab-mail_room (~> 0.0.7)
|
||||
gitlab-markup (~> 1.7.1)
|
||||
gitlab-net-dns (~> 0.9.1)
|
||||
gitlab-puma (~> 4.3.3.gitlab.2)
|
||||
|
@ -1483,13 +1482,13 @@ DEPENDENCIES
|
|||
stackprof (~> 0.2.15)
|
||||
state_machines-activerecord (~> 0.6.0)
|
||||
sys-filesystem (~> 1.1.6)
|
||||
terser (~> 1.0)
|
||||
test-prof (~> 0.12.0)
|
||||
thin (~> 1.7.0)
|
||||
timecop (~> 0.9.1)
|
||||
toml-rb (~> 1.0.0)
|
||||
truncato (~> 0.7.11)
|
||||
u2f (~> 0.2.1)
|
||||
uglifier (~> 2.7.2)
|
||||
unf (~> 0.1.4)
|
||||
unicorn (~> 5.5)
|
||||
unicorn-worker-killer (~> 0.4.4)
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
import { flatten } from 'lodash';
|
||||
import { s__ } from '~/locale';
|
||||
import AccessorUtilities from '~/lib/utils/accessor';
|
||||
import { shouldDisableShortcuts } from './shortcuts_toggle';
|
||||
|
||||
export const LOCAL_STORAGE_KEY = 'gl-keyboard-shortcuts-customizations';
|
||||
|
||||
let parsedCustomizations = {};
|
||||
const localStorageIsSafe = AccessorUtilities.isLocalStorageAccessSafe();
|
||||
|
||||
if (localStorageIsSafe) {
|
||||
try {
|
||||
parsedCustomizations = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY) || '{}');
|
||||
} catch (e) {
|
||||
/* do nothing */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A map of command => keys of all keyboard shortcuts
|
||||
* that have been customized by the user.
|
||||
*
|
||||
* @example
|
||||
* { "globalShortcuts.togglePerformanceBar": ["p e r f"] }
|
||||
*
|
||||
* @type { Object.<string, string[]> }
|
||||
*/
|
||||
export const customizations = parsedCustomizations;
|
||||
|
||||
// All available commands
|
||||
export const TOGGLE_PERFORMANCE_BAR = 'globalShortcuts.togglePerformanceBar';
|
||||
|
||||
/** All keybindings, grouped and ordered with descriptions */
|
||||
export const keybindingGroups = [
|
||||
{
|
||||
groupId: 'globalShortcuts',
|
||||
name: s__('KeyboardShortcuts|Global Shortcuts'),
|
||||
keybindings: [
|
||||
{
|
||||
description: s__('KeyboardShortcuts|Toggle the Performance Bar'),
|
||||
command: TOGGLE_PERFORMANCE_BAR,
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
defaultKeys: ['p b'],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// For each keybinding object, add a `customKeys` property populated with the
|
||||
// user's custom keybindings (if the command has been customized).
|
||||
// `customKeys` will be `undefined` if the command hasn't been customized.
|
||||
.map(group => {
|
||||
return {
|
||||
...group,
|
||||
keybindings: group.keybindings.map(binding => ({
|
||||
...binding,
|
||||
customKeys: customizations[binding.command],
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* A simple map of command => keys. All user customizations are included in this map.
|
||||
* This mapping is used to simplify `keysFor` below.
|
||||
*
|
||||
* @example
|
||||
* { "globalShortcuts.togglePerformanceBar": ["p e r f"] }
|
||||
*/
|
||||
const commandToKeys = flatten(keybindingGroups.map(group => group.keybindings)).reduce(
|
||||
(acc, binding) => {
|
||||
acc[binding.command] = binding.customKeys || binding.defaultKeys;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets keyboard shortcuts associated with a command
|
||||
*
|
||||
* @param {string} command The command string. All command
|
||||
* strings are available as imports from this file.
|
||||
*
|
||||
* @returns {string[]} An array of keyboard shortcut strings bound to the command
|
||||
*
|
||||
* @example
|
||||
* import { keysFor, TOGGLE_PERFORMANCE_BAR } from '~/behaviors/shortcuts/keybindings'
|
||||
*
|
||||
* Mousetrap.bind(keysFor(TOGGLE_PERFORMANCE_BAR), handler);
|
||||
*/
|
||||
export const keysFor = command => {
|
||||
if (shouldDisableShortcuts()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return commandToKeys[command];
|
||||
};
|
|
@ -9,6 +9,7 @@ import axios from '../../lib/utils/axios_utils';
|
|||
import { refreshCurrentPage, visitUrl } from '../../lib/utils/url_utility';
|
||||
import findAndFollowLink from '../../lib/utils/navigation_utility';
|
||||
import { parseBoolean, getCspNonceValue } from '~/lib/utils/common_utils';
|
||||
import { keysFor, TOGGLE_PERFORMANCE_BAR } from './keybindings';
|
||||
|
||||
const defaultStopCallback = Mousetrap.prototype.stopCallback;
|
||||
Mousetrap.prototype.stopCallback = function customStopCallback(e, element, combo) {
|
||||
|
@ -70,7 +71,7 @@ export default class Shortcuts {
|
|||
Mousetrap.bind('s', Shortcuts.focusSearch);
|
||||
Mousetrap.bind('/', Shortcuts.focusSearch);
|
||||
Mousetrap.bind('f', this.focusFilter.bind(this));
|
||||
Mousetrap.bind('p b', Shortcuts.onTogglePerfBar);
|
||||
Mousetrap.bind(keysFor(TOGGLE_PERFORMANCE_BAR), Shortcuts.onTogglePerfBar);
|
||||
|
||||
const findFileURL = document.body.dataset.findFile;
|
||||
|
||||
|
|
|
@ -132,7 +132,13 @@ export default {
|
|||
>
|
||||
<div v-if="icon.name" data-testid="designEvent" class="design-event gl-absolute">
|
||||
<span :title="icon.tooltip" :aria-label="icon.tooltip">
|
||||
<gl-icon :name="icon.name" :size="18" :class="icon.classes" />
|
||||
<gl-icon
|
||||
:name="icon.name"
|
||||
:size="18"
|
||||
:class="icon.classes"
|
||||
data-qa-selector="design_status_icon"
|
||||
:data-qa-status="icon.name"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<gl-intersection-observer @appear="onAppear">
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
|
||||
import { __ } from '~/locale';
|
||||
import {
|
||||
MATCH_LINE_TYPE,
|
||||
|
@ -23,21 +22,8 @@ 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 shouldRenderCommentButton = (isLoggedIn, isCommentButtonRendered) => {
|
||||
return isCommentButtonRendered && isLoggedIn;
|
||||
};
|
||||
|
||||
export const hasDiscussions = line => line?.discussions?.length > 0;
|
||||
|
|
|
@ -81,11 +81,7 @@ export default {
|
|||
return utils.addCommentTooltip(this.line);
|
||||
},
|
||||
shouldRenderCommentButton() {
|
||||
return utils.shouldRenderCommentButton(
|
||||
this.isLoggedIn,
|
||||
true,
|
||||
gon.features?.mergeRefHeadComments,
|
||||
);
|
||||
return utils.shouldRenderCommentButton(this.isLoggedIn, true);
|
||||
},
|
||||
shouldShowCommentButton() {
|
||||
return utils.shouldShowCommentButton(
|
||||
|
|
|
@ -102,11 +102,7 @@ export default {
|
|||
return utils.addCommentTooltip(this.line.right);
|
||||
},
|
||||
shouldRenderCommentButton() {
|
||||
return utils.shouldRenderCommentButton(
|
||||
this.isLoggedIn,
|
||||
this.isCommentButtonRendered,
|
||||
gon.features?.mergeRefHeadComments,
|
||||
);
|
||||
return utils.shouldRenderCommentButton(this.isLoggedIn, this.isCommentButtonRendered);
|
||||
},
|
||||
shouldShowCommentButtonLeft() {
|
||||
return utils.shouldShowCommentButton(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { __ } from '~/locale';
|
||||
|
||||
export default IssuableTokenKeys => {
|
||||
export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
|
||||
const draftToken = {
|
||||
token: {
|
||||
formattedKey: __('Draft'),
|
||||
|
@ -51,18 +51,20 @@ export default IssuableTokenKeys => {
|
|||
IssuableTokenKeys.tokenKeysWithAlternative.push(draftToken.token);
|
||||
IssuableTokenKeys.conditions.push(...draftToken.conditions);
|
||||
|
||||
const targetBranchToken = {
|
||||
formattedKey: __('Target-Branch'),
|
||||
key: 'target-branch',
|
||||
type: 'string',
|
||||
param: '',
|
||||
symbol: '',
|
||||
icon: 'arrow-right',
|
||||
tag: 'branch',
|
||||
};
|
||||
if (!disableTargetBranchFilter) {
|
||||
const targetBranchToken = {
|
||||
formattedKey: __('Target-Branch'),
|
||||
key: 'target-branch',
|
||||
type: 'string',
|
||||
param: '',
|
||||
symbol: '',
|
||||
icon: 'arrow-right',
|
||||
tag: 'branch',
|
||||
};
|
||||
|
||||
IssuableTokenKeys.tokenKeys.push(targetBranchToken);
|
||||
IssuableTokenKeys.tokenKeysWithAlternative.push(targetBranchToken);
|
||||
IssuableTokenKeys.tokenKeys.push(targetBranchToken);
|
||||
IssuableTokenKeys.tokenKeysWithAlternative.push(targetBranchToken);
|
||||
}
|
||||
|
||||
const approvedBy = {
|
||||
token: {
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
<script>
|
||||
import { GlIcon, GlButton, GlTooltipDirective, GlAvatarLink, GlAvatarLabeled } from '@gitlab/ui';
|
||||
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
GlButton,
|
||||
GlAvatarLink,
|
||||
GlAvatarLabeled,
|
||||
TimeAgoTooltip,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
createdAt: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
author: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
statusBadgeClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
statusIcon: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
blocked: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
confidential: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
authorId() {
|
||||
return getIdFromGraphQLId(`${this.author.id}`);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.toggleSidebarButtonEl = document.querySelector('.js-toggle-right-sidebar-button');
|
||||
},
|
||||
methods: {
|
||||
handleRightSidebarToggleClick() {
|
||||
if (this.toggleSidebarButtonEl) {
|
||||
this.toggleSidebarButtonEl.dispatchEvent(new Event('click'));
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="detail-page-header">
|
||||
<div class="detail-page-header-body">
|
||||
<div data-testid="status" class="issuable-status-box status-box" :class="statusBadgeClass">
|
||||
<gl-icon v-if="statusIcon" :name="statusIcon" class="d-block d-sm-none" />
|
||||
<span class="d-none d-sm-block"><slot name="status-badge"></slot></span>
|
||||
</div>
|
||||
<div class="issuable-meta gl-display-flex gl-align-items-center">
|
||||
<div class="gl-display-inline-block">
|
||||
<div v-if="blocked" data-testid="blocked" class="issuable-warning-icon inline">
|
||||
<gl-icon name="lock" :aria-label="__('Blocked')" />
|
||||
</div>
|
||||
<div v-if="confidential" data-testid="confidential" class="issuable-warning-icon inline">
|
||||
<gl-icon name="eye-slash" :aria-label="__('Confidential')" />
|
||||
</div>
|
||||
</div>
|
||||
<span>
|
||||
{{ __('Opened') }}
|
||||
<time-ago-tooltip data-testid="startTimeItem" :time="createdAt" />
|
||||
{{ __('by') }}
|
||||
</span>
|
||||
<gl-avatar-link
|
||||
data-testid="avatar"
|
||||
:data-user-id="authorId"
|
||||
:data-username="author.username"
|
||||
:data-name="author.name"
|
||||
:href="author.webUrl"
|
||||
target="_blank"
|
||||
class="js-user-link gl-ml-2"
|
||||
>
|
||||
<gl-avatar-labeled
|
||||
:size="24"
|
||||
:src="author.avatarUrl"
|
||||
:label="author.name"
|
||||
class="d-none d-sm-inline-flex gl-ml-1"
|
||||
/>
|
||||
<strong class="author d-sm-none d-inline">@{{ author.username }}</strong>
|
||||
</gl-avatar-link>
|
||||
</div>
|
||||
<gl-button
|
||||
data-testid="sidebar-toggle"
|
||||
icon="chevron-double-lg-left"
|
||||
class="d-block d-sm-none gutter-toggle issuable-gutter-toggle"
|
||||
:aria-label="__('Expand sidebar')"
|
||||
@click="handleRightSidebarToggleClick"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-testid="header-actions"
|
||||
class="detail-page-header-actions js-issuable-actions js-issuable-buttons gl-display-flex gl-display-md-block"
|
||||
>
|
||||
<slot name="header-actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -85,6 +85,21 @@ export const getDayName = date =>
|
|||
__('Saturday'),
|
||||
][date.getDay()];
|
||||
|
||||
/**
|
||||
* Returns the i18n month name from a given date
|
||||
* @example
|
||||
* formatDateAsMonth(new Date('2020-06-28')) -> 'Jun'
|
||||
* @param {String} datetime where month is extracted from
|
||||
* @param {Object} options
|
||||
* @param {Boolean} options.abbreviated whether to use the abbreviated month string, or not
|
||||
* @return {String} the i18n month name
|
||||
*/
|
||||
export function formatDateAsMonth(datetime, options = {}) {
|
||||
const { abbreviated = true } = options;
|
||||
const month = new Date(datetime).getMonth();
|
||||
return getMonthNames(abbreviated)[month];
|
||||
}
|
||||
|
||||
/**
|
||||
* @example
|
||||
* dateFormat('2017-12-05','mmm d, yyyy h:MMtt Z' ) -> "Dec 5, 2017 12:00am GMT+0000"
|
||||
|
|
|
@ -5,7 +5,7 @@ import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered
|
|||
import { FILTERED_SEARCH } from '~/pages/constants';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys);
|
||||
addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys, true);
|
||||
|
||||
initFilteredSearch({
|
||||
page: FILTERED_SEARCH.MERGE_REQUESTS,
|
||||
|
|
|
@ -38,6 +38,14 @@
|
|||
top: $mr-file-header-top;
|
||||
z-index: 120;
|
||||
|
||||
.with-system-header & {
|
||||
top: $mr-file-header-top + $system-header-height;
|
||||
}
|
||||
|
||||
.with-system-header.with-performance-bar & {
|
||||
top: $mr-file-header-top + $system-header-height + $performance-bar-height;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
@ -1078,6 +1086,14 @@ table.code {
|
|||
max-height: calc(100vh - #{$top-pos});
|
||||
z-index: 202;
|
||||
|
||||
.with-system-header & {
|
||||
top: $top-pos + $system-header-height;
|
||||
}
|
||||
|
||||
.with-system-header.with-performance-bar & {
|
||||
top: $top-pos + $system-header-height + $performance-bar-height;
|
||||
}
|
||||
|
||||
.with-performance-bar & {
|
||||
$performance-bar-top-pos: $performance-bar-height + $top-pos;
|
||||
top: $performance-bar-top-pos;
|
||||
|
|
|
@ -771,6 +771,14 @@ $mr-widget-min-height: 69px;
|
|||
position: sticky;
|
||||
top: $header-height + $mr-tabs-height;
|
||||
|
||||
.with-system-header & {
|
||||
top: $header-height + $mr-tabs-height + $system-header-height;
|
||||
}
|
||||
|
||||
.with-system-header.with-performance-bar & {
|
||||
top: $header-height + $mr-tabs-height + $system-header-height + $performance-bar-height;
|
||||
}
|
||||
|
||||
.mr-version-menus-container {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
@ -788,6 +796,14 @@ $mr-widget-min-height: 69px;
|
|||
background-color: $white;
|
||||
border-bottom: 1px solid $border-color;
|
||||
|
||||
.with-system-header & {
|
||||
top: $header-height + $system-header-height;
|
||||
}
|
||||
|
||||
.with-system-header.with-performance-bar & {
|
||||
top: $header-height + $system-header-height + $performance-bar-height;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
|
|
|
@ -17,13 +17,7 @@ module SnippetsActions
|
|||
respond_to :html
|
||||
end
|
||||
|
||||
def edit
|
||||
# We need to load some info from the existing blob
|
||||
snippet.content = blob.data
|
||||
snippet.file_name = blob.path
|
||||
|
||||
render 'edit'
|
||||
end
|
||||
def edit; end
|
||||
|
||||
# This endpoint is being replaced by Snippets::BlobController#raw
|
||||
# Support for old raw links will be maintainted via this action but
|
||||
|
@ -55,7 +49,6 @@ module SnippetsActions
|
|||
def show
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
conditionally_expand_blob(blob)
|
||||
@note = Note.new(noteable: @snippet, project: @snippet.project)
|
||||
@noteable = @snippet
|
||||
|
||||
|
@ -80,29 +73,6 @@ module SnippetsActions
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
update_params = snippet_params.merge(spammable_params)
|
||||
|
||||
service_response = Snippets::UpdateService.new(@snippet.project, current_user, update_params).execute(@snippet)
|
||||
@snippet = service_response.payload[:snippet]
|
||||
|
||||
handle_repository_error(:edit)
|
||||
end
|
||||
|
||||
def destroy
|
||||
service_response = Snippets::DestroyService.new(current_user, @snippet).execute
|
||||
|
||||
if service_response.success?
|
||||
redirect_to gitlab_dashboard_snippets_path(@snippet), status: :found
|
||||
elsif service_response.http_status == 403
|
||||
access_denied!
|
||||
else
|
||||
redirect_to gitlab_snippet_path(@snippet),
|
||||
status: :found,
|
||||
alert: service_response.message
|
||||
end
|
||||
end
|
||||
# rubocop:enable Gitlab/ModuleWithInstanceVariables
|
||||
|
||||
private
|
||||
|
@ -124,12 +94,4 @@ module SnippetsActions
|
|||
def convert_line_endings(content)
|
||||
params[:line_ending] == 'raw' ? content : content.gsub(/\r\n/, "\n")
|
||||
end
|
||||
|
||||
def handle_repository_error(action)
|
||||
errors = Array(snippet.errors.delete(:repository))
|
||||
|
||||
flash.now[:alert] = errors.first if errors.present?
|
||||
|
||||
recaptcha_check_with_fallback(errors.empty?) { render action }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -173,7 +173,6 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
|
|||
end
|
||||
|
||||
def update_diff_discussion_positions!
|
||||
return unless Feature.enabled?(:merge_ref_head_comments, @merge_request.target_project, default_enabled: true)
|
||||
return unless Feature.enabled?(:merge_red_head_comments_position_on_demand, @merge_request.target_project, default_enabled: true)
|
||||
return if @merge_request.has_any_diff_note_positions?
|
||||
|
||||
|
|
|
@ -29,7 +29,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
before_action only: [:show] do
|
||||
push_frontend_experiment(:suggest_pipeline)
|
||||
push_frontend_feature_flag(:widget_visibility_polling, @project, default_enabled: true)
|
||||
push_frontend_feature_flag(:merge_ref_head_comments, @project, default_enabled: true)
|
||||
push_frontend_feature_flag(:mr_commit_neighbor_nav, @project, default_enabled: true)
|
||||
push_frontend_feature_flag(:multiline_comments, @project, default_enabled: true)
|
||||
push_frontend_feature_flag(:file_identifier_hash)
|
||||
|
|
|
@ -7,12 +7,11 @@ class Projects::SnippetsController < Projects::Snippets::ApplicationController
|
|||
|
||||
before_action :check_snippets_available!
|
||||
|
||||
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam]
|
||||
before_action :snippet, only: [:show, :edit, :raw, :toggle_award_emoji, :mark_as_spam]
|
||||
|
||||
before_action :authorize_create_snippet!, only: [:new, :create]
|
||||
before_action :authorize_read_snippet!, except: [:new, :create, :index]
|
||||
before_action :authorize_update_snippet!, only: [:edit, :update]
|
||||
before_action :authorize_admin_snippet!, only: [:destroy]
|
||||
before_action :authorize_create_snippet!, only: :new
|
||||
before_action :authorize_read_snippet!, except: [:new, :index]
|
||||
before_action :authorize_update_snippet!, only: :edit
|
||||
|
||||
def index
|
||||
@snippet_counts = ::Snippets::CountService
|
||||
|
@ -33,14 +32,6 @@ class Projects::SnippetsController < Projects::Snippets::ApplicationController
|
|||
@snippet = @noteable = @project.snippets.build
|
||||
end
|
||||
|
||||
def create
|
||||
create_params = snippet_params.merge(spammable_params)
|
||||
service_response = ::Snippets::CreateService.new(project, current_user, create_params).execute
|
||||
@snippet = service_response.payload[:snippet]
|
||||
|
||||
handle_repository_error(:new)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
alias_method :awardable, :snippet
|
||||
|
@ -49,8 +40,4 @@ class Projects::SnippetsController < Projects::Snippets::ApplicationController
|
|||
def spammable_path
|
||||
project_snippet_path(@project, @snippet)
|
||||
end
|
||||
|
||||
def snippet_params
|
||||
params.require(:project_snippet).permit(:title, :content, :file_name, :private, :visibility_level, :description)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,12 +6,11 @@ class SnippetsController < Snippets::ApplicationController
|
|||
include ToggleAwardEmoji
|
||||
include SpammableActions
|
||||
|
||||
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam]
|
||||
before_action :snippet, only: [:show, :edit, :raw, :toggle_award_emoji, :mark_as_spam]
|
||||
|
||||
before_action :authorize_create_snippet!, only: [:new, :create]
|
||||
before_action :authorize_create_snippet!, only: :new
|
||||
before_action :authorize_read_snippet!, only: [:show, :raw]
|
||||
before_action :authorize_update_snippet!, only: [:edit, :update]
|
||||
before_action :authorize_admin_snippet!, only: [:destroy]
|
||||
before_action :authorize_update_snippet!, only: :edit
|
||||
|
||||
skip_before_action :authenticate_user!, only: [:index, :show, :raw]
|
||||
|
||||
|
@ -40,18 +39,6 @@ class SnippetsController < Snippets::ApplicationController
|
|||
@snippet = PersonalSnippet.new
|
||||
end
|
||||
|
||||
def create
|
||||
create_params = snippet_params.merge(files: params.delete(:files))
|
||||
service_response = Snippets::CreateService.new(nil, current_user, create_params).execute
|
||||
@snippet = service_response.payload[:snippet]
|
||||
|
||||
if service_response.error? && @snippet.errors[:repository].present?
|
||||
handle_repository_error(:new)
|
||||
else
|
||||
recaptcha_check_with_fallback { render :new }
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
alias_method :awardable, :snippet
|
||||
|
@ -60,8 +47,4 @@ class SnippetsController < Snippets::ApplicationController
|
|||
def spammable_path
|
||||
snippet_path(@snippet)
|
||||
end
|
||||
|
||||
def snippet_params
|
||||
params.require(:personal_snippet).permit(:title, :content, :file_name, :private, :visibility_level, :description).merge(spammable_params)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -24,4 +24,9 @@ module ContainerExpirationPoliciesHelper
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def container_expiration_policies_historic_entry_enabled?(project)
|
||||
Gitlab::CurrentSettings.container_expiration_policies_enable_historic_entries ||
|
||||
Feature.enabled?(:container_expiration_policies_historic_entry, project)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -526,7 +526,6 @@ module Ci
|
|||
.concat(job_jwt_variables)
|
||||
.concat(scoped_variables)
|
||||
.concat(job_variables)
|
||||
.concat(environment_changed_page_variables)
|
||||
.concat(persisted_environment_variables)
|
||||
.to_runner_variables
|
||||
end
|
||||
|
@ -563,15 +562,6 @@ module Ci
|
|||
end
|
||||
end
|
||||
|
||||
def environment_changed_page_variables
|
||||
Gitlab::Ci::Variables::Collection.new.tap do |variables|
|
||||
break variables unless environment_status && Feature.enabled?(:modifed_path_ci_variables, project)
|
||||
|
||||
variables.append(key: 'CI_MERGE_REQUEST_CHANGED_PAGE_PATHS', value: environment_status.changed_paths.join(','))
|
||||
variables.append(key: 'CI_MERGE_REQUEST_CHANGED_PAGE_URLS', value: environment_status.changed_urls.join(','))
|
||||
end
|
||||
end
|
||||
|
||||
def deploy_token_variables
|
||||
Gitlab::Ci::Variables::Collection.new.tap do |variables|
|
||||
break variables unless gitlab_deploy_token
|
||||
|
|
|
@ -72,14 +72,6 @@ class EnvironmentStatus
|
|||
.merge_request_diff_files.where(deleted_file: false)
|
||||
end
|
||||
|
||||
def changed_paths
|
||||
changes.map { |change| change[:path] }
|
||||
end
|
||||
|
||||
def changed_urls
|
||||
changes.map { |change| change[:external_url] }
|
||||
end
|
||||
|
||||
def has_route_map?
|
||||
project.route_map_for(sha).present?
|
||||
end
|
||||
|
|
|
@ -69,9 +69,6 @@ class DiscussionEntity < Grape::Entity
|
|||
end
|
||||
|
||||
def display_merge_ref_discussions?(discussion)
|
||||
return unless discussion.diff_discussion?
|
||||
return if discussion.legacy_diff_discussion?
|
||||
|
||||
Feature.enabled?(:merge_ref_head_comments, discussion.project, default_enabled: true)
|
||||
discussion.diff_discussion? && !discussion.legacy_diff_discussion?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -125,8 +125,6 @@ module MergeRequests
|
|||
end
|
||||
|
||||
def update_diff_discussion_positions!
|
||||
return if Feature.disabled?(:merge_ref_head_comments, merge_request.target_project, default_enabled: true)
|
||||
|
||||
Discussions::CaptureDiffNotePositionsService.new(merge_request).execute
|
||||
end
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@ module Notes
|
|||
Gitlab::Tracking.event('Notes::CreateService', 'execute', tracking_data_for(note))
|
||||
end
|
||||
|
||||
if Feature.enabled?(:merge_ref_head_comments, project, default_enabled: true) && note.for_merge_request? && note.diff_note? && note.start_of_discussion?
|
||||
if note.for_merge_request? && note.diff_note? && note.start_of_discussion?
|
||||
Discussions::CaptureDiffNotePositionService.new(note.noteable, note.diff_file&.paths).execute(note.discussion)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
.top-area
|
||||
= render 'shared/issuable/nav', type: :merge_requests, display_count: !@no_filters_set
|
||||
|
||||
= render 'shared/issuable/search_bar', type: :merge_requests
|
||||
= render 'shared/issuable/search_bar', type: :merge_requests, disable_target_branch: true
|
||||
|
||||
- if current_user && @no_filters_set
|
||||
= render 'shared/dashboard/no_filter_selected'
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
= form_tag project_compare_index_path(@project), method: :post, class: 'form-inline js-requires-input js-signature-container', data: { 'signatures-path' => signatures_namespace_project_compare_index_path } do
|
||||
- if params[:to] && params[:from]
|
||||
.compare-switch-container
|
||||
= link_to sprite_icon('substitute'), { from: params[:to], to: params[:from] }, class: 'commits-compare-switch has-tooltip btn btn-white', title: 'Swap revisions'
|
||||
= link_to sprite_icon('substitute'), { from: params[:to], to: params[:from] }, class: 'commits-compare-switch has-tooltip btn gl-button btn-white', title: 'Swap revisions'
|
||||
.form-group.dropdown.compare-form-group.to.js-compare-to-dropdown
|
||||
.input-group.inline-input-group
|
||||
%span.input-group-prepend
|
||||
.input-group-text
|
||||
= s_("CompareBranches|Source")
|
||||
= hidden_field_tag :to, params[:to]
|
||||
= button_tag type: 'button', title: params[:to], class: "btn form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do
|
||||
= button_tag type: 'button', title: params[:to], class: "btn gl-button form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do
|
||||
.dropdown-toggle-text.str-truncated.monospace.float-left= params[:to] || _("Select branch/tag")
|
||||
= sprite_icon('chevron-down', css_class: 'float-right')
|
||||
= render 'shared/ref_dropdown'
|
||||
|
@ -19,12 +19,12 @@
|
|||
.input-group-text
|
||||
= s_("CompareBranches|Target")
|
||||
= hidden_field_tag :from, params[:from]
|
||||
= button_tag type: 'button', title: params[:from], class: "btn form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
|
||||
= button_tag type: 'button', title: params[:from], class: "btn gl-button form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
|
||||
.dropdown-toggle-text.str-truncated.monospace.float-left= params[:from] || _("Select branch/tag")
|
||||
= sprite_icon('chevron-down', css_class: 'float-right')
|
||||
= render 'shared/ref_dropdown'
|
||||
|
||||
= button_tag s_("CompareBranches|Compare"), class: "btn btn-success commits-compare-btn"
|
||||
= button_tag s_("CompareBranches|Compare"), class: "btn gl-button btn-success commits-compare-btn"
|
||||
- if @merge_request.present?
|
||||
= link_to _("View open merge request"), project_merge_request_path(@project, @merge_request), class: 'gl-ml-3 btn'
|
||||
- elsif create_mr_button?
|
||||
|
|
|
@ -5,4 +5,4 @@
|
|||
older_than_options: older_than_options.to_json,
|
||||
is_admin: current_user&.admin.to_s,
|
||||
admin_settings_path: ci_cd_admin_application_settings_path(anchor: 'js-registry-settings'),
|
||||
enable_historic_entries: Gitlab::CurrentSettings.try(:container_expiration_policies_enable_historic_entries).to_s} }
|
||||
enable_historic_entries: container_expiration_policies_historic_entry_enabled?(@project).to_s} }
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
- type = local_assigns.fetch(:type)
|
||||
- board = local_assigns.fetch(:board, nil)
|
||||
- show_sorting_dropdown = local_assigns.fetch(:show_sorting_dropdown, true)
|
||||
- disable_target_branch = local_assigns.fetch(:disable_target_branch, false)
|
||||
- placeholder = local_assigns[:placeholder] || _('Search or filter results...')
|
||||
- is_not_boards_modal_or_productivity_analytics = type != :boards_modal && type != :productivity_analytics
|
||||
- block_css_class = is_not_boards_modal_or_productivity_analytics ? 'row-content-block second-block' : ''
|
||||
|
@ -154,11 +155,12 @@
|
|||
%li.filter-dropdown-item{ data: { value: 'no', capitalize: true } }
|
||||
%button.btn.btn-link{ type: 'button' }
|
||||
= _('No')
|
||||
#js-dropdown-target-branch.filtered-search-input-dropdown-menu.dropdown-menu
|
||||
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
|
||||
%li.filter-dropdown-item
|
||||
%button.btn.btn-link.js-data-value.monospace
|
||||
{{title}}
|
||||
- unless disable_target_branch
|
||||
#js-dropdown-target-branch.filtered-search-input-dropdown-menu.dropdown-menu
|
||||
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
|
||||
%li.filter-dropdown-item
|
||||
%button.btn.btn-link.js-data-value.monospace
|
||||
{{title}}
|
||||
|
||||
= render_if_exists 'shared/issuable/filter_weight', type: type
|
||||
|
||||
|
|
|
@ -100,7 +100,7 @@ class GitGarbageCollectWorker # rubocop:disable Scalability/IdempotentWorker
|
|||
end
|
||||
|
||||
def flush_ref_caches(project)
|
||||
project.repository.after_create_branch
|
||||
project.repository.expire_branches_cache
|
||||
project.repository.branch_names
|
||||
project.repository.has_visible_content?
|
||||
end
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Apply GitLab UI button styles to buttons in app/views/projects/compare directory
|
||||
merge_request: 44342
|
||||
author: Lakshit
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add cache:when keyword for ci yml config
|
||||
merge_request: 41822
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add feature flag for a phased rollout of cleanup policies
|
||||
merge_request: 44444
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix Rails/SaveBang offenses in spec/support/*
|
||||
merge_request: 44884
|
||||
author: matthewbried
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Disable target branch filter option on merge requests dashboard
|
||||
merge_request:
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fixed merge request tabs overlapping with system header
|
||||
merge_request:
|
||||
author:
|
||||
type: fixed
|
|
@ -12,7 +12,7 @@ Rails.application.configure do
|
|||
config.public_file_server.enabled = false
|
||||
|
||||
# Compress JavaScripts and CSS.
|
||||
config.assets.js_compressor = :uglifier
|
||||
config.assets.js_compressor = :terser
|
||||
# config.assets.css_compressor = :sass
|
||||
|
||||
# Don't fallback to assets pipeline if a precompiled asset is missed
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
name: additional_snowplow_tracking
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/12088
|
||||
rollout_issue_url:
|
||||
group: group::product_analytics
|
||||
group: group::product analytics
|
||||
type: development
|
||||
default_enabled: false
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
name: ci_child_of_child_pipeline
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41102
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/243747
|
||||
group: 'group::continuous integration'
|
||||
group: group::continuous integration
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
name: ci_lint_vue
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/42401
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/249661
|
||||
group: group::continuous intergration
|
||||
group: group::continuous integration
|
||||
type: development
|
||||
default_enabled: false
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: container_expiration_policies_historic_entry
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44444
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/262639
|
||||
type: development
|
||||
group: group::package
|
||||
default_enabled: false
|
|
@ -3,5 +3,5 @@ name: deploy_boards_dedupe_instances
|
|||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40768
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/258214
|
||||
type: development
|
||||
group: group::progressive-delivery
|
||||
group: group::progressive delivery
|
||||
default_enabled: false
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
name: drop_license_management_artifact
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group: composition_analysis
|
||||
group: group::composition analysis
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
name: ingress_modsecurity
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20194
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/258554
|
||||
group: "group::container security"
|
||||
group: group::container security
|
||||
type: development
|
||||
default_enabled: false
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
name: junit_pipeline_screenshots_view
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/202114
|
||||
rollout_issue_url:
|
||||
group: 'group::verify testing'
|
||||
group: group::verify testing
|
||||
type: development
|
||||
default_enabled: false
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
name: merge_ref_head_comments
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
type: development
|
||||
default_enabled: true
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
name: modifed_path_ci_variables
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
type: development
|
||||
default_enabled: false
|
|
@ -2,6 +2,6 @@
|
|||
name: product_analytics
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36443
|
||||
rollout_issue_url:
|
||||
group: group::product_analytics
|
||||
group: group::product analytics
|
||||
type: development
|
||||
default_enabled: false
|
||||
|
|
|
@ -3,5 +3,5 @@ name: project_finder_similarity_sort
|
|||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43136
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/263249
|
||||
type: development
|
||||
group: group::threat_insights
|
||||
group: group::threat insights
|
||||
default_enabled: false
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: push_rules_supersede_code_owners
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44126
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/262019
|
||||
type: development
|
||||
group: group::source code
|
||||
default_enabled: false
|
|
@ -2,6 +2,6 @@
|
|||
name: rebalance_issues
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40124
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/239344
|
||||
group: 'group::project management'
|
||||
group: group::project management
|
||||
type: development
|
||||
default_enabled: false
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
name: save_raw_usage_data
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38457
|
||||
rollout_issue_url:
|
||||
group: group::product_analytics
|
||||
group: group::product analytics
|
||||
type: development
|
||||
default_enabled: false
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
name: track_issue_activity_actions
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40904
|
||||
rollout_issue_url:
|
||||
group: group::project_management
|
||||
group: group::project management
|
||||
type: development
|
||||
default_enabled: false
|
|
@ -2,6 +2,6 @@
|
|||
name: usage_data_api
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41301
|
||||
rollout_issue_url:
|
||||
group: group::product_analytics
|
||||
group: group::product analytics
|
||||
type: development
|
||||
default_enabled: false
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
name: usage_data_i_source_code_code_intelligence
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41881
|
||||
rollout_issue_url:
|
||||
group: group::source_code
|
||||
group: group::source code
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Sprockets.register_compressor 'application/javascript', :terser, Terser::Compressor
|
|
@ -26,7 +26,7 @@ production:
|
|||
# http://redis.io/topics/sentinel
|
||||
#
|
||||
# You must specify a list of a few sentinels that will handle client connection
|
||||
# please read here for more information: https://docs.gitlab.com/ce/administration/high_availability/redis.html
|
||||
# please read here for more information: https://docs.gitlab.com/ee/administration/redis/index.html
|
||||
##
|
||||
# url: redis://master:6380
|
||||
# sentinels:
|
||||
|
|
|
@ -26,7 +26,7 @@ production:
|
|||
# http://redis.io/topics/sentinel
|
||||
#
|
||||
# You must specify a list of a few sentinels that will handle client connection
|
||||
# please read here for more information: https://docs.gitlab.com/ce/administration/high_availability/redis.html
|
||||
# please read here for more information: https://docs.gitlab.com/ee/administration/redis/index.html
|
||||
##
|
||||
# url: redis://master:6381
|
||||
# sentinels:
|
||||
|
|
|
@ -26,7 +26,7 @@ production:
|
|||
# http://redis.io/topics/sentinel
|
||||
#
|
||||
# You must specify a list of a few sentinels that will handle client connection
|
||||
# please read here for more information: https://docs.gitlab.com/ce/administration/high_availability/redis.html
|
||||
# please read here for more information: https://docs.gitlab.com/ee/administration/redis/index.html
|
||||
##
|
||||
# url: redis://master:6382
|
||||
# sentinels:
|
||||
|
|
|
@ -22,7 +22,7 @@ production:
|
|||
# http://redis.io/topics/sentinel
|
||||
#
|
||||
# You must specify a list of a few sentinels that will handle client connection
|
||||
# please read here for more information: https://docs.gitlab.com/ce/administration/high_availability/redis.html
|
||||
# please read here for more information: https://docs.gitlab.com/ee/administration/redis/index.html
|
||||
##
|
||||
# url: redis://master:6379
|
||||
# sentinels:
|
||||
|
|
|
@ -368,7 +368,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
|||
resource :jira, only: [:show], controller: :jira
|
||||
end
|
||||
|
||||
resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
|
||||
resources :snippets, except: [:create, :update, :destroy], concerns: :awardable, constraints: { id: /\d+/ } do
|
||||
member do
|
||||
get :raw
|
||||
post :mark_as_spam
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
resources :snippets, concerns: :awardable do
|
||||
resources :snippets, except: [:create, :update, :destroy], concerns: :awardable, constraints: { id: /\d+/ } do
|
||||
member do
|
||||
get :raw
|
||||
post :mark_as_spam
|
||||
|
|
|
@ -3,17 +3,31 @@
|
|||
require './spec/support/sidekiq_middleware'
|
||||
|
||||
Gitlab::Seeder.quiet do
|
||||
chance_for_decrement = 0.1 # 10% chance that we'll generate smaller count than the previous count
|
||||
max_increase = 10000
|
||||
max_decrease = 1000
|
||||
|
||||
model_class = Analytics::InstanceStatistics::Measurement
|
||||
recorded_at = Date.today
|
||||
|
||||
# Insert random counts for the last 60 days
|
||||
measurements = 60.times.flat_map do
|
||||
recorded_at = (recorded_at - 1.day).end_of_day - 5.minutes
|
||||
measurements = model_class.identifiers.flat_map do |_, id|
|
||||
recorded_at = 60.days.ago
|
||||
current_count = rand(1_000_000)
|
||||
|
||||
# Insert random counts for the last 60 days
|
||||
Array.new(60) do
|
||||
recorded_at = (recorded_at + 1.day).end_of_day - 5.minutes
|
||||
|
||||
# Normally our counts should slowly increase as the gitlab instance grows.
|
||||
# Small chance (10%) to have a slight decrease (simulating cleanups, bulk delete)
|
||||
if rand < chance_for_decrement
|
||||
current_count -= rand(max_decrease)
|
||||
else
|
||||
current_count += rand(max_increase)
|
||||
end
|
||||
|
||||
model_class.identifiers.map do |_, id|
|
||||
{
|
||||
recorded_at: recorded_at,
|
||||
count: rand(1_000_000),
|
||||
count: current_count,
|
||||
identifier: id
|
||||
}
|
||||
end
|
||||
|
|
|
@ -104,12 +104,12 @@ The following table lists available parameters for jobs:
|
|||
| [`script`](#script) | Shell script that is executed by a runner. |
|
||||
| [`after_script`](#before_script-and-after_script) | Override a set of commands that are executed after job. |
|
||||
| [`allow_failure`](#allow_failure) | Allow job to fail. Failed job does not contribute to commit status. |
|
||||
| [`artifacts`](#artifacts) | List of files and directories to attach to a job on success. Also available: `artifacts:paths`, `artifacts:exclude`, `artifacts:expose_as`, `artifacts:name`, `artifacts:untracked`, `artifacts:when`, `artifacts:expire_in`, `artifacts:reports`. |
|
||||
| [`artifacts`](#artifacts) | List of files and directories to attach to a job on success. Also available: `artifacts:paths`, `artifacts:exclude`, `artifacts:expose_as`, `artifacts:name`, `artifacts:untracked`, `artifacts:when`, `artifacts:expire_in`, and `artifacts:reports`. |
|
||||
| [`before_script`](#before_script-and-after_script) | Override a set of commands that are executed before job. |
|
||||
| [`cache`](#cache) | List of files that should be cached between subsequent runs. Also available: `cache:paths`, `cache:key`, `cache:untracked`, and `cache:policy`. |
|
||||
| [`cache`](#cache) | List of files that should be cached between subsequent runs. Also available: `cache:paths`, `cache:key`, `cache:untracked`, `cache:when`, and `cache:policy`. |
|
||||
| [`coverage`](#coverage) | Code coverage settings for a given job. |
|
||||
| [`dependencies`](#dependencies) | Restrict which artifacts are passed to a specific job by providing a list of jobs to fetch artifacts from. |
|
||||
| [`environment`](#environment) | Name of an environment to which the job deploys. Also available: `environment:name`, `environment:url`, `environment:on_stop`, `environment:auto_stop_in` and `environment:action`. |
|
||||
| [`environment`](#environment) | Name of an environment to which the job deploys. Also available: `environment:name`, `environment:url`, `environment:on_stop`, `environment:auto_stop_in`, and `environment:action`. |
|
||||
| [`except`](#onlyexcept-basic) | Limit when jobs are not created. Also available: [`except:refs`, `except:kubernetes`, `except:variables`, and `except:changes`](#onlyexcept-advanced). |
|
||||
| [`extends`](#extends) | Configuration entries that this job inherits from. |
|
||||
| [`image`](#image) | Use Docker images. Also available: `image:name` and `image:entrypoint`. |
|
||||
|
@ -2914,6 +2914,28 @@ rspec:
|
|||
- binaries/
|
||||
```
|
||||
|
||||
#### `cache:when`
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/18969) in GitLab 13.5 and GitLab Runner v13.5.0.
|
||||
|
||||
`cache:when` defines when to save the cache, based on the status of the job. You can
|
||||
set `cache:when` to:
|
||||
|
||||
- `on_success` - save the cache only when the job succeeds. This is the default.
|
||||
- `on_failure` - save the cache only when the job fails.
|
||||
- `always` - save the cache regardless of the job status.
|
||||
|
||||
For example, to store a cache whether or not the job fails or succeeds:
|
||||
|
||||
```yaml
|
||||
rspec:
|
||||
script: rspec
|
||||
cache:
|
||||
paths:
|
||||
- rspec/
|
||||
when: 'always'
|
||||
```
|
||||
|
||||
#### `cache:policy`
|
||||
|
||||
> Introduced in GitLab 9.4.
|
||||
|
@ -3236,7 +3258,7 @@ failure.
|
|||
1. `on_failure` - upload artifacts only when the job fails.
|
||||
1. `always` - upload artifacts regardless of the job status.
|
||||
|
||||
To upload artifacts only when job fails:
|
||||
For example, to upload artifacts only when a job fails:
|
||||
|
||||
```yaml
|
||||
job:
|
||||
|
|
|
@ -76,6 +76,10 @@ How we use SVG for our [Icons and Illustrations](icons.md).
|
|||
|
||||
General information about frontend [dependencies](dependencies.md) and how we manage them.
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
How we implement [keyboard shortcuts](keyboard_shortcuts.md) that can be customized and disabled.
|
||||
|
||||
## Frontend FAQ
|
||||
|
||||
Read the [frontend's FAQ](frontend_faq.md) for common small pieces of helpful information.
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
# Implementing keyboard shortcuts
|
||||
|
||||
We use [Mousetrap](https://craig.is/killing/mice) to implement keyboard
|
||||
shortcuts in GitLab.
|
||||
|
||||
Mousetrap provides an API that allows keyboard shortcut strings (like
|
||||
`mod+shift+p` or `p b`) to be bound to a JavaScript handler:
|
||||
|
||||
```javascript
|
||||
// Don't do this; see note below
|
||||
Mousetrap.bind('p b', togglePerformanceBar)
|
||||
```
|
||||
|
||||
However, associating a hard-coded key sequence to a handler (as shown above)
|
||||
prevents these keyboard shortcuts from being customized or disabled by users.
|
||||
|
||||
To allow keyboard shortcuts to be customized, commands are defined in
|
||||
`~/behaviors/shortcuts/keybindings.js`. The `keysFor` method is responsible for
|
||||
returning the correct key sequence for the provided command:
|
||||
|
||||
```javascript
|
||||
import { keysFor, TOGGLE_PERFORMANCE_BAR } from '~/behaviors/shortcuts/keybindings'
|
||||
|
||||
Mousetrap.bind(keysFor(TOGGLE_PERFORMANCE_BAR), togglePerformanceBar);
|
||||
```
|
||||
|
||||
## Shortcut customization
|
||||
|
||||
`keybindings.js` stores keyboard shortcut customizations as a JSON string in
|
||||
`localStorage`. When `keybindings.js` is first imported, it fetches any
|
||||
customizations from `localStorage` and merges these customizations into the
|
||||
default set of keybindings. There is no UI to edit these customizations.
|
||||
|
||||
## Adding new shortcuts
|
||||
|
||||
Because keyboard shortcuts can be customized or disabled by end users,
|
||||
developers are encouraged to build _lots_ of keyboard shortcuts into GitLab.
|
||||
Shortcuts that are less likely to be used should be
|
||||
[disabled](#disabling-shortcuts) by default.
|
||||
|
||||
To add a new shortcut, define and export a new command string in
|
||||
`keybindings.js`:
|
||||
|
||||
```javascript
|
||||
export const MAKE_COFFEE = 'foodAndBeverage.makeCoffee';
|
||||
```
|
||||
|
||||
Next, add a new command definition under the appropriate group in the
|
||||
`keybindingGroups` array:
|
||||
|
||||
```javascript
|
||||
{
|
||||
description: s__('KeyboardShortcuts|Make coffee'),
|
||||
command: MAKE_COFFEE,
|
||||
defaultKeys: ['mod+shift+c'],
|
||||
customKeys: customizations[MAKE_COFFEE],
|
||||
}
|
||||
```
|
||||
|
||||
Finally, in the application code, import the `keysFor` function and the new
|
||||
command and bind the shortcut to the handler using Mousetrap:
|
||||
|
||||
```javascript
|
||||
import { keysFor, MAKE_COFFEE } from '~/behaviors/shortcuts/keybindings'
|
||||
|
||||
Mousetrap.bind(keysFor(MAKE_COFFEE), makeCoffee);
|
||||
```
|
||||
|
||||
See the existing the command definitions in `keybindings.js` for more examples.
|
||||
|
||||
## Disabling shortcuts
|
||||
|
||||
A shortcut can be disabled, also known as _unassigned_, by assigning the
|
||||
shortcut to an empty array `[]`. For example, to introduce a new shortcut that
|
||||
is disabled by default, a command can be defined like this:
|
||||
|
||||
```javascript
|
||||
export const MAKE_MOCHA = 'foodAndBeverage.makeMocha';
|
||||
|
||||
{
|
||||
description: s__('KeyboardShortcuts|Make a mocha'),
|
||||
command: MAKE_MOCHA,
|
||||
defaultKeys: [],
|
||||
customKeys: customizations[MAKE_MOCHA],
|
||||
}
|
||||
```
|
||||
|
||||
## Make cross-platform shortcuts
|
||||
|
||||
It's difficult to make shortcuts that work well in all platforms and browsers.
|
||||
This is one of the reasons that being able to customize and disable shortcuts is
|
||||
so important.
|
||||
|
||||
One important way to make keyboard shortcuts more portable is to use the `mod`
|
||||
shortcut string, which resolves to `command` on Mac and `ctrl` otherwise.
|
||||
|
||||
See [Mousetrap's documentation](https://craig.is/killing/mice#api.bind.combo)
|
||||
for more information.
|
|
@ -1419,26 +1419,20 @@ Example:
|
|||
|
||||
```markdown
|
||||
| header 1 | header 2 | header 3 |
|
||||
| --- | ------ |---------:|
|
||||
| --- | ------ |----------|
|
||||
| cell 1 | cell 2 | cell 3 |
|
||||
| cell 4 | cell 5 is longer | cell 6 is much longer than the others, but that's ok. It eventually wraps the text when the cell is too large for the display size. |
|
||||
| cell 7 | | cell <br> 9 |
|
||||
| cell 10 | <ul><li> - [ ] Task One </li></ul> | <ul><li> - [ ] Task Two </li><li> - [ ] Task Three </li></ul> |
|
||||
| cell 7 | | cell 9 |
|
||||
```
|
||||
|
||||
| header 1 | header 2 | header 3 |
|
||||
| --- | ------ |---------:|
|
||||
| --- | ------ |----------|
|
||||
| cell 1 | cell 2 | cell 3 |
|
||||
| cell 4 | cell 5 is longer | cell 6 is much longer than the others, but that's ok. It eventually wraps the text when the cell is too large for the display size. |
|
||||
| cell 7 | | cell <br> 9 |
|
||||
| cell 10 | <ul><li> - [ ] Task One </li></ul> | <ul><li> - [ ] Task Two </li><li> - [ ] Task Three </li></ul> |
|
||||
| cell 7 | | cell 9 |
|
||||
|
||||
Additionally, you can choose the alignment of text within columns by adding colons (`:`)
|
||||
to the sides of the "dash" lines in the second row. This affects every cell in the column.
|
||||
|
||||
NOTE: **Note:**
|
||||
[Within GitLab itself](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/user/markdown.md#tables),
|
||||
the headers are always left-aligned in Chrome and Firefox, and centered in Safari.
|
||||
to the sides of the "dash" lines in the second row. This affects every cell in the column:
|
||||
|
||||
```markdown
|
||||
| Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned |
|
||||
|
@ -1452,6 +1446,34 @@ the headers are always left-aligned in Chrome and Firefox, and centered in Safar
|
|||
| Cell 1 | Cell 2 | Cell 3 | Cell 4 | Cell 5 | Cell 6 |
|
||||
| Cell 7 | Cell 8 | Cell 9 | Cell 10 | Cell 11 | Cell 12 |
|
||||
|
||||
[Within GitLab itself](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/user/markdown.md#tables),
|
||||
the headers are always left-aligned in Chrome and Firefox, and centered in Safari.
|
||||
|
||||
You can use HTML formatting to adjust the rendering of tables. For example, you can
|
||||
use `<br>` tags to force a cell to have multiple lines:
|
||||
|
||||
```markdown
|
||||
| Name | Details |
|
||||
|------|---------|
|
||||
| Item1 | This is on one line |
|
||||
| Item2 | This item has:<br>- Multiple items<br>- That we want listed separately |
|
||||
```
|
||||
|
||||
| Name | Details |
|
||||
|------|---------|
|
||||
| Item1 | This is on one line |
|
||||
| Item2 | This item has:<br>- Multiple items<br>- That we want listed separately |
|
||||
|
||||
You can use HTML formatting within GitLab itself to add [task lists](#task-lists) with checkboxes,
|
||||
but they do not render properly on `docs.gitlab.com`:
|
||||
|
||||
```markdown
|
||||
| header 1 | header 2 |
|
||||
|----------|----------|
|
||||
| cell 1 | cell 2 |
|
||||
| cell 3 | <ul><li> - [ ] Task one </li><li> - [ ] Task two </li></ul> |
|
||||
```
|
||||
|
||||
#### Copy from spreadsheet and paste in Markdown
|
||||
|
||||
[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/27205) in GitLab 12.7.
|
||||
|
|
|
@ -469,6 +469,20 @@ Cleanup policies can be run on all projects, with these exceptions:
|
|||
|
||||
There are performance risks with enabling it for all projects, especially if you
|
||||
are using an [external registry](./index.md#use-with-external-container-registries).
|
||||
- For self-managed GitLab instances, you can enable or disable the cleanup policy for a specific
|
||||
project.
|
||||
|
||||
To enable it:
|
||||
|
||||
```ruby
|
||||
Feature.enable(:container_expiration_policies_historic_entry, Project.find(<project id>))
|
||||
```
|
||||
|
||||
To disable it:
|
||||
|
||||
```ruby
|
||||
Feature.disable(:container_expiration_policies_historic_entry, Project.find(<project id>))
|
||||
```
|
||||
|
||||
### How the cleanup policy works
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ module API
|
|||
module Entities
|
||||
module JobRequest
|
||||
class Cache < Grape::Entity
|
||||
expose :key, :untracked, :paths, :policy
|
||||
expose :key, :untracked, :paths, :policy, :when
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,14 +9,28 @@ module Gitlab
|
|||
#
|
||||
class Cache < ::Gitlab::Config::Entry::Node
|
||||
include ::Gitlab::Config::Entry::Configurable
|
||||
include ::Gitlab::Config::Entry::Validatable
|
||||
include ::Gitlab::Config::Entry::Attributable
|
||||
|
||||
ALLOWED_KEYS = %i[key untracked paths policy].freeze
|
||||
ALLOWED_KEYS = %i[key untracked paths when policy].freeze
|
||||
ALLOWED_POLICY = %w[pull-push push pull].freeze
|
||||
DEFAULT_POLICY = 'pull-push'
|
||||
ALLOWED_WHEN = %w[on_success on_failure always].freeze
|
||||
DEFAULT_WHEN = 'on_success'
|
||||
|
||||
validations do
|
||||
validates :config, allowed_keys: ALLOWED_KEYS
|
||||
validates :policy, inclusion: { in: %w[pull-push push pull], message: 'should be pull-push, push, or pull' }, allow_blank: true
|
||||
validates :config, type: Hash, allowed_keys: ALLOWED_KEYS
|
||||
validates :policy,
|
||||
inclusion: { in: ALLOWED_POLICY, message: 'should be pull-push, push, or pull' },
|
||||
allow_blank: true
|
||||
|
||||
with_options allow_nil: true do
|
||||
validates :when,
|
||||
inclusion: {
|
||||
in: ALLOWED_WHEN,
|
||||
message: 'should be on_success, on_failure or always'
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
entry :key, Entry::Key,
|
||||
|
@ -28,13 +42,15 @@ module Gitlab
|
|||
entry :paths, Entry::Paths,
|
||||
description: 'Specify which paths should be cached across builds.'
|
||||
|
||||
attributes :policy
|
||||
attributes :policy, :when
|
||||
|
||||
def value
|
||||
result = super
|
||||
|
||||
result[:key] = key_value
|
||||
result[:policy] = policy || DEFAULT_POLICY
|
||||
# Use self.when to avoid conflict with reserved word
|
||||
result[:when] = self.when || DEFAULT_WHEN
|
||||
|
||||
result
|
||||
end
|
||||
|
|
|
@ -7,7 +7,7 @@ module Gitlab
|
|||
##
|
||||
# Entry that represents a set of needs dependencies.
|
||||
#
|
||||
class Needs < ::Gitlab::Config::Entry::Node
|
||||
class Needs < ::Gitlab::Config::Entry::ComposableArray
|
||||
include ::Gitlab::Config::Entry::Validatable
|
||||
|
||||
validations do
|
||||
|
@ -29,27 +29,16 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def compose!(deps = nil)
|
||||
super(deps) do
|
||||
[@config].flatten.each_with_index do |need, index|
|
||||
@entries[index] = ::Gitlab::Config::Entry::Factory.new(Entry::Need)
|
||||
.value(need)
|
||||
.with(key: "need", parent: self, description: "need definition.") # rubocop:disable CodeReuse/ActiveRecord
|
||||
.create!
|
||||
end
|
||||
|
||||
@entries.each_value do |entry|
|
||||
entry.compose!(deps)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def value
|
||||
values = @entries.values.select(&:type)
|
||||
values = @entries.select(&:type)
|
||||
values.group_by(&:type).transform_values do |values|
|
||||
values.map(&:value)
|
||||
end
|
||||
end
|
||||
|
||||
def composable_class
|
||||
Entry::Need
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,7 +7,7 @@ module Gitlab
|
|||
##
|
||||
# Entry that represents a configuration of the ports of a Docker service.
|
||||
#
|
||||
class Ports < ::Gitlab::Config::Entry::Node
|
||||
class Ports < ::Gitlab::Config::Entry::ComposableArray
|
||||
include ::Gitlab::Config::Entry::Validatable
|
||||
|
||||
validations do
|
||||
|
@ -16,28 +16,8 @@ module Gitlab
|
|||
validates :config, port_unique: true
|
||||
end
|
||||
|
||||
def compose!(deps = nil)
|
||||
super do
|
||||
@entries = []
|
||||
@config.each do |config|
|
||||
@entries << ::Gitlab::Config::Entry::Factory.new(Entry::Port)
|
||||
.value(config || {})
|
||||
.with(key: "port", parent: self, description: "port definition.") # rubocop:disable CodeReuse/ActiveRecord
|
||||
.create!
|
||||
end
|
||||
|
||||
@entries.each do |entry|
|
||||
entry.compose!(deps)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def value
|
||||
@entries.map(&:value)
|
||||
end
|
||||
|
||||
def descendants
|
||||
@entries
|
||||
def composable_class
|
||||
Entry::Port
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,7 +4,7 @@ module Gitlab
|
|||
module Ci
|
||||
class Config
|
||||
module Entry
|
||||
class Rules < ::Gitlab::Config::Entry::Node
|
||||
class Rules < ::Gitlab::Config::Entry::ComposableArray
|
||||
include ::Gitlab::Config::Entry::Validatable
|
||||
|
||||
validations do
|
||||
|
@ -12,24 +12,13 @@ module Gitlab
|
|||
validates :config, type: Array
|
||||
end
|
||||
|
||||
def compose!(deps = nil)
|
||||
super(deps) do
|
||||
@config.each_with_index do |rule, index|
|
||||
@entries[index] = ::Gitlab::Config::Entry::Factory.new(Entry::Rules::Rule)
|
||||
.value(rule)
|
||||
.with(key: "rule", parent: self, description: "rule definition.") # rubocop:disable CodeReuse/ActiveRecord
|
||||
.create!
|
||||
end
|
||||
|
||||
@entries.each_value do |entry|
|
||||
entry.compose!(deps)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def value
|
||||
@config
|
||||
end
|
||||
|
||||
def composable_class
|
||||
Entry::Rules::Rule
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,7 +7,7 @@ module Gitlab
|
|||
##
|
||||
# Entry that represents a configuration of Docker services.
|
||||
#
|
||||
class Services < ::Gitlab::Config::Entry::Node
|
||||
class Services < ::Gitlab::Config::Entry::ComposableArray
|
||||
include ::Gitlab::Config::Entry::Validatable
|
||||
|
||||
validations do
|
||||
|
@ -15,28 +15,8 @@ module Gitlab
|
|||
validates :config, services_with_ports_alias_unique: true, if: ->(record) { record.opt(:with_image_ports) }
|
||||
end
|
||||
|
||||
def compose!(deps = nil)
|
||||
super do
|
||||
@entries = []
|
||||
@config.each do |config|
|
||||
@entries << ::Gitlab::Config::Entry::Factory.new(Entry::Service)
|
||||
.value(config || {})
|
||||
.with(key: "service", parent: self, description: "service definition.") # rubocop:disable CodeReuse/ActiveRecord
|
||||
.create!
|
||||
end
|
||||
|
||||
@entries.each do |entry|
|
||||
entry.compose!(deps)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def value
|
||||
@entries.map(&:value)
|
||||
end
|
||||
|
||||
def descendants
|
||||
@entries
|
||||
def composable_class
|
||||
Entry::Service
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,6 +13,7 @@ module Gitlab
|
|||
@paths = local_cache.delete(:paths)
|
||||
@policy = local_cache.delete(:policy)
|
||||
@untracked = local_cache.delete(:untracked)
|
||||
@when = local_cache.delete(:when)
|
||||
|
||||
raise ArgumentError, "unknown cache keys: #{local_cache.keys}" if local_cache.any?
|
||||
end
|
||||
|
@ -24,7 +25,8 @@ module Gitlab
|
|||
key: key_string,
|
||||
paths: @paths,
|
||||
policy: @policy,
|
||||
untracked: @untracked
|
||||
untracked: @untracked,
|
||||
when: @when
|
||||
}.compact.presence
|
||||
}.compact
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Config
|
||||
module Entry
|
||||
##
|
||||
# Entry that represents a composable array definition
|
||||
#
|
||||
class ComposableArray < ::Gitlab::Config::Entry::Node
|
||||
include ::Gitlab::Config::Entry::Validatable
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
# TODO: Refactor `Validatable` code so that validations can apply to a child class
|
||||
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/263231
|
||||
validations do
|
||||
validates :config, type: Array
|
||||
end
|
||||
|
||||
def compose!(deps = nil)
|
||||
super do
|
||||
@entries = Array(@entries)
|
||||
|
||||
# TODO: Isolate handling for a hash via: `[@config].flatten` to the `Needs` entry
|
||||
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/264376
|
||||
[@config].flatten.each_with_index do |value, index|
|
||||
raise ArgumentError, 'Missing Composable class' unless composable_class
|
||||
|
||||
composable_class_name = composable_class.name.demodulize.underscore
|
||||
|
||||
@entries << ::Gitlab::Config::Entry::Factory.new(composable_class)
|
||||
.value(value)
|
||||
.with(key: composable_class_name, parent: self, description: "#{composable_class_name} definition") # rubocop:disable CodeReuse/ActiveRecord
|
||||
.create!
|
||||
end
|
||||
|
||||
@entries.each do |entry|
|
||||
entry.compose!(deps)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def value
|
||||
@entries.map(&:value)
|
||||
end
|
||||
|
||||
def descendants
|
||||
@entries
|
||||
end
|
||||
|
||||
def composable_class
|
||||
strong_memoize(:composable_class) do
|
||||
opt(:composable_class)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -10,7 +10,7 @@ module Gitlab
|
|||
class ComposableHash < ::Gitlab::Config::Entry::Node
|
||||
include ::Gitlab::Config::Entry::Validatable
|
||||
|
||||
# TODO: Refactor Validatable so these validations will not apply to a child class
|
||||
# TODO: Refactor `Validatable` code so that validations can apply to a child class
|
||||
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/263231
|
||||
validations do
|
||||
validates :config, type: Hash
|
||||
|
|
|
@ -37,7 +37,7 @@ module SystemCheck
|
|||
@custom_error_message
|
||||
)
|
||||
for_more_information(
|
||||
'doc/administration/high_availability/redis.md#provide-your-own-redis-instance'
|
||||
'doc/administration/redis/index.html#redis-replication-and-failover-using-the-non-bundled-redis'
|
||||
)
|
||||
fix_and_rerun
|
||||
end
|
||||
|
|
|
@ -38,7 +38,7 @@ namespace :gettext do
|
|||
Rake::Task['gettext:find'].invoke
|
||||
|
||||
# leave only the required changes.
|
||||
unless system(*%w(git checkout -- locale/*/gitlab.po))
|
||||
unless system(*%w(git -c core.hooksPath=/dev/null checkout -- locale/*/gitlab.po))
|
||||
raise 'failed to cleanup generated locale/*/gitlab.po files'
|
||||
end
|
||||
|
||||
|
|
|
@ -14773,6 +14773,12 @@ msgstr ""
|
|||
msgid "KeyboardKey|Ctrl+"
|
||||
msgstr ""
|
||||
|
||||
msgid "KeyboardShortcuts|Global Shortcuts"
|
||||
msgstr ""
|
||||
|
||||
msgid "KeyboardShortcuts|Toggle the Performance Bar"
|
||||
msgstr ""
|
||||
|
||||
msgid "Keys"
|
||||
msgstr ""
|
||||
|
||||
|
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
After Width: | Height: | Size: 82 KiB |
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 119 KiB |
|
@ -30,6 +30,7 @@ module QA
|
|||
view 'app/assets/javascripts/design_management/components/list/item.vue' do
|
||||
element :design_file_name
|
||||
element :design_image
|
||||
element :design_status_icon
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/design_management/pages/index.vue' do
|
||||
|
@ -79,6 +80,11 @@ module QA
|
|||
raise ElementNotFound, %Q(Attempted to attach design "#{filename}" but it did not appear) unless found
|
||||
end
|
||||
|
||||
def update_design(filename)
|
||||
filepath = ::File.join('qa', 'fixtures', 'designs', 'update', filename)
|
||||
add_design(filepath)
|
||||
end
|
||||
|
||||
def click_design(filename)
|
||||
click_element(:design_file_name, text: filename)
|
||||
end
|
||||
|
@ -101,6 +107,14 @@ module QA
|
|||
def has_design?(filename)
|
||||
has_element?(:design_file_name, text: filename)
|
||||
end
|
||||
|
||||
def has_created_icon?
|
||||
has_element?(:design_status_icon, status: 'file-addition-solid')
|
||||
end
|
||||
|
||||
def has_modified_icon?
|
||||
has_element?(:design_status_icon, status: 'file-modified-solid')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,18 +3,15 @@
|
|||
module QA
|
||||
module Resource
|
||||
class Design < Base
|
||||
attr_reader :id
|
||||
attr_accessor :filename
|
||||
|
||||
attribute :issue do
|
||||
Issue.fabricate_via_api!
|
||||
end
|
||||
|
||||
attribute :filepath do
|
||||
::File.absolute_path(::File.join('spec', 'fixtures', @filename))
|
||||
end
|
||||
|
||||
attribute :id
|
||||
attribute :filename
|
||||
|
||||
def initialize
|
||||
@update = false
|
||||
@filename = 'banana_sample.gif'
|
||||
end
|
||||
|
||||
|
@ -26,6 +23,12 @@ module QA
|
|||
issue.add_design(filepath)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filepath
|
||||
::File.absolute_path(::File.join('qa', 'fixtures', 'designs', @filename))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -41,7 +41,7 @@ module QA
|
|||
context 'when using attachments in comments', :object_storage do
|
||||
let(:gif_file_name) { 'banana_sample.gif' }
|
||||
let(:file_to_attach) do
|
||||
File.absolute_path(File.join('spec', 'fixtures', gif_file_name))
|
||||
File.absolute_path(File.join('qa', 'fixtures', 'designs', gif_file_name))
|
||||
end
|
||||
|
||||
before do
|
||||
|
|
|
@ -5,7 +5,7 @@ module QA
|
|||
context 'Design Management' do
|
||||
let(:issue) { Resource::Issue.fabricate_via_api! }
|
||||
let(:design_filename) { 'banana_sample.gif' }
|
||||
let(:design) { File.absolute_path(File.join('spec', 'fixtures', design_filename)) }
|
||||
let(:design) { File.absolute_path(File.join('qa', 'fixtures', 'designs', design_filename)) }
|
||||
let(:annotation) { "This design is great!" }
|
||||
|
||||
before do
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
RSpec.describe 'Create' do
|
||||
context 'Design Management' do
|
||||
let(:design) do
|
||||
Resource::Design.fabricate! do |design|
|
||||
design.filename = 'tanuki.jpg'
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
Flow::Login.sign_in
|
||||
end
|
||||
|
||||
it 'user adds a design and modifies it', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/273' do
|
||||
design.issue.visit!
|
||||
|
||||
Page::Project::Issue::Show.perform do |issue|
|
||||
expect(issue).to have_created_icon
|
||||
end
|
||||
|
||||
Page::Project::Issue::Show.perform do |issue|
|
||||
issue.update_design(design.filename)
|
||||
expect(issue).to have_modified_icon
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -82,215 +82,6 @@ RSpec.describe Projects::SnippetsController do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'POST #create' do
|
||||
def create_snippet(project, snippet_params = {}, additional_params = {})
|
||||
sign_in(user)
|
||||
|
||||
project.add_developer(user)
|
||||
|
||||
post :create, params: {
|
||||
namespace_id: project.namespace.to_param,
|
||||
project_id: project,
|
||||
project_snippet: { title: 'Title', content: 'Content', description: 'Description' }.merge(snippet_params)
|
||||
}.merge(additional_params)
|
||||
|
||||
Snippet.last
|
||||
end
|
||||
|
||||
it 'creates the snippet correctly' do
|
||||
snippet = create_snippet(project, visibility_level: Snippet::PRIVATE)
|
||||
|
||||
expect(snippet.title).to eq('Title')
|
||||
expect(snippet.content).to eq('Content')
|
||||
expect(snippet.description).to eq('Description')
|
||||
end
|
||||
|
||||
context 'when the snippet is spam' do
|
||||
before do
|
||||
allow_next_instance_of(Spam::AkismetService) do |instance|
|
||||
allow(instance).to receive(:spam?).and_return(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the snippet is private' do
|
||||
it 'creates the snippet' do
|
||||
expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }
|
||||
.to change { Snippet.count }.by(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the snippet is public' do
|
||||
it 'rejects the snippet' do
|
||||
expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }
|
||||
.not_to change { Snippet.count }
|
||||
expect(response).to render_template(:new)
|
||||
end
|
||||
|
||||
it 'creates a spam log' do
|
||||
expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }
|
||||
.to log_spam(title: 'Title', user_id: user.id, noteable_type: 'ProjectSnippet')
|
||||
end
|
||||
|
||||
it 'renders :new with reCAPTCHA disabled' do
|
||||
stub_application_setting(recaptcha_enabled: false)
|
||||
|
||||
create_snippet(project, visibility_level: Snippet::PUBLIC)
|
||||
|
||||
expect(response).to render_template(:new)
|
||||
end
|
||||
|
||||
context 'reCAPTCHA enabled' do
|
||||
before do
|
||||
stub_application_setting(recaptcha_enabled: true)
|
||||
end
|
||||
|
||||
it 'renders :verify with reCAPTCHA enabled' do
|
||||
create_snippet(project, visibility_level: Snippet::PUBLIC)
|
||||
|
||||
expect(response).to render_template(:verify)
|
||||
end
|
||||
|
||||
it 'renders snippet page when reCAPTCHA verified' do
|
||||
spammy_title = 'Whatever'
|
||||
|
||||
spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
|
||||
create_snippet(project,
|
||||
{ visibility_level: Snippet::PUBLIC },
|
||||
{ spam_log_id: spam_logs.last.id,
|
||||
recaptcha_verification: true })
|
||||
|
||||
expect(response).to redirect_to(project_snippet_path(project, Snippet.last))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT #update' do
|
||||
let(:visibility_level) { Snippet::PUBLIC }
|
||||
let(:snippet) { create :project_snippet, author: user, project: project, visibility_level: visibility_level }
|
||||
|
||||
def update_snippet(snippet_params = {}, additional_params = {})
|
||||
sign_in(user)
|
||||
|
||||
project.add_developer(user)
|
||||
|
||||
put :update, params: {
|
||||
namespace_id: project.namespace.to_param,
|
||||
project_id: project,
|
||||
id: snippet,
|
||||
project_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params)
|
||||
}.merge(additional_params)
|
||||
|
||||
snippet.reload
|
||||
end
|
||||
|
||||
context 'when the snippet is spam' do
|
||||
before do
|
||||
allow_next_instance_of(Spam::AkismetService) do |instance|
|
||||
allow(instance).to receive(:spam?).and_return(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the snippet is private' do
|
||||
let(:visibility_level) { Snippet::PRIVATE }
|
||||
|
||||
it 'updates the snippet' do
|
||||
expect { update_snippet(title: 'Foo') }
|
||||
.to change { snippet.reload.title }.to('Foo')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the snippet is public' do
|
||||
it 'rejects the snippet' do
|
||||
expect { update_snippet(title: 'Foo') }
|
||||
.not_to change { snippet.reload.title }
|
||||
end
|
||||
|
||||
it 'creates a spam log' do
|
||||
expect { update_snippet(title: 'Foo') }
|
||||
.to log_spam(title: 'Foo', user_id: user.id, noteable_type: 'ProjectSnippet')
|
||||
end
|
||||
|
||||
it 'renders :edit with reCAPTCHA disabled' do
|
||||
stub_application_setting(recaptcha_enabled: false)
|
||||
|
||||
update_snippet(title: 'Foo')
|
||||
|
||||
expect(response).to render_template(:edit)
|
||||
end
|
||||
|
||||
context 'reCAPTCHA enabled' do
|
||||
before do
|
||||
stub_application_setting(recaptcha_enabled: true)
|
||||
end
|
||||
|
||||
it 'renders :verify with reCAPTCHA enabled' do
|
||||
update_snippet(title: 'Foo')
|
||||
|
||||
expect(response).to render_template(:verify)
|
||||
end
|
||||
|
||||
it 'renders snippet page when reCAPTCHA verified' do
|
||||
spammy_title = 'Whatever'
|
||||
|
||||
spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
|
||||
snippet = update_snippet({ title: spammy_title },
|
||||
{ spam_log_id: spam_logs.last.id,
|
||||
recaptcha_verification: true })
|
||||
|
||||
expect(response).to redirect_to(project_snippet_path(project, snippet))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the private snippet is made public' do
|
||||
let(:visibility_level) { Snippet::PRIVATE }
|
||||
|
||||
it 'rejects the snippet' do
|
||||
expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }
|
||||
.not_to change { snippet.reload.title }
|
||||
end
|
||||
|
||||
it 'creates a spam log' do
|
||||
expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }
|
||||
.to log_spam(title: 'Foo', user_id: user.id, noteable_type: 'ProjectSnippet')
|
||||
end
|
||||
|
||||
it 'renders :edit with reCAPTCHA disabled' do
|
||||
stub_application_setting(recaptcha_enabled: false)
|
||||
|
||||
update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC)
|
||||
|
||||
expect(response).to render_template(:edit)
|
||||
end
|
||||
|
||||
context 'reCAPTCHA enabled' do
|
||||
before do
|
||||
stub_application_setting(recaptcha_enabled: true)
|
||||
end
|
||||
|
||||
it 'renders :verify' do
|
||||
update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC)
|
||||
|
||||
expect(response).to render_template(:verify)
|
||||
end
|
||||
|
||||
it 'renders snippet page' do
|
||||
spammy_title = 'Whatever'
|
||||
|
||||
spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
|
||||
snippet = update_snippet({ title: spammy_title, visibility_level: Snippet::PUBLIC },
|
||||
{ spam_log_id: spam_logs.last.id,
|
||||
recaptcha_verification: true })
|
||||
|
||||
expect(response).to redirect_to(project_snippet_path(project, snippet))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #mark_as_spam' do
|
||||
let_it_be(:snippet) { create(:project_snippet, :private, project: project, author: user) }
|
||||
|
||||
|
@ -329,12 +120,6 @@ RSpec.describe Projects::SnippetsController do
|
|||
expect(assigns(:snippet)).to eq(project_snippet)
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'renders the blob from the repository' do
|
||||
subject
|
||||
|
||||
expect(assigns(:blob)).to eq(project_snippet.blobs.first)
|
||||
end
|
||||
end
|
||||
|
||||
%w[show raw].each do |action|
|
||||
|
@ -395,6 +180,16 @@ RSpec.describe Projects::SnippetsController do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'GET #show as JSON' do
|
||||
it 'renders the blob from the repository' do
|
||||
project_snippet = create(:project_snippet, :public, :repository, project: project, author: user)
|
||||
|
||||
get :show, params: { namespace_id: project.namespace, project_id: project, id: project_snippet.to_param }, format: :json
|
||||
|
||||
expect(assigns(:blob)).to eq(project_snippet.blobs.first)
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET #show for embeddable content" do
|
||||
let(:project_snippet) { create(:project_snippet, :repository, snippet_permission, project: project, author: user) }
|
||||
let(:extra_params) { {} }
|
||||
|
@ -533,62 +328,4 @@ RSpec.describe Projects::SnippetsController do
|
|||
it_behaves_like 'content disposition headers'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE #destroy' do
|
||||
let_it_be(:snippet) { create(:project_snippet, :private, project: project, author: user) }
|
||||
|
||||
let(:params) do
|
||||
{
|
||||
namespace_id: project.namespace.to_param,
|
||||
project_id: project,
|
||||
id: snippet.to_param
|
||||
}
|
||||
end
|
||||
|
||||
subject { delete :destroy, params: params }
|
||||
|
||||
context 'when current user has ability to destroy the snippet' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'removes the snippet' do
|
||||
subject
|
||||
|
||||
expect { snippet.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
context 'when snippet is succesfuly destroyed' do
|
||||
it 'redirects to the project snippets page' do
|
||||
subject
|
||||
|
||||
expect(response).to redirect_to(project_snippets_path(project))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when snippet is not destroyed' do
|
||||
before do
|
||||
allow(snippet).to receive(:destroy).and_return(false)
|
||||
controller.instance_variable_set(:@snippet, snippet)
|
||||
end
|
||||
|
||||
it 'renders the snippet page with errors' do
|
||||
subject
|
||||
|
||||
expect(flash[:alert]).to eq('Failed to remove snippet.')
|
||||
expect(response).to redirect_to(project_snippet_path(project, snippet))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when current_user does not have ability to destroy the snippet' do
|
||||
it 'responds with status 404' do
|
||||
sign_in(other_user)
|
||||
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -86,12 +86,6 @@ RSpec.describe SnippetsController do
|
|||
expect(assigns(:snippet)).to eq(personal_snippet)
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'renders the blob from the repository' do
|
||||
subject
|
||||
|
||||
expect(assigns(:blob)).to eq(personal_snippet.blobs.first)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the personal snippet is private' do
|
||||
|
@ -200,7 +194,7 @@ RSpec.describe SnippetsController do
|
|||
end
|
||||
|
||||
it 'responds with status 404' do
|
||||
get :show, params: { id: 'doesntexist' }
|
||||
get :show, params: { id: non_existing_record_id }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
|
@ -208,234 +202,20 @@ RSpec.describe SnippetsController do
|
|||
|
||||
context 'when not signed in' do
|
||||
it 'responds with status 404' do
|
||||
get :show, params: { id: 'doesntexist' }
|
||||
get :show, params: { id: non_existing_record_id }
|
||||
|
||||
expect(response).to redirect_to(new_user_session_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #create' do
|
||||
def create_snippet(snippet_params = {}, additional_params = {})
|
||||
sign_in(user)
|
||||
context 'when requesting JSON' do
|
||||
it 'renders the blob from the repository' do
|
||||
personal_snippet = create(:personal_snippet, :public, :repository, author: user)
|
||||
|
||||
post :create, params: {
|
||||
personal_snippet: { title: 'Title', content: 'Content', description: 'Description' }.merge(snippet_params)
|
||||
}.merge(additional_params)
|
||||
get :show, params: { id: personal_snippet.to_param }, format: :json
|
||||
|
||||
Snippet.last
|
||||
end
|
||||
|
||||
it 'creates the snippet correctly' do
|
||||
snippet = create_snippet(visibility_level: Snippet::PRIVATE)
|
||||
|
||||
expect(snippet.title).to eq('Title')
|
||||
expect(snippet.content).to eq('Content')
|
||||
expect(snippet.description).to eq('Description')
|
||||
end
|
||||
|
||||
context 'when user is not allowed to create a personal snippet' do
|
||||
let(:user) { create(:user, :external) }
|
||||
|
||||
it 'responds with status 404' do
|
||||
aggregate_failures do
|
||||
expect do
|
||||
create_snippet(visibility_level: Snippet::PUBLIC)
|
||||
end.not_to change { Snippet.count }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the controller receives the files param' do
|
||||
let(:files) { %w(foo bar) }
|
||||
|
||||
it 'passes the files param to the snippet create service' do
|
||||
expect(Snippets::CreateService).to receive(:new).with(nil, user, hash_including(files: files)).and_call_original
|
||||
|
||||
create_snippet({ title: nil }, { files: files })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the snippet is spam' do
|
||||
before do
|
||||
allow_next_instance_of(Spam::AkismetService) do |instance|
|
||||
allow(instance).to receive(:spam?).and_return(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the snippet is private' do
|
||||
it 'creates the snippet' do
|
||||
expect { create_snippet(visibility_level: Snippet::PRIVATE) }
|
||||
.to change { Snippet.count }.by(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the snippet is public' do
|
||||
it 'rejects the snippet' do
|
||||
expect { create_snippet(visibility_level: Snippet::PUBLIC) }
|
||||
.not_to change { Snippet.count }
|
||||
end
|
||||
|
||||
it 'creates a spam log' do
|
||||
expect { create_snippet(visibility_level: Snippet::PUBLIC) }
|
||||
.to log_spam(title: 'Title', user: user, noteable_type: 'PersonalSnippet')
|
||||
end
|
||||
|
||||
it 'renders :new with reCAPTCHA disabled' do
|
||||
stub_application_setting(recaptcha_enabled: false)
|
||||
|
||||
create_snippet(visibility_level: Snippet::PUBLIC)
|
||||
|
||||
expect(response).to render_template(:new)
|
||||
end
|
||||
|
||||
context 'reCAPTCHA enabled' do
|
||||
before do
|
||||
stub_application_setting(recaptcha_enabled: true)
|
||||
end
|
||||
|
||||
it 'renders :verify' do
|
||||
create_snippet(visibility_level: Snippet::PUBLIC)
|
||||
|
||||
expect(response).to render_template(:verify)
|
||||
end
|
||||
|
||||
it 'renders snippet page' do
|
||||
spammy_title = 'Whatever'
|
||||
|
||||
spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
|
||||
snippet = create_snippet({ title: spammy_title },
|
||||
{ spam_log_id: spam_logs.last.id,
|
||||
recaptcha_verification: true })
|
||||
|
||||
expect(response).to redirect_to(snippet_path(snippet))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT #update' do
|
||||
let(:project) { create :project }
|
||||
let(:visibility_level) { Snippet::PUBLIC }
|
||||
let(:snippet) { create :personal_snippet, author: user, project: project, visibility_level: visibility_level }
|
||||
|
||||
def update_snippet(snippet_params = {}, additional_params = {})
|
||||
sign_in(user)
|
||||
|
||||
put :update, params: {
|
||||
id: snippet.id,
|
||||
personal_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params)
|
||||
}.merge(additional_params)
|
||||
|
||||
snippet.reload
|
||||
end
|
||||
|
||||
context 'when the snippet is spam' do
|
||||
before do
|
||||
allow_next_instance_of(Spam::AkismetService) do |instance|
|
||||
allow(instance).to receive(:spam?).and_return(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the snippet is private' do
|
||||
let(:visibility_level) { Snippet::PRIVATE }
|
||||
|
||||
it 'updates the snippet' do
|
||||
expect { update_snippet(title: 'Foo') }
|
||||
.to change { snippet.reload.title }.to('Foo')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a private snippet is made public' do
|
||||
let(:visibility_level) { Snippet::PRIVATE }
|
||||
|
||||
it 'rejects the snippet' do
|
||||
expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }
|
||||
.not_to change { snippet.reload.title }
|
||||
end
|
||||
|
||||
it 'creates a spam log' do
|
||||
expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }
|
||||
.to log_spam(title: 'Foo', user: user, noteable_type: 'PersonalSnippet')
|
||||
end
|
||||
|
||||
it 'renders :edit with reCAPTCHA disabled' do
|
||||
stub_application_setting(recaptcha_enabled: false)
|
||||
|
||||
update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC)
|
||||
|
||||
expect(response).to render_template(:edit)
|
||||
end
|
||||
|
||||
context 'reCAPTCHA enabled' do
|
||||
before do
|
||||
stub_application_setting(recaptcha_enabled: true)
|
||||
end
|
||||
|
||||
it 'renders :verify' do
|
||||
update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC)
|
||||
|
||||
expect(response).to render_template(:verify)
|
||||
end
|
||||
|
||||
it 'renders snippet page when reCAPTCHA verified' do
|
||||
spammy_title = 'Whatever'
|
||||
|
||||
spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
|
||||
snippet = update_snippet({ title: spammy_title, visibility_level: Snippet::PUBLIC },
|
||||
{ spam_log_id: spam_logs.last.id,
|
||||
recaptcha_verification: true })
|
||||
|
||||
expect(response).to redirect_to(snippet_path(snippet))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the snippet is public' do
|
||||
it 'rejects the snippet' do
|
||||
expect { update_snippet(title: 'Foo') }
|
||||
.not_to change { snippet.reload.title }
|
||||
end
|
||||
|
||||
it 'creates a spam log' do
|
||||
expect {update_snippet(title: 'Foo') }
|
||||
.to log_spam(title: 'Foo', user: user, noteable_type: 'PersonalSnippet')
|
||||
end
|
||||
|
||||
it 'renders :edit with reCAPTCHA disabled' do
|
||||
stub_application_setting(recaptcha_enabled: false)
|
||||
|
||||
update_snippet(title: 'Foo')
|
||||
|
||||
expect(response).to render_template(:edit)
|
||||
end
|
||||
|
||||
context 'recaptcha enabled' do
|
||||
before do
|
||||
stub_application_setting(recaptcha_enabled: true)
|
||||
end
|
||||
|
||||
it 'renders :verify' do
|
||||
update_snippet(title: 'Foo')
|
||||
|
||||
expect(response).to render_template(:verify)
|
||||
end
|
||||
|
||||
it 'renders snippet page when reCAPTCHA verified' do
|
||||
spammy_title = 'Whatever'
|
||||
|
||||
spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
|
||||
snippet = update_snippet({ title: spammy_title },
|
||||
{ spam_log_id: spam_logs.last.id,
|
||||
recaptcha_verification: true })
|
||||
|
||||
expect(response).to redirect_to(snippet_path(snippet))
|
||||
end
|
||||
end
|
||||
expect(assigns(:blob)).to eq(personal_snippet.blobs.first)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -632,7 +412,7 @@ RSpec.describe SnippetsController do
|
|||
end
|
||||
|
||||
it 'responds with status 404' do
|
||||
get :raw, params: { id: 'doesntexist' }
|
||||
get :raw, params: { id: non_existing_record_id }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
|
@ -640,7 +420,7 @@ RSpec.describe SnippetsController do
|
|||
|
||||
context 'when not signed in' do
|
||||
it 'redirects to the sign in path' do
|
||||
get :raw, params: { id: 'doesntexist' }
|
||||
get :raw, params: { id: non_existing_record_id }
|
||||
|
||||
expect(response).to redirect_to(new_user_session_path)
|
||||
end
|
||||
|
@ -688,56 +468,4 @@ RSpec.describe SnippetsController do
|
|||
expect(json_response.keys).to match_array(%w(body references))
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE #destroy' do
|
||||
let!(:snippet) { create :personal_snippet, author: user }
|
||||
|
||||
context 'when current user has ability to destroy the snippet' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'removes the snippet' do
|
||||
delete :destroy, params: { id: snippet.to_param }
|
||||
|
||||
expect { snippet.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
context 'when snippet is succesfuly destroyed' do
|
||||
it 'redirects to the project snippets page' do
|
||||
delete :destroy, params: { id: snippet.to_param }
|
||||
|
||||
expect(response).to redirect_to(dashboard_snippets_path)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when snippet is not destroyed' do
|
||||
before do
|
||||
allow(snippet).to receive(:destroy).and_return(false)
|
||||
controller.instance_variable_set(:@snippet, snippet)
|
||||
end
|
||||
|
||||
it 'renders the snippet page with errors' do
|
||||
delete :destroy, params: { id: snippet.to_param }
|
||||
|
||||
expect(flash[:alert]).to eq('Failed to remove snippet.')
|
||||
expect(response).to redirect_to(snippet_path(snippet))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when current_user does not have ability to destroy the snippet' do
|
||||
let(:another_user) { create(:user) }
|
||||
|
||||
before do
|
||||
sign_in(another_user)
|
||||
end
|
||||
|
||||
it 'responds with status 404' do
|
||||
delete :destroy, params: { id: snippet.to_param }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -384,7 +384,8 @@ FactoryBot.define do
|
|||
key: 'cache_key',
|
||||
untracked: false,
|
||||
paths: ['vendor/*'],
|
||||
policy: 'pull-push'
|
||||
policy: 'pull-push',
|
||||
when: 'on_success'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
|
@ -19,6 +19,12 @@ RSpec.describe 'Dashboard Merge Requests' do
|
|||
sign_in(current_user)
|
||||
end
|
||||
|
||||
it 'disables target branch filter' do
|
||||
visit merge_requests_dashboard_path
|
||||
|
||||
expect(page).not_to have_selector('#js-dropdown-target-branch', visible: false)
|
||||
end
|
||||
|
||||
context 'new merge request dropdown' do
|
||||
let(:project_with_disabled_merge_requests) { create(:project, :merge_requests_disabled) }
|
||||
|
||||
|
|
|
@ -3,27 +3,35 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration policy', :js do
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project, namespace: user.namespace, container_registry_enabled: container_registry_enabled) }
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:project, reload: true) { create(:project, namespace: user.namespace) }
|
||||
|
||||
let(:container_registry_enabled) { true }
|
||||
let(:container_registry_enabled_on_project) { true }
|
||||
|
||||
subject { visit project_settings_ci_cd_path(project) }
|
||||
|
||||
before do
|
||||
project.update!(container_registry_enabled: container_registry_enabled_on_project)
|
||||
|
||||
sign_in(user)
|
||||
stub_container_registry_config(enabled: true)
|
||||
stub_container_registry_config(enabled: container_registry_enabled)
|
||||
stub_feature_flags(new_variables_ui: false)
|
||||
end
|
||||
|
||||
context 'as owner' do
|
||||
before do
|
||||
visit project_settings_ci_cd_path(project)
|
||||
end
|
||||
|
||||
it 'shows available section' do
|
||||
subject
|
||||
|
||||
settings_block = find('#js-registry-policies')
|
||||
expect(settings_block).to have_text 'Cleanup policy for tags'
|
||||
end
|
||||
|
||||
it 'saves cleanup policy submit the form' do
|
||||
subject
|
||||
|
||||
within '#js-registry-policies' do
|
||||
within '.card-body' do
|
||||
select('7 days until tags are automatically removed', from: 'Expiration interval:')
|
||||
|
@ -40,6 +48,8 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
|
|||
end
|
||||
|
||||
it 'does not save cleanup policy submit form with invalid regex' do
|
||||
subject
|
||||
|
||||
within '#js-registry-policies' do
|
||||
within '.card-body' do
|
||||
fill_in('Tags with names matching this regex pattern will expire:', with: '*-production')
|
||||
|
@ -53,25 +63,53 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
|
|||
end
|
||||
end
|
||||
|
||||
context 'when registry is disabled' do
|
||||
before do
|
||||
stub_container_registry_config(enabled: false)
|
||||
visit project_settings_ci_cd_path(project)
|
||||
context 'with a project without expiration policy' do
|
||||
where(:application_setting, :feature_flag, :result) do
|
||||
true | true | :available_section
|
||||
true | false | :available_section
|
||||
false | true | :available_section
|
||||
false | false | :disabled_message
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
project.container_expiration_policy.destroy!
|
||||
stub_feature_flags(container_expiration_policies_historic_entry: false)
|
||||
stub_application_setting(container_expiration_policies_enable_historic_entries: application_setting)
|
||||
stub_feature_flags(container_expiration_policies_historic_entry: project) if feature_flag
|
||||
end
|
||||
|
||||
it 'displays the expected result' do
|
||||
subject
|
||||
|
||||
within '#js-registry-policies' do
|
||||
case result
|
||||
when :available_section
|
||||
expect(find('.card-header')).to have_content('Tag expiration policy')
|
||||
when :disabled_message
|
||||
expect(find('.gl-alert-title')).to have_content('Cleanup policy for tags is disabled')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when registry is disabled' do
|
||||
let(:container_registry_enabled) { false }
|
||||
|
||||
it 'does not exists' do
|
||||
subject
|
||||
|
||||
expect(page).not_to have_selector('#js-registry-policies')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when container registry is disabled on project' do
|
||||
let(:container_registry_enabled) { false }
|
||||
|
||||
before do
|
||||
visit project_settings_ci_cd_path(project)
|
||||
end
|
||||
let(:container_registry_enabled_on_project) { false }
|
||||
|
||||
it 'does not exists' do
|
||||
subject
|
||||
|
||||
expect(page).not_to have_selector('#js-registry-policies')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
|
||||
|
||||
describe('~/behaviors/shortcuts/keybindings.js', () => {
|
||||
let keysFor;
|
||||
let TOGGLE_PERFORMANCE_BAR;
|
||||
let LOCAL_STORAGE_KEY;
|
||||
|
||||
beforeAll(() => {
|
||||
useLocalStorageSpy();
|
||||
});
|
||||
|
||||
const setupCustomizations = async customizationsAsString => {
|
||||
localStorage.clear();
|
||||
|
||||
if (customizationsAsString) {
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, customizationsAsString);
|
||||
}
|
||||
|
||||
jest.resetModules();
|
||||
({ keysFor, TOGGLE_PERFORMANCE_BAR, LOCAL_STORAGE_KEY } = await import(
|
||||
'~/behaviors/shortcuts/keybindings'
|
||||
));
|
||||
};
|
||||
|
||||
describe('when a command has not been customized', () => {
|
||||
beforeEach(async () => {
|
||||
await setupCustomizations('{}');
|
||||
});
|
||||
|
||||
it('returns the default keybinding for the command', () => {
|
||||
expect(keysFor(TOGGLE_PERFORMANCE_BAR)).toEqual(['p b']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a command has been customized', () => {
|
||||
const customization = ['p b a r'];
|
||||
|
||||
beforeEach(async () => {
|
||||
await setupCustomizations(JSON.stringify({ [TOGGLE_PERFORMANCE_BAR]: customization }));
|
||||
});
|
||||
|
||||
it('returns the default keybinding for the command', () => {
|
||||
expect(keysFor(TOGGLE_PERFORMANCE_BAR)).toEqual(customization);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the localStorage entry isn't valid JSON", () => {
|
||||
beforeEach(async () => {
|
||||
await setupCustomizations('{');
|
||||
});
|
||||
|
||||
it('returns the default keybinding for the command', () => {
|
||||
expect(keysFor(TOGGLE_PERFORMANCE_BAR)).toEqual(['p b']);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when localStorage doesn't contain the ${LOCAL_STORAGE_KEY} key`, () => {
|
||||
beforeEach(async () => {
|
||||
await setupCustomizations();
|
||||
});
|
||||
|
||||
it('returns the default keybinding for the command', () => {
|
||||
expect(keysFor(TOGGLE_PERFORMANCE_BAR)).toEqual(['p b']);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,5 +1,4 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import { createStore } from '~/mr_notes/stores';
|
||||
import InlineDiffTableRow from '~/diffs/components/inline_diff_table_row.vue';
|
||||
import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
|
||||
|
@ -28,13 +27,6 @@ describe('InlineDiffTableRow', () => {
|
|||
});
|
||||
};
|
||||
|
||||
const setWindowLocation = value => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
writable: true,
|
||||
value,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
store.state.notes.userData = TEST_USER;
|
||||
|
@ -122,22 +114,15 @@ describe('InlineDiffTableRow', () => {
|
|||
const findNoteButton = () => wrapper.find({ ref: 'addDiffNoteButton' });
|
||||
|
||||
it.each`
|
||||
userData | query | mergeRefHeadComments | expectation
|
||||
${TEST_USER} | ${'diff_head=false'} | ${false} | ${true}
|
||||
${TEST_USER} | ${'diff_head=true'} | ${true} | ${true}
|
||||
${TEST_USER} | ${'diff_head=true'} | ${false} | ${false}
|
||||
${null} | ${''} | ${true} | ${false}
|
||||
`(
|
||||
'exists is $expectation - with userData ($userData) query ($query)',
|
||||
({ userData, query, mergeRefHeadComments, expectation }) => {
|
||||
store.state.notes.userData = userData;
|
||||
gon.features = { mergeRefHeadComments };
|
||||
setWindowLocation({ href: `${TEST_HOST}?${query}` });
|
||||
createComponent({}, store);
|
||||
userData | expectation
|
||||
${TEST_USER} | ${true}
|
||||
${null} | ${false}
|
||||
`('exists is $expectation - with userData ($userData)', ({ userData, expectation }) => {
|
||||
store.state.notes.userData = userData;
|
||||
createComponent({}, store);
|
||||
|
||||
expect(findNoteButton().exists()).toBe(expectation);
|
||||
},
|
||||
);
|
||||
expect(findNoteButton().exists()).toBe(expectation);
|
||||
});
|
||||
|
||||
it.each`
|
||||
isHover | line | expectation
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import Vue from 'vue';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import { createStore } from '~/mr_notes/stores';
|
||||
import ParallelDiffTableRow from '~/diffs/components/parallel_diff_table_row.vue';
|
||||
import diffFileMockData from '../mock_data/diff_file';
|
||||
|
@ -186,13 +185,6 @@ describe('ParallelDiffTableRow', () => {
|
|||
});
|
||||
};
|
||||
|
||||
const setWindowLocation = value => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
writable: true,
|
||||
value,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
thisLine = diffFileMockData.parallel_diff_lines[2];
|
||||
|
@ -228,19 +220,15 @@ describe('ParallelDiffTableRow', () => {
|
|||
const findNoteButton = () => wrapper.find({ ref: 'addDiffNoteButtonLeft' });
|
||||
|
||||
it.each`
|
||||
hover | line | userData | query | mergeRefHeadComments | expectation
|
||||
${true} | ${{}} | ${TEST_USER} | ${'diff_head=false'} | ${false} | ${true}
|
||||
${true} | ${{ line: { left: null } }} | ${TEST_USER} | ${'diff_head=false'} | ${false} | ${false}
|
||||
${true} | ${{}} | ${TEST_USER} | ${'diff_head=true'} | ${true} | ${true}
|
||||
${true} | ${{}} | ${TEST_USER} | ${'diff_head=true'} | ${false} | ${false}
|
||||
${true} | ${{}} | ${null} | ${''} | ${true} | ${false}
|
||||
${false} | ${{}} | ${TEST_USER} | ${'diff_head=false'} | ${false} | ${false}
|
||||
hover | line | userData | expectation
|
||||
${true} | ${{}} | ${TEST_USER} | ${true}
|
||||
${true} | ${{ line: { left: null } }} | ${TEST_USER} | ${false}
|
||||
${true} | ${{}} | ${null} | ${false}
|
||||
${false} | ${{}} | ${TEST_USER} | ${false}
|
||||
`(
|
||||
'exists is $expectation - with userData ($userData) query ($query)',
|
||||
async ({ hover, line, userData, query, mergeRefHeadComments, expectation }) => {
|
||||
'exists is $expectation - with userData ($userData)',
|
||||
async ({ hover, line, userData, expectation }) => {
|
||||
store.state.notes.userData = userData;
|
||||
gon.features = { mergeRefHeadComments };
|
||||
setWindowLocation({ href: `${TEST_HOST}?${query}` });
|
||||
createComponent(line, store);
|
||||
if (hover) await wrapper.find('.line_holder').trigger('mouseover');
|
||||
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlIcon, GlAvatarLabeled } from '@gitlab/ui';
|
||||
|
||||
import IssuableHeader from '~/issuable_show/components/issuable_header.vue';
|
||||
|
||||
import { mockIssuableShowProps, mockIssuable } from '../mock_data';
|
||||
|
||||
const issuableHeaderProps = {
|
||||
...mockIssuable,
|
||||
...mockIssuableShowProps,
|
||||
};
|
||||
|
||||
const createComponent = (propsData = issuableHeaderProps) =>
|
||||
shallowMount(IssuableHeader, {
|
||||
propsData,
|
||||
slots: {
|
||||
'status-badge': 'Open',
|
||||
'header-actions': `
|
||||
<button class="js-close">Close issuable</button>
|
||||
<a class="js-new" href="/gitlab-org/gitlab-shell/-/issues/new">New issuable</a>
|
||||
`,
|
||||
},
|
||||
});
|
||||
|
||||
describe('IssuableHeader', () => {
|
||||
let wrapper;
|
||||
const findByTestId = testId => wrapper.find(`[data-testid="${testId}"]`);
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('computed', () => {
|
||||
describe('authorId', () => {
|
||||
it('returns numeric ID from GraphQL ID of `author` prop', () => {
|
||||
expect(wrapper.vm.authorId).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleRightSidebarToggleClick', () => {
|
||||
beforeEach(() => {
|
||||
setFixtures('<button class="js-toggle-right-sidebar-button">Collapse sidebar</button>');
|
||||
});
|
||||
|
||||
it('dispatches `click` event on sidebar toggle button', () => {
|
||||
wrapper.vm.toggleSidebarButtonEl = document.querySelector('.js-toggle-right-sidebar-button');
|
||||
jest.spyOn(wrapper.vm.toggleSidebarButtonEl, 'dispatchEvent').mockImplementation(jest.fn);
|
||||
|
||||
wrapper.vm.handleRightSidebarToggleClick();
|
||||
|
||||
expect(wrapper.vm.toggleSidebarButtonEl.dispatchEvent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'click',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
it('renders issuable status icon and text', () => {
|
||||
const statusBoxEl = findByTestId('status');
|
||||
|
||||
expect(statusBoxEl.exists()).toBe(true);
|
||||
expect(statusBoxEl.find(GlIcon).props('name')).toBe(mockIssuableShowProps.statusIcon);
|
||||
expect(statusBoxEl.text()).toContain('Open');
|
||||
});
|
||||
|
||||
it('renders blocked icon when issuable is blocked', async () => {
|
||||
wrapper.setProps({
|
||||
blocked: true,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const blockedEl = findByTestId('blocked');
|
||||
|
||||
expect(blockedEl.exists()).toBe(true);
|
||||
expect(blockedEl.find(GlIcon).props('name')).toBe('lock');
|
||||
});
|
||||
|
||||
it('renders confidential icon when issuable is confidential', async () => {
|
||||
wrapper.setProps({
|
||||
confidential: true,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const confidentialEl = findByTestId('confidential');
|
||||
|
||||
expect(confidentialEl.exists()).toBe(true);
|
||||
expect(confidentialEl.find(GlIcon).props('name')).toBe('eye-slash');
|
||||
});
|
||||
|
||||
it('renders issuable author avatar', () => {
|
||||
const { username, name, webUrl, avatarUrl } = mockIssuable.author;
|
||||
const avatarElAttrs = {
|
||||
'data-user-id': '1',
|
||||
'data-username': username,
|
||||
'data-name': name,
|
||||
href: webUrl,
|
||||
target: '_blank',
|
||||
};
|
||||
const avatarEl = findByTestId('avatar');
|
||||
expect(avatarEl.exists()).toBe(true);
|
||||
expect(avatarEl.attributes()).toMatchObject(avatarElAttrs);
|
||||
expect(avatarEl.find(GlAvatarLabeled).attributes()).toMatchObject({
|
||||
size: '24',
|
||||
src: avatarUrl,
|
||||
label: name,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders sidebar toggle button', () => {
|
||||
const toggleButtonEl = findByTestId('sidebar-toggle');
|
||||
|
||||
expect(toggleButtonEl.exists()).toBe(true);
|
||||
expect(toggleButtonEl.props('icon')).toBe('chevron-double-lg-left');
|
||||
});
|
||||
|
||||
it('renders header actions', () => {
|
||||
const actionsEl = findByTestId('header-actions');
|
||||
|
||||
expect(actionsEl.find('button.js-close').exists()).toBe(true);
|
||||
expect(actionsEl.find('a.js-new').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|