Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
ef7cfec30c
commit
8ff63012e9
|
|
@ -40,10 +40,7 @@ export const diffCompareDropdownTargetVersions = (state, getters) => {
|
|||
};
|
||||
};
|
||||
|
||||
if (gon.features?.diffCompareWithHead) {
|
||||
return [...state.mergeRequestDiffs.slice(1).map(formatVersion), baseVersion, headVersion];
|
||||
}
|
||||
return [...state.mergeRequestDiffs.slice(1).map(formatVersion), baseVersion];
|
||||
return [...state.mergeRequestDiffs.slice(1).map(formatVersion), baseVersion, headVersion];
|
||||
};
|
||||
|
||||
export const diffCompareDropdownSourceVersions = (state, getters) => {
|
||||
|
|
|
|||
|
|
@ -440,7 +440,6 @@ export default {
|
|||
class="flex-grow-1"
|
||||
toggle-class="dropdown-menu-toggle"
|
||||
:default-branch="defaultBranch"
|
||||
:selected-dashboard="selectedDashboard"
|
||||
@selectDashboard="selectDashboard($event)"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import { mapState, mapActions, mapGetters } from 'vuex';
|
||||
import {
|
||||
GlAlert,
|
||||
GlIcon,
|
||||
|
|
@ -36,11 +36,6 @@ export default {
|
|||
GlModal: GlModalDirective,
|
||||
},
|
||||
props: {
|
||||
selectedDashboard: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
defaultBranch: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
|
@ -56,11 +51,15 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
...mapState('monitoringDashboard', ['allDashboards']),
|
||||
...mapGetters('monitoringDashboard', ['selectedDashboard']),
|
||||
isSystemDashboard() {
|
||||
return this.selectedDashboard.system_dashboard;
|
||||
return this.selectedDashboard?.system_dashboard;
|
||||
},
|
||||
selectedDashboardText() {
|
||||
return this.selectedDashboard.display_name;
|
||||
return this.selectedDashboard?.display_name;
|
||||
},
|
||||
selectedDashboardPath() {
|
||||
return this.selectedDashboard?.path;
|
||||
},
|
||||
|
||||
filteredDashboards() {
|
||||
|
|
@ -145,7 +144,7 @@ export default {
|
|||
<gl-dropdown-item
|
||||
v-for="dashboard in starredDashboards"
|
||||
:key="dashboard.path"
|
||||
:active="dashboard.path === selectedDashboard.path"
|
||||
:active="dashboard.path === selectedDashboardPath"
|
||||
active-class="is-active"
|
||||
@click="selectDashboard(dashboard)"
|
||||
>
|
||||
|
|
@ -163,7 +162,7 @@ export default {
|
|||
<gl-dropdown-item
|
||||
v-for="dashboard in nonStarredDashboards"
|
||||
:key="dashboard.path"
|
||||
:active="dashboard.path === selectedDashboard.path"
|
||||
:active="dashboard.path === selectedDashboardPath"
|
||||
active-class="is-active"
|
||||
@click="selectDashboard(dashboard)"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -345,6 +345,35 @@ export const receiveAnnotationsFailure = ({ commit }) => commit(types.RECEIVE_AN
|
|||
|
||||
// Dashboard manipulation
|
||||
|
||||
export const toggleStarredValue = ({ commit, state, getters }) => {
|
||||
const { selectedDashboard } = getters;
|
||||
|
||||
if (state.isUpdatingStarredValue) {
|
||||
// Prevent repeating requests for the same change
|
||||
return;
|
||||
}
|
||||
if (!selectedDashboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
const method = selectedDashboard.starred ? 'DELETE' : 'POST';
|
||||
const url = selectedDashboard.user_starred_path;
|
||||
const newStarredValue = !selectedDashboard.starred;
|
||||
|
||||
commit(types.REQUEST_DASHBOARD_STARRING);
|
||||
|
||||
axios({
|
||||
url,
|
||||
method,
|
||||
})
|
||||
.then(() => {
|
||||
commit(types.RECEIVE_DASHBOARD_STARRING_SUCCESS, newStarredValue);
|
||||
})
|
||||
.catch(() => {
|
||||
commit(types.RECEIVE_DASHBOARD_STARRING_FAILURE);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a new array of metrics to a panel group
|
||||
* @param {*} data An object containing
|
||||
|
|
|
|||
|
|
@ -3,6 +3,19 @@ import { NOT_IN_DB_PREFIX } from '../constants';
|
|||
const metricsIdsInPanel = panel =>
|
||||
panel.metrics.filter(metric => metric.metricId && metric.result).map(metric => metric.metricId);
|
||||
|
||||
/**
|
||||
* Returns a reference to the currently selected dashboard
|
||||
* from the list of dashboards.
|
||||
*
|
||||
* @param {Object} state
|
||||
*/
|
||||
export const selectedDashboard = state => {
|
||||
const { allDashboards } = state;
|
||||
return (
|
||||
allDashboards.find(({ path }) => path === state.currentDashboard) || allDashboards[0] || null
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all state for metric in the dashboard or a group. The
|
||||
* states are not repeated so the dashboard or group can show
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ export const RECEIVE_METRICS_DASHBOARD_FAILURE = 'RECEIVE_METRICS_DASHBOARD_FAIL
|
|||
export const SET_PROM_QUERY_VARIABLES = 'SET_PROM_QUERY_VARIABLES';
|
||||
export const UPDATE_VARIABLE_DATA = 'UPDATE_VARIABLE_DATA';
|
||||
|
||||
export const REQUEST_DASHBOARD_STARRING = 'REQUEST_DASHBOARD_STARRING';
|
||||
export const RECEIVE_DASHBOARD_STARRING_SUCCESS = 'RECEIVE_DASHBOARD_STARRING_SUCCESS';
|
||||
export const RECEIVE_DASHBOARD_STARRING_FAILURE = 'RECEIVE_DASHBOARD_STARRING_FAILURE';
|
||||
|
||||
// Annotations
|
||||
export const RECEIVE_ANNOTATIONS_SUCCESS = 'RECEIVE_ANNOTATIONS_SUCCESS';
|
||||
export const RECEIVE_ANNOTATIONS_FAILURE = 'RECEIVE_ANNOTATIONS_FAILURE';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import Vue from 'vue';
|
||||
import { pick } from 'lodash';
|
||||
import * as types from './mutation_types';
|
||||
import { selectedDashboard } from './getters';
|
||||
import { mapToDashboardViewModel, normalizeQueryResult } from './utils';
|
||||
import { BACKOFF_TIMEOUT } from '../../lib/utils/common_utils';
|
||||
import { endpointKeys, initialStateKeys, metricStates } from '../constants';
|
||||
|
|
@ -71,6 +73,23 @@ export default {
|
|||
state.showEmptyState = true;
|
||||
},
|
||||
|
||||
[types.REQUEST_DASHBOARD_STARRING](state) {
|
||||
state.isUpdatingStarredValue = true;
|
||||
},
|
||||
[types.RECEIVE_DASHBOARD_STARRING_SUCCESS](state, newStarredValue) {
|
||||
const dashboard = selectedDashboard(state);
|
||||
const index = state.allDashboards.findIndex(d => d === dashboard);
|
||||
|
||||
state.isUpdatingStarredValue = false;
|
||||
|
||||
// Trigger state updates in the reactivity system for this change
|
||||
// https://vuejs.org/v2/guide/reactivity.html#For-Arrays
|
||||
Vue.set(state.allDashboards, index, { ...dashboard, starred: newStarredValue });
|
||||
},
|
||||
[types.RECEIVE_DASHBOARD_STARRING_FAILURE](state) {
|
||||
state.isUpdatingStarredValue = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Deployments and environments
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export default () => ({
|
|||
emptyState: 'gettingStarted',
|
||||
showEmptyState: true,
|
||||
showErrorBanner: true,
|
||||
isUpdatingStarredValue: false,
|
||||
dashboard: {
|
||||
panelGroups: [],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,18 +1,68 @@
|
|||
<script>
|
||||
import { GlFormTextarea } from '@gitlab/ui';
|
||||
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
|
||||
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
|
||||
import PublishToolbar from '../components/publish_toolbar.vue';
|
||||
import EditHeader from '../components/edit_header.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlFormTextarea,
|
||||
RichContentEditor,
|
||||
PublishToolbar,
|
||||
EditHeader,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
props: {
|
||||
value: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
savingChanges: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
returnUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editableContent: this.content,
|
||||
saveable: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
modified() {
|
||||
return this.content !== this.editableContent;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onSubmit() {
|
||||
this.$emit('submit', { content: this.editableContent });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-form-textarea :value="value" v-on="$listeners" />
|
||||
<div class="d-flex flex-grow-1 flex-column">
|
||||
<edit-header class="py-2" :title="title" />
|
||||
<rich-content-editor v-if="glFeatures.richContentEditor" v-model="editableContent" />
|
||||
<gl-form-textarea v-else v-model="editableContent" class="h-100 shadow-none" />
|
||||
<publish-toolbar
|
||||
class="gl-fixed gl-left-0 gl-bottom-0 gl-w-full"
|
||||
:return-url="returnUrl"
|
||||
:saveable="modified"
|
||||
:saving-changes="savingChanges"
|
||||
@submit="onSubmit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
<script>
|
||||
import { GlSkeletonLoader } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlSkeletonLoader,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-skeleton-loader :width="500" :height="102">
|
||||
<rect width="500" height="16" rx="4" />
|
||||
<rect y="20" width="375" height="16" rx="4" />
|
||||
<rect x="380" y="20" width="120" height="16" rx="4" />
|
||||
<rect y="40" width="250" height="16" rx="4" />
|
||||
<rect x="255" y="40" width="150" height="16" rx="4" />
|
||||
<rect x="410" y="40" width="90" height="16" rx="4" />
|
||||
</gl-skeleton-loader>
|
||||
</template>
|
||||
|
|
@ -1,13 +1,9 @@
|
|||
<script>
|
||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||
import { GlSkeletonLoader } from '@gitlab/ui';
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
|
||||
import SkeletonLoader from '../components/skeleton_loader.vue';
|
||||
import EditArea from '../components/edit_area.vue';
|
||||
import EditHeader from '../components/edit_header.vue';
|
||||
import SavedChangesMessage from '../components/saved_changes_message.vue';
|
||||
import PublishToolbar from '../components/publish_toolbar.vue';
|
||||
import InvalidContentMessage from '../components/invalid_content_message.vue';
|
||||
import SubmitChangesError from '../components/submit_changes_error.vue';
|
||||
|
||||
|
|
@ -20,16 +16,12 @@ import { LOAD_CONTENT_ERROR } from '../constants';
|
|||
|
||||
export default {
|
||||
components: {
|
||||
RichContentEditor,
|
||||
SkeletonLoader,
|
||||
EditArea,
|
||||
EditHeader,
|
||||
InvalidContentMessage,
|
||||
GlSkeletonLoader,
|
||||
SavedChangesMessage,
|
||||
PublishToolbar,
|
||||
SubmitChangesError,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
apollo: {
|
||||
appData: {
|
||||
query: appDataQuery,
|
||||
|
|
@ -58,80 +50,51 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'content',
|
||||
'isLoadingContent',
|
||||
'isSavingChanges',
|
||||
'isContentLoaded',
|
||||
'returnUrl',
|
||||
'title',
|
||||
'submitChangesError',
|
||||
'savedContentMeta',
|
||||
]),
|
||||
...mapGetters(['contentChanged']),
|
||||
},
|
||||
mounted() {
|
||||
if (this.appData.isSupportedContent) {
|
||||
this.loadContent();
|
||||
}
|
||||
...mapState(['isSavingChanges', 'submitChangesError', 'savedContentMeta']),
|
||||
isLoadingContent() {
|
||||
return this.$apollo.queries.sourceContent.loading;
|
||||
},
|
||||
isContentLoaded() {
|
||||
return Boolean(this.sourceContent);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['loadContent', 'setContent', 'submitChanges', 'dismissSubmitChangesError']),
|
||||
...mapActions(['setContent', 'submitChanges', 'dismissSubmitChangesError']),
|
||||
onSubmit({ content }) {
|
||||
this.setContent(content);
|
||||
this.submitChanges();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="d-flex justify-content-center h-100 pt-2">
|
||||
<div class="container d-flex gl-flex-direction-column pt-2 h-100">
|
||||
<!-- Success view -->
|
||||
<saved-changes-message
|
||||
v-if="savedContentMeta"
|
||||
class="w-75"
|
||||
:branch="savedContentMeta.branch"
|
||||
:commit="savedContentMeta.commit"
|
||||
:merge-request="savedContentMeta.mergeRequest"
|
||||
:return-url="returnUrl"
|
||||
:return-url="appData.returnUrl"
|
||||
/>
|
||||
|
||||
<!-- Main view -->
|
||||
<template v-else-if="appData.isSupportedContent">
|
||||
<div v-if="isLoadingContent" class="w-50 h-50">
|
||||
<gl-skeleton-loader :width="500" :height="102">
|
||||
<rect width="500" height="16" rx="4" />
|
||||
<rect y="20" width="375" height="16" rx="4" />
|
||||
<rect x="380" y="20" width="120" height="16" rx="4" />
|
||||
<rect y="40" width="250" height="16" rx="4" />
|
||||
<rect x="255" y="40" width="150" height="16" rx="4" />
|
||||
<rect x="410" y="40" width="90" height="16" rx="4" />
|
||||
</gl-skeleton-loader>
|
||||
</div>
|
||||
<div v-if="isContentLoaded" class="d-flex flex-grow-1 flex-column">
|
||||
<submit-changes-error
|
||||
v-if="submitChangesError"
|
||||
class="w-75 align-self-center"
|
||||
:error="submitChangesError"
|
||||
@retry="submitChanges"
|
||||
@dismiss="dismissSubmitChangesError"
|
||||
/>
|
||||
<edit-header class="w-75 align-self-center py-2" :title="title" />
|
||||
<rich-content-editor
|
||||
v-if="glFeatures.richContentEditor"
|
||||
class="w-75 gl-align-self-center"
|
||||
:value="content"
|
||||
@input="setContent"
|
||||
/>
|
||||
<edit-area
|
||||
v-else
|
||||
class="w-75 h-100 shadow-none align-self-center"
|
||||
:value="content"
|
||||
@input="setContent"
|
||||
/>
|
||||
<publish-toolbar
|
||||
:return-url="returnUrl"
|
||||
:saveable="contentChanged"
|
||||
:saving-changes="isSavingChanges"
|
||||
@submit="submitChanges"
|
||||
/>
|
||||
</div>
|
||||
<skeleton-loader v-if="isLoadingContent" class="w-75 gl-align-self-center gl-mt-5" />
|
||||
<submit-changes-error
|
||||
v-if="submitChangesError"
|
||||
:error="submitChangesError"
|
||||
@retry="submitChanges"
|
||||
@dismiss="dismissSubmitChangesError"
|
||||
/>
|
||||
<edit-area
|
||||
v-if="isContentLoaded"
|
||||
:title="sourceContent.title"
|
||||
:content="sourceContent.content"
|
||||
:saving-changes="isSavingChanges"
|
||||
:return-url="appData.returnUrl"
|
||||
@submit="onSubmit"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Error view -->
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@ export default {
|
|||
class="labels-select-dropdown-button js-dropdown-button w-100 text-left"
|
||||
@click="handleButtonClick"
|
||||
>
|
||||
<span class="dropdown-toggle-text" :class="{ 'flex-fill': isDropdownVariantStandalone }">{{
|
||||
dropdownButtonText
|
||||
}}</span>
|
||||
<span class="dropdown-toggle-text flex-fill">
|
||||
{{ dropdownButtonText }}
|
||||
</span>
|
||||
<gl-icon name="chevron-down" class="pull-right" />
|
||||
</gl-button>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
<script>
|
||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||
import { GlLoadingIcon, GlButton, GlIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui';
|
||||
import { GlLoadingIcon, GlButton, GlSearchBoxByType, GlLink } from '@gitlab/ui';
|
||||
|
||||
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
|
||||
|
||||
import LabelItem from './label_item.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlLoadingIcon,
|
||||
GlButton,
|
||||
GlIcon,
|
||||
GlSearchBoxByType,
|
||||
GlLink,
|
||||
LabelItem,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -60,11 +62,6 @@ export default {
|
|||
'updateSelectedLabels',
|
||||
'toggleDropdownContents',
|
||||
]),
|
||||
getDropdownLabelBoxStyle(label) {
|
||||
return {
|
||||
backgroundColor: label.color,
|
||||
};
|
||||
},
|
||||
isLabelSelected(label) {
|
||||
return this.selectedLabelsList.includes(label.id);
|
||||
},
|
||||
|
|
@ -138,24 +135,19 @@ export default {
|
|||
@click="toggleDropdownContents"
|
||||
/>
|
||||
</div>
|
||||
<div class="dropdown-input">
|
||||
<div class="dropdown-input" @click.stop="() => {}">
|
||||
<gl-search-box-by-type v-model="searchKey" :autofocus="true" />
|
||||
</div>
|
||||
<div v-if="!labelsFetchInProgress" ref="labelsListContainer" class="dropdown-content">
|
||||
<div v-show="!labelsFetchInProgress" ref="labelsListContainer" class="dropdown-content">
|
||||
<ul class="list-unstyled mb-0">
|
||||
<li v-for="(label, index) in visibleLabels" :key="label.id" class="d-block text-left">
|
||||
<gl-link
|
||||
class="d-flex align-items-baseline text-break-word label-item"
|
||||
:class="{ 'is-focused': index === currentHighlightItem }"
|
||||
@click="handleLabelClick(label)"
|
||||
>
|
||||
<gl-icon v-show="label.set" name="mobile-issue-close" class="mr-2 align-self-center" />
|
||||
<span v-show="!label.set" class="mr-3 pr-2"></span>
|
||||
<span class="dropdown-label-box" :style="getDropdownLabelBoxStyle(label)"></span>
|
||||
<span>{{ label.title }}</span>
|
||||
</gl-link>
|
||||
<label-item
|
||||
:label="label"
|
||||
:highlight="index === currentHighlightItem"
|
||||
@clickLabel="handleLabelClick(label)"
|
||||
/>
|
||||
</li>
|
||||
<li v-if="!visibleLabels.length" class="p-2 text-center">
|
||||
<li v-show="!visibleLabels.length" class="p-2 text-center">
|
||||
{{ __('No matching results') }}
|
||||
</li>
|
||||
</ul>
|
||||
|
|
@ -170,9 +162,9 @@ export default {
|
|||
>
|
||||
</li>
|
||||
<li>
|
||||
<gl-link :href="labelsManagePath" class="d-flex flex-row text-break-word label-item">
|
||||
{{ footerManageLabelTitle }}
|
||||
</gl-link>
|
||||
<gl-link :href="labelsManagePath" class="d-flex flex-row text-break-word label-item">{{
|
||||
footerManageLabelTitle
|
||||
}}</gl-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
<script>
|
||||
import { GlIcon, GlLink } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
GlLink,
|
||||
},
|
||||
props: {
|
||||
label: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
highlight: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isSet: this.label.set,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
labelBoxStyle() {
|
||||
return {
|
||||
backgroundColor: this.label.color,
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleClick() {
|
||||
this.isSet = !this.isSet;
|
||||
this.$emit('clickLabel', this.label);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-link
|
||||
class="d-flex align-items-baseline text-break-word label-item"
|
||||
:class="{ 'is-focused': highlight }"
|
||||
@click="handleClick"
|
||||
>
|
||||
<gl-icon v-show="isSet" name="mobile-issue-close" class="mr-2 align-self-center" />
|
||||
<span v-show="!isSet" data-testid="no-icon" class="mr-3 pr-2"></span>
|
||||
<span class="dropdown-label-box" data-testid="label-color-box" :style="labelBoxStyle"></span>
|
||||
<span>{{ label.title }}</span>
|
||||
</gl-link>
|
||||
</template>
|
||||
|
|
@ -58,29 +58,13 @@ export default {
|
|||
},
|
||||
|
||||
[types.UPDATE_SELECTED_LABELS](state, { labels }) {
|
||||
// Iterate over all the labels and update
|
||||
// `set` prop value to represent their current state.
|
||||
const labelIds = labels.map(label => label.id);
|
||||
state.labels = state.labels.reduce((allLabels, label) => {
|
||||
if (labelIds.includes(label.id)) {
|
||||
allLabels.push({
|
||||
...label,
|
||||
touched: true,
|
||||
set: !label.set,
|
||||
});
|
||||
} else {
|
||||
// In case multiselect is not allowed
|
||||
// we unselect any existing selected label
|
||||
const unchangedLabel = state.allowMultiselect
|
||||
? label
|
||||
: {
|
||||
...label,
|
||||
touched: true,
|
||||
set: false,
|
||||
};
|
||||
allLabels.push(unchangedLabel);
|
||||
}
|
||||
return allLabels;
|
||||
}, []);
|
||||
// Find the label to update from all the labels
|
||||
// and change `set` prop value to represent their current state.
|
||||
const labelId = labels.pop()?.id;
|
||||
const candidateLabel = state.labels.find(label => labelId === label.id);
|
||||
if (candidateLabel) {
|
||||
candidateLabel.touched = true;
|
||||
candidateLabel.set = !candidateLabel.set;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
push_frontend_feature_flag(:code_navigation, @project)
|
||||
push_frontend_feature_flag(:widget_visibility_polling, @project, default_enabled: true)
|
||||
push_frontend_feature_flag(:merge_ref_head_comments, @project)
|
||||
push_frontend_feature_flag(:diff_compare_with_head, @project)
|
||||
push_frontend_feature_flag(:accessibility_merge_request_widget, @project)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -367,7 +367,7 @@ module ApplicationSettingsHelper
|
|||
end
|
||||
end
|
||||
|
||||
ApplicationSettingsHelper.prepend_if_ee('EE::ApplicationSettingsHelper') # rubocop: disable Cop/InjectEnterpriseEditionModule
|
||||
ApplicationSettingsHelper.prepend_if_ee('EE::ApplicationSettingsHelper')
|
||||
|
||||
# The methods in `EE::ApplicationSettingsHelper` should be available as both
|
||||
# instance and class methods.
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ module AuthHelper
|
|||
extend self
|
||||
end
|
||||
|
||||
AuthHelper.prepend_if_ee('EE::AuthHelper') # rubocop: disable Cop/InjectEnterpriseEditionModule
|
||||
AuthHelper.prepend_if_ee('EE::AuthHelper')
|
||||
|
||||
# The methods added in EE should be available as both class and instance
|
||||
# methods, just like the methods provided by `AuthHelper` itself.
|
||||
|
|
|
|||
|
|
@ -249,7 +249,7 @@ module MilestonesHelper
|
|||
if milestone.legacy_group_milestone?
|
||||
group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: params)
|
||||
else
|
||||
group_milestone_path(@group, milestone.iid, milestone: params)
|
||||
group_milestone_path(milestone.group, milestone.iid, milestone: params)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ProjectsHelper
|
||||
prepend_if_ee('::EE::ProjectsHelper') # rubocop: disable Cop/InjectEnterpriseEditionModule
|
||||
|
||||
def project_incident_management_setting
|
||||
@project_incident_management_setting ||= @project.incident_management_setting ||
|
||||
@project.build_incident_management_setting
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ module ServicesHelper
|
|||
extend self
|
||||
end
|
||||
|
||||
ServicesHelper.prepend_if_ee('EE::ServicesHelper') # rubocop: disable Cop/InjectEnterpriseEditionModule
|
||||
ServicesHelper.prepend_if_ee('EE::ServicesHelper')
|
||||
|
||||
# The methods in `EE::ServicesHelper` should be available as both instance and
|
||||
# class methods.
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ module SystemNoteHelper
|
|||
extend self
|
||||
end
|
||||
|
||||
SystemNoteHelper.prepend_if_ee('EE::SystemNoteHelper') # rubocop: disable Cop/InjectEnterpriseEditionModule
|
||||
SystemNoteHelper.prepend_if_ee('EE::SystemNoteHelper')
|
||||
|
||||
# The methods in `EE::SystemNoteHelper` should be available as both instance and
|
||||
# class methods.
|
||||
|
|
|
|||
|
|
@ -150,5 +150,5 @@ end
|
|||
|
||||
Noteable.extend(Noteable::ClassMethods)
|
||||
|
||||
Noteable::ClassMethods.prepend_if_ee('EE::Noteable::ClassMethods') # rubocop: disable Cop/InjectEnterpriseEditionModule
|
||||
Noteable::ClassMethods.prepend_if_ee('EE::Noteable::ClassMethods')
|
||||
Noteable.prepend_if_ee('EE::Noteable')
|
||||
|
|
|
|||
|
|
@ -50,8 +50,8 @@ module ProtectedRefAccess
|
|||
end
|
||||
end
|
||||
|
||||
ProtectedRefAccess.include_if_ee('EE::ProtectedRefAccess::Scopes') # rubocop: disable Cop/InjectEnterpriseEditionModule
|
||||
ProtectedRefAccess.prepend_if_ee('EE::ProtectedRefAccess') # rubocop: disable Cop/InjectEnterpriseEditionModule
|
||||
ProtectedRefAccess.include_if_ee('EE::ProtectedRefAccess::Scopes')
|
||||
ProtectedRefAccess.prepend_if_ee('EE::ProtectedRefAccess')
|
||||
|
||||
# When using `prepend` (or `include` for that matter), the `ClassMethods`
|
||||
# constants are not merged. This means that `class_methods` in
|
||||
|
|
|
|||
|
|
@ -875,7 +875,7 @@ class MergeRequest < ApplicationRecord
|
|||
# rubocop: enable CodeReuse/ServiceClass
|
||||
|
||||
def diffable_merge_ref?
|
||||
Feature.enabled?(:diff_compare_with_head, target_project) && can_be_merged? && merge_ref_head.present?
|
||||
can_be_merged? && merge_ref_head.present?
|
||||
end
|
||||
|
||||
# Returns boolean indicating the merge_status should be rechecked in order to
|
||||
|
|
|
|||
|
|
@ -82,6 +82,8 @@ class SentNotification < ApplicationRecord
|
|||
if new_position.is_a?(Hash)
|
||||
new_position = new_position.with_indifferent_access
|
||||
new_position = Gitlab::Diff::Position.new(new_position)
|
||||
else
|
||||
new_position = nil
|
||||
end
|
||||
|
||||
super(new_position)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,14 @@ module Ci
|
|||
end
|
||||
end
|
||||
|
||||
condition(:unprotected_ref) do
|
||||
if @subject.tag?
|
||||
!ProtectedTag.protected?(@subject.project, @subject.ref)
|
||||
else
|
||||
!ProtectedBranch.protected?(@subject.project, @subject.ref)
|
||||
end
|
||||
end
|
||||
|
||||
condition(:owner_of_job) do
|
||||
@subject.triggered_by?(@user)
|
||||
end
|
||||
|
|
@ -34,7 +42,7 @@ module Ci
|
|||
prevent :erase_build
|
||||
end
|
||||
|
||||
rule { can?(:admin_build) | (can?(:update_build) & owner_of_job) }.enable :erase_build
|
||||
rule { can?(:admin_build) | (can?(:update_build) & owner_of_job & unprotected_ref) }.enable :erase_build
|
||||
|
||||
rule { can?(:public_access) & branch_allows_collaboration }.policy do
|
||||
enable :update_build
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ class AnalyticsSummaryEntity < Grape::Entity
|
|||
private
|
||||
|
||||
def value
|
||||
return object.value if object.value.is_a? String
|
||||
|
||||
object.value&.nonzero? ? object.value.to_s : '-'
|
||||
object.value.to_s
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ module Branches
|
|||
if new_branch
|
||||
success(new_branch)
|
||||
else
|
||||
error("Invalid reference name: #{branch_name}")
|
||||
error("Invalid reference name: #{ref}")
|
||||
end
|
||||
rescue Gitlab::Git::PreReceiveError => ex
|
||||
error(ex.message)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,13 @@
|
|||
:resource_boundary: :unknown
|
||||
:weight: 1
|
||||
:idempotent: true
|
||||
- :name: authorized_project_update:authorized_project_update_user_refresh_with_low_urgency
|
||||
:feature_category: :authentication_and_authorization
|
||||
:has_external_dependencies:
|
||||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
:weight: 1
|
||||
:idempotent: true
|
||||
- :name: auto_devops:auto_devops_disable
|
||||
:feature_category: :auto_devops
|
||||
:has_external_dependencies:
|
||||
|
|
@ -934,13 +941,6 @@
|
|||
:resource_boundary: :unknown
|
||||
:weight: 2
|
||||
:idempotent: true
|
||||
- :name: authorized_project_update_user_refresh_with_low_urgency
|
||||
:feature_category: :authentication_and_authorization
|
||||
:has_external_dependencies:
|
||||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
:weight: 1
|
||||
:idempotent: true
|
||||
- :name: authorized_projects
|
||||
:feature_category: :authentication_and_authorization
|
||||
:has_external_dependencies:
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ module AuthorizedProjectUpdate
|
|||
class UserRefreshWithLowUrgencyWorker < ::AuthorizedProjectsWorker
|
||||
feature_category :authentication_and_authorization
|
||||
urgency :low
|
||||
queue_namespace :authorized_project_update
|
||||
|
||||
idempotent!
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Disallow developers to delete builds of protected branches
|
||||
merge_request: 28881
|
||||
author: Alexander Kutelev
|
||||
type: changed
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: backfill environment_id on deployment_merge_requests
|
||||
merge_request: 27219
|
||||
author:
|
||||
type: other
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allow multiple usage of EE extension/inclusion on last lines
|
||||
merge_request: 31183
|
||||
author: Rajendra Kadam
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Link to subgroup milestones correctly from group milestones page
|
||||
merge_request: 31383
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allow showing merge request diffs compared to current version of target branch
|
||||
merge_request: 31325
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -160,5 +160,5 @@ class Rack::Attack
|
|||
end
|
||||
end
|
||||
|
||||
::Rack::Attack.extend_if_ee('::EE::Gitlab::Rack::Attack') # rubocop: disable Cop/InjectEnterpriseEditionModule
|
||||
::Rack::Attack.extend_if_ee('::EE::Gitlab::Rack::Attack')
|
||||
::Rack::Attack::Request.prepend_if_ee('::EE::Gitlab::Rack::Attack::Request')
|
||||
|
|
|
|||
|
|
@ -34,8 +34,6 @@
|
|||
- 2
|
||||
- - authorized_project_update
|
||||
- 1
|
||||
- - authorized_project_update_user_refresh_with_low_urgency
|
||||
- 1
|
||||
- - authorized_projects
|
||||
- 2
|
||||
- - auto_devops
|
||||
|
|
|
|||
|
|
@ -4,46 +4,19 @@ class BackfillEnvironmentIdOnDeploymentMergeRequests < ActiveRecord::Migration[6
|
|||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
BATCH_SIZE = 400
|
||||
DELAY = 1.minute
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
max_mr_id = DeploymentMergeRequest
|
||||
.select(:merge_request_id)
|
||||
.distinct
|
||||
.order(merge_request_id: :desc)
|
||||
.limit(1)
|
||||
.pluck(:merge_request_id)
|
||||
.first || 0
|
||||
# no-op
|
||||
|
||||
last_mr_id = 0
|
||||
step = 0
|
||||
|
||||
while last_mr_id < max_mr_id
|
||||
stop =
|
||||
DeploymentMergeRequest
|
||||
.select(:merge_request_id)
|
||||
.distinct
|
||||
.where('merge_request_id > ?', last_mr_id)
|
||||
.order(:merge_request_id)
|
||||
.offset(BATCH_SIZE)
|
||||
.limit(1)
|
||||
.pluck(:merge_request_id)
|
||||
.first
|
||||
|
||||
stop ||= max_mr_id
|
||||
|
||||
migrate_in(
|
||||
step * DELAY,
|
||||
'BackfillEnvironmentIdDeploymentMergeRequests',
|
||||
[last_mr_id + 1, stop]
|
||||
)
|
||||
|
||||
last_mr_id = stop
|
||||
step += 1
|
||||
end
|
||||
# this migration is deleted because there is no foreign key for
|
||||
# deployments.environment_id and this caused a failure upgrading
|
||||
# deployments_merge_requests.environment_id
|
||||
#
|
||||
# Details on the following issues:
|
||||
# * https://gitlab.com/gitlab-org/gitlab/-/issues/217191
|
||||
# * https://gitlab.com/gitlab-org/gitlab/-/issues/26229
|
||||
end
|
||||
|
||||
def down
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ The `description_html` - was added to response JSON in [GitLab 12.7](https://git
|
|||
|
||||
Get all labels for a given project.
|
||||
|
||||
By default, this request returns 20 results at a time because the API results [are paginated](README.md#pagination).
|
||||
|
||||
```plaintext
|
||||
GET /projects/:id/labels
|
||||
```
|
||||
|
|
|
|||
|
|
@ -39,8 +39,9 @@ runtime dependencies needed to compile the project:
|
|||
- `artifacts`: **Use for stage results that will be passed between stages.**
|
||||
|
||||
Artifacts are files generated by a job which are stored and uploaded, and can then
|
||||
be fetched and used by jobs in later stages of the **same pipeline**. This data
|
||||
will not be available in different pipelines, but is available to be downloaded
|
||||
be fetched and used by jobs in later stages of the **same pipeline**. In other words,
|
||||
[you can't create an artifact in job-A in stage-1, and then use this artifact in job-B in stage-1](https://gitlab.com/gitlab-org/gitlab/-/issues/25837).
|
||||
This data will not be available in different pipelines, but is available to be downloaded
|
||||
from the UI.
|
||||
|
||||
The name `artifacts` sounds like it's only useful outside of the job, like for downloading
|
||||
|
|
|
|||
|
|
@ -403,7 +403,9 @@ instance and project. In addition, all admins can use the admin interface under
|
|||
| See events in the system | | | | ✓ |
|
||||
| Admin interface | | | | ✓ |
|
||||
|
||||
1. Only if the job was triggered by the user
|
||||
1. Only if the job was:
|
||||
- Triggered by the user
|
||||
- [Since GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/35069), not run for a protected branch
|
||||
|
||||
### Job permissions
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ source and target branch can be shown mixed together making it hard to
|
|||
understand which changes are being added and which already exist in the
|
||||
target branch.
|
||||
|
||||
In GitLab 12.10, we added an **experimental** comparison mode, which
|
||||
In GitLab 12.10, we added a comparison mode, which
|
||||
shows a diff calculated by simulating how it would look like once merged - a more accurate
|
||||
representation of the changes rather than using the base of the two
|
||||
branches. The new mode is available from the comparison target drop down
|
||||
|
|
@ -67,26 +67,6 @@ current default comparison.
|
|||
|
||||

|
||||
|
||||
### Enable or disable `HEAD` comparison mode **(CORE ONLY)**
|
||||
|
||||
`HEAD` comparison mode is under development and not ready for production use. It is
|
||||
deployed behind a feature flag that is **disabled by default**.
|
||||
[GitLab administrators with access to the GitLab Rails console](../../../administration/troubleshooting/navigating_gitlab_via_rails_console.md#starting-a-rails-console-session)
|
||||
can enable it for your instance. You're welcome to test it, but use it at your
|
||||
own risk.
|
||||
|
||||
To enable it:
|
||||
|
||||
```ruby
|
||||
Feature.enable(:diff_compare_with_head)
|
||||
```
|
||||
|
||||
To disable it:
|
||||
|
||||
```ruby
|
||||
Feature.disable(:diff_compare_with_head)
|
||||
```
|
||||
|
||||
<!-- ## Troubleshooting
|
||||
|
||||
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
|
||||
|
|
|
|||
|
|
@ -5,7 +5,14 @@ module Gitlab
|
|||
# BackfillEnvironmentIdDeploymentMergeRequests deletes duplicates
|
||||
# from deployment_merge_requests table and backfills environment_id
|
||||
class BackfillEnvironmentIdDeploymentMergeRequests
|
||||
def perform(start_mr_id, stop_mr_id)
|
||||
def perform(_start_mr_id, _stop_mr_id)
|
||||
# no-op
|
||||
|
||||
# Background migration removed due to
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/217191
|
||||
end
|
||||
|
||||
def backfill_range(start_mr_id, stop_mr_id)
|
||||
start_mr_id = Integer(start_mr_id)
|
||||
stop_mr_id = Integer(stop_mr_id)
|
||||
|
||||
|
|
|
|||
|
|
@ -28,8 +28,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def deployments_summary
|
||||
@deployments_summary ||=
|
||||
Summary::Deploy.new(project: @project, from: @from, to: @to)
|
||||
@deployments_summary ||= Summary::Deploy.new(project: @project, from: @from, to: @to)
|
||||
end
|
||||
|
||||
def deploy_stats
|
||||
|
|
@ -39,7 +38,7 @@ module Gitlab
|
|||
def deployment_frequency_stats
|
||||
serialize(
|
||||
Summary::DeploymentFrequency.new(
|
||||
deployments: deployments_summary.value,
|
||||
deployments: deployments_summary.value.raw_value,
|
||||
from: @from,
|
||||
to: @to),
|
||||
with_unit: true
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def value
|
||||
@value ||= count_commits
|
||||
@value ||= commits_count ? Value::PrettyNumeric.new(commits_count) : Value::None.new
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -18,10 +18,10 @@ module Gitlab
|
|||
# a limit. Since we need a commit count, we _can't_ enforce a limit, so
|
||||
# the easiest way forward is to replicate the relevant portions of the
|
||||
# `log` function here.
|
||||
def count_commits
|
||||
def commits_count
|
||||
return unless ref
|
||||
|
||||
gitaly_commit_client.commit_count(ref, after: @from, before: @to)
|
||||
@commits_count ||= gitaly_commit_client.commit_count(ref, after: @from, before: @to)
|
||||
end
|
||||
|
||||
def gitaly_commit_client
|
||||
|
|
|
|||
|
|
@ -4,18 +4,20 @@ module Gitlab
|
|||
module CycleAnalytics
|
||||
module Summary
|
||||
class Deploy < Base
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
def title
|
||||
n_('Deploy', 'Deploys', value)
|
||||
end
|
||||
|
||||
def value
|
||||
strong_memoize(:value) do
|
||||
query = @project.deployments.success.where("created_at >= ?", @from)
|
||||
query = query.where("created_at <= ?", @to) if @to
|
||||
query.count
|
||||
end
|
||||
@value ||= Value::PrettyNumeric.new(deployments_count)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def deployments_count
|
||||
query = @project.deployments.success.where("created_at >= ?", @from)
|
||||
query = query.where("created_at <= ?", @to) if @to
|
||||
query.count
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -17,8 +17,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def value
|
||||
@value ||=
|
||||
frequency(@deployments, @from, @to || Time.now)
|
||||
@value ||= frequency(@deployments, @from, @to || Time.now)
|
||||
end
|
||||
|
||||
def unit
|
||||
|
|
|
|||
|
|
@ -16,7 +16,16 @@ module Gitlab
|
|||
end
|
||||
|
||||
def value
|
||||
@value ||= IssuesFinder.new(@current_user, project_id: @project.id, created_after: @from, created_before: @to).execute.count
|
||||
@value ||= Value::PrettyNumeric.new(issues_count)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def issues_count
|
||||
IssuesFinder
|
||||
.new(@current_user, project_id: @project.id, created_after: @from, created_before: @to)
|
||||
.execute
|
||||
.count
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module CycleAnalytics
|
||||
module Summary
|
||||
class Value
|
||||
attr_reader :value
|
||||
|
||||
def raw_value
|
||||
value
|
||||
end
|
||||
|
||||
def to_s
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
class None < self
|
||||
def to_s
|
||||
'-'
|
||||
end
|
||||
end
|
||||
|
||||
class Numeric < self
|
||||
def initialize(value)
|
||||
@value = value
|
||||
end
|
||||
|
||||
def to_s
|
||||
value.zero? ? '0' : value.to_s
|
||||
end
|
||||
end
|
||||
|
||||
class PrettyNumeric < Numeric
|
||||
def to_s
|
||||
# 0 is shown as -
|
||||
value.nonzero? ? super : None.new.to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -4,10 +4,11 @@ module Gitlab
|
|||
module CycleAnalytics
|
||||
module SummaryHelper
|
||||
def frequency(count, from, to)
|
||||
return count if count.zero?
|
||||
return Summary::Value::None.new if count.zero?
|
||||
|
||||
freq = (count / days(from, to)).round(1)
|
||||
freq.zero? ? '0' : freq
|
||||
|
||||
Summary::Value::Numeric.new(freq)
|
||||
end
|
||||
|
||||
def days(from, to)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ module Gitlab
|
|||
class << self
|
||||
include ActionView::RecordIdentifier
|
||||
|
||||
# Using a case statement here is preferable for readability and maintainability.
|
||||
# See discussion in https://gitlab.com/gitlab-org/gitlab/-/issues/217397
|
||||
#
|
||||
# rubocop:disable Metrics/CyclomaticComplexity
|
||||
def build(object, **options)
|
||||
# Objects are sometimes wrapped in a BatchLoader instance
|
||||
|
|
|
|||
|
|
@ -3857,15 +3857,6 @@ msgstr ""
|
|||
msgid "Checkout|(x%{numberOfUsers})"
|
||||
msgstr ""
|
||||
|
||||
msgid "Checkout|1. Your profile"
|
||||
msgstr ""
|
||||
|
||||
msgid "Checkout|2. Checkout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Checkout|3. Your GitLab group"
|
||||
msgstr ""
|
||||
|
||||
msgid "Checkout|Billing address"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -6513,6 +6504,9 @@ msgstr ""
|
|||
msgid "Customize your pipeline configuration."
|
||||
msgstr ""
|
||||
|
||||
msgid "Cycle Time"
|
||||
msgstr ""
|
||||
|
||||
msgid "CycleAnalyticsEvent|Issue closed"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -17256,6 +17250,18 @@ msgstr ""
|
|||
msgid "Registration"
|
||||
msgstr ""
|
||||
|
||||
msgid "Registration|Checkout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Registration|Your GitLab group"
|
||||
msgstr ""
|
||||
|
||||
msgid "Registration|Your first project"
|
||||
msgstr ""
|
||||
|
||||
msgid "Registration|Your profile"
|
||||
msgstr ""
|
||||
|
||||
msgid "Related Deployed Jobs"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ module RuboCop
|
|||
CHECK_LINE_METHODS =
|
||||
Set.new(%i[include_if_ee extend_if_ee prepend_if_ee]).freeze
|
||||
|
||||
CHECK_LINE_METHODS_REGEXP = Regexp.union(CHECK_LINE_METHODS.map(&:to_s)).freeze
|
||||
|
||||
DISALLOW_METHODS = Set.new(%i[include extend prepend]).freeze
|
||||
|
||||
def ee_const?(node)
|
||||
|
|
@ -48,7 +50,13 @@ module RuboCop
|
|||
# the expression is the last line _of code_.
|
||||
last_line -= 1 if buffer.source.end_with?("\n")
|
||||
|
||||
add_offense(node, message: INVALID_LINE) if line < last_line
|
||||
last_line_content = buffer.source.split("\n")[-1]
|
||||
|
||||
if CHECK_LINE_METHODS_REGEXP.match?(last_line_content)
|
||||
ignore_node(node)
|
||||
elsif line < last_line
|
||||
add_offense(node, message: INVALID_LINE)
|
||||
end
|
||||
end
|
||||
|
||||
def verify_argument_type(node)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ describe('Compare diff version dropdowns', () => {
|
|||
};
|
||||
localState.targetBranchName = 'baseVersion';
|
||||
localState.mergeRequestDiffs = diffsMockData;
|
||||
gon.features = { diffCompareWithHead: true };
|
||||
});
|
||||
|
||||
describe('selectedTargetIndex', () => {
|
||||
|
|
@ -129,14 +128,6 @@ describe('Compare diff version dropdowns', () => {
|
|||
});
|
||||
assertVersions(targetVersions);
|
||||
});
|
||||
|
||||
it('does not list head version if feature flag is not enabled', () => {
|
||||
gon.features = { diffCompareWithHead: false };
|
||||
setupTest();
|
||||
const targetVersions = getters.diffCompareDropdownTargetVersions(localState, getters);
|
||||
|
||||
expect(targetVersions.find(version => version.isHead)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('diffCompareDropdownSourceVersions', () => {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ exports[`Dashboard template matches the default snapshot 1`] = `
|
|||
data-qa-selector="dashboards_filter_dropdown"
|
||||
defaultbranch="master"
|
||||
id="monitor-dashboards-dropdown"
|
||||
selecteddashboard="[object Object]"
|
||||
toggle-class="dropdown-menu-toggle"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ const notStarredDashboards = dashboardGitResponse.filter(({ starred }) => !starr
|
|||
describe('DashboardsDropdown', () => {
|
||||
let wrapper;
|
||||
let mockDashboards;
|
||||
let mockSelectedDashboard;
|
||||
|
||||
function createComponent(props, opts = {}) {
|
||||
const storeOpts = {
|
||||
|
|
@ -23,6 +24,7 @@ describe('DashboardsDropdown', () => {
|
|||
},
|
||||
computed: {
|
||||
allDashboards: () => mockDashboards,
|
||||
selectedDashboard: () => mockSelectedDashboard,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -46,6 +48,7 @@ describe('DashboardsDropdown', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
mockDashboards = dashboardGitResponse;
|
||||
mockSelectedDashboard = null;
|
||||
});
|
||||
|
||||
describe('when it receives dashboards data', () => {
|
||||
|
|
@ -153,13 +156,12 @@ describe('DashboardsDropdown', () => {
|
|||
let modalDirective;
|
||||
|
||||
beforeEach(() => {
|
||||
[mockSelectedDashboard] = dashboardGitResponse;
|
||||
modalDirective = jest.fn();
|
||||
duplicateDashboardAction = jest.fn().mockResolvedValue();
|
||||
|
||||
wrapper = createComponent(
|
||||
{
|
||||
selectedDashboard: dashboardGitResponse[0],
|
||||
},
|
||||
{},
|
||||
{
|
||||
directives: {
|
||||
GlModal: modalDirective,
|
||||
|
|
|
|||
|
|
@ -325,6 +325,7 @@ export const dashboardGitResponse = [
|
|||
project_blob_path: null,
|
||||
path: 'config/prometheus/common_metrics.yml',
|
||||
starred: false,
|
||||
user_starred_path: `${mockProjectDir}/metrics_user_starred_dashboards?dashboard_path=config/prometheus/common_metrics.yml`,
|
||||
},
|
||||
{
|
||||
default: false,
|
||||
|
|
@ -334,6 +335,7 @@ export const dashboardGitResponse = [
|
|||
project_blob_path: `${mockProjectDir}/-/blob/master/.gitlab/dashboards/dashboard.yml`,
|
||||
path: '.gitlab/dashboards/dashboard.yml',
|
||||
starred: true,
|
||||
user_starred_path: `${mockProjectDir}/metrics_user_starred_dashboards?dashboard_path=.gitlab/dashboards/dashboard.yml`,
|
||||
},
|
||||
...customDashboardsData,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
fetchEnvironmentsData,
|
||||
fetchDashboardData,
|
||||
fetchAnnotations,
|
||||
toggleStarredValue,
|
||||
fetchPrometheusMetric,
|
||||
setInitialState,
|
||||
filterEnvironments,
|
||||
|
|
@ -350,6 +351,49 @@ describe('Monitoring store actions', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Toggles starred value of current dashboard', () => {
|
||||
const { state } = store;
|
||||
let unstarredDashboard;
|
||||
let starredDashboard;
|
||||
|
||||
beforeEach(() => {
|
||||
state.isUpdatingStarredValue = false;
|
||||
[unstarredDashboard, starredDashboard] = dashboardGitResponse;
|
||||
});
|
||||
|
||||
describe('toggleStarredValue', () => {
|
||||
it('performs no changes if no dashboard is selected', () => {
|
||||
return testAction(toggleStarredValue, null, state, [], []);
|
||||
});
|
||||
|
||||
it('performs no changes if already changing starred value', () => {
|
||||
state.selectedDashboard = unstarredDashboard;
|
||||
state.isUpdatingStarredValue = true;
|
||||
return testAction(toggleStarredValue, null, state, [], []);
|
||||
});
|
||||
|
||||
it('stars dashboard if it is not starred', () => {
|
||||
state.selectedDashboard = unstarredDashboard;
|
||||
mock.onPost(unstarredDashboard.user_starred_path).reply(200);
|
||||
|
||||
return testAction(toggleStarredValue, null, state, [
|
||||
{ type: types.REQUEST_DASHBOARD_STARRING },
|
||||
{ type: types.RECEIVE_DASHBOARD_STARRING_SUCCESS, payload: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it('unstars dashboard if it is starred', () => {
|
||||
state.selectedDashboard = starredDashboard;
|
||||
mock.onPost(starredDashboard.user_starred_path).reply(200);
|
||||
|
||||
return testAction(toggleStarredValue, null, state, [
|
||||
{ type: types.REQUEST_DASHBOARD_STARRING },
|
||||
{ type: types.RECEIVE_DASHBOARD_STARRING_FAILURE },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Set initial state', () => {
|
||||
let mockedState;
|
||||
beforeEach(() => {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import * as getters from '~/monitoring/stores/getters';
|
|||
import mutations from '~/monitoring/stores/mutations';
|
||||
import * as types from '~/monitoring/stores/mutation_types';
|
||||
import { metricStates } from '~/monitoring/constants';
|
||||
import { environmentData, metricsResult } from '../mock_data';
|
||||
import { environmentData, metricsResult, dashboardGitResponse } from '../mock_data';
|
||||
import {
|
||||
metricsDashboardPayload,
|
||||
metricResultStatus,
|
||||
|
|
@ -350,4 +350,48 @@ describe('Monitoring store Getters', () => {
|
|||
expect(variablesArray).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectedDashboard', () => {
|
||||
const { selectedDashboard } = getters;
|
||||
|
||||
it('returns a dashboard', () => {
|
||||
const state = {
|
||||
allDashboards: dashboardGitResponse,
|
||||
currentDashboard: dashboardGitResponse[0].path,
|
||||
};
|
||||
expect(selectedDashboard(state)).toEqual(dashboardGitResponse[0]);
|
||||
});
|
||||
|
||||
it('returns a non-default dashboard', () => {
|
||||
const state = {
|
||||
allDashboards: dashboardGitResponse,
|
||||
currentDashboard: dashboardGitResponse[1].path,
|
||||
};
|
||||
expect(selectedDashboard(state)).toEqual(dashboardGitResponse[1]);
|
||||
});
|
||||
|
||||
it('returns a default dashboard when no dashboard is selected', () => {
|
||||
const state = {
|
||||
allDashboards: dashboardGitResponse,
|
||||
currentDashboard: null,
|
||||
};
|
||||
expect(selectedDashboard(state)).toEqual(dashboardGitResponse[0]);
|
||||
});
|
||||
|
||||
it('returns a default dashboard when dashboard cannot be found', () => {
|
||||
const state = {
|
||||
allDashboards: dashboardGitResponse,
|
||||
currentDashboard: 'wrong_path',
|
||||
};
|
||||
expect(selectedDashboard(state)).toEqual(dashboardGitResponse[0]);
|
||||
});
|
||||
|
||||
it('returns null when no dashboards are present', () => {
|
||||
const state = {
|
||||
allDashboards: [],
|
||||
currentDashboard: dashboardGitResponse[0].path,
|
||||
};
|
||||
expect(selectedDashboard(state)).toEqual(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -72,6 +72,49 @@ describe('Monitoring mutations', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Dashboard starring mutations', () => {
|
||||
it('REQUEST_DASHBOARD_STARRING', () => {
|
||||
stateCopy = { isUpdatingStarredValue: false };
|
||||
mutations[types.REQUEST_DASHBOARD_STARRING](stateCopy);
|
||||
|
||||
expect(stateCopy.isUpdatingStarredValue).toBe(true);
|
||||
});
|
||||
|
||||
describe('RECEIVE_DASHBOARD_STARRING_SUCCESS', () => {
|
||||
let allDashboards;
|
||||
|
||||
beforeEach(() => {
|
||||
allDashboards = [...dashboardGitResponse];
|
||||
stateCopy = {
|
||||
allDashboards,
|
||||
currentDashboard: allDashboards[1].path,
|
||||
isUpdatingStarredValue: true,
|
||||
};
|
||||
});
|
||||
|
||||
it('sets a dashboard as starred', () => {
|
||||
mutations[types.RECEIVE_DASHBOARD_STARRING_SUCCESS](stateCopy, true);
|
||||
|
||||
expect(stateCopy.isUpdatingStarredValue).toBe(false);
|
||||
expect(stateCopy.allDashboards[1].starred).toBe(true);
|
||||
});
|
||||
|
||||
it('sets a dashboard as unstarred', () => {
|
||||
mutations[types.RECEIVE_DASHBOARD_STARRING_SUCCESS](stateCopy, false);
|
||||
|
||||
expect(stateCopy.isUpdatingStarredValue).toBe(false);
|
||||
expect(stateCopy.allDashboards[1].starred).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('RECEIVE_DASHBOARD_STARRING_FAILURE', () => {
|
||||
stateCopy = { isUpdatingStarredValue: true };
|
||||
mutations[types.RECEIVE_DASHBOARD_STARRING_FAILURE](stateCopy);
|
||||
|
||||
expect(stateCopy.isUpdatingStarredValue).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RECEIVE_DEPLOYMENTS_DATA_SUCCESS', () => {
|
||||
it('stores the deployment data', () => {
|
||||
stateCopy.deploymentData = [];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
|
||||
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
|
||||
|
||||
import EditArea from '~/static_site_editor/components/edit_area.vue';
|
||||
import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue';
|
||||
import EditHeader from '~/static_site_editor/components/edit_header.vue';
|
||||
|
||||
import { sourceContentTitle as title, sourceContent as content, returnUrl } from '../mock_data';
|
||||
|
||||
describe('~/static_site_editor/components/edit_area.vue', () => {
|
||||
let wrapper;
|
||||
const savingChanges = true;
|
||||
const newContent = `new ${content}`;
|
||||
|
||||
const buildWrapper = (propsData = {}) => {
|
||||
wrapper = shallowMount(EditArea, {
|
||||
provide: {
|
||||
glFeatures: { richContentEditor: true },
|
||||
},
|
||||
propsData: {
|
||||
title,
|
||||
content,
|
||||
returnUrl,
|
||||
savingChanges,
|
||||
...propsData,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findEditHeader = () => wrapper.find(EditHeader);
|
||||
const findRichContentEditor = () => wrapper.find(RichContentEditor);
|
||||
const findPublishToolbar = () => wrapper.find(PublishToolbar);
|
||||
|
||||
beforeEach(() => {
|
||||
buildWrapper();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('renders edit header', () => {
|
||||
expect(findEditHeader().exists()).toBe(true);
|
||||
expect(findEditHeader().props('title')).toBe(title);
|
||||
});
|
||||
|
||||
it('renders rich content editor', () => {
|
||||
expect(findRichContentEditor().exists()).toBe(true);
|
||||
expect(findRichContentEditor().props('value')).toBe(content);
|
||||
});
|
||||
|
||||
it('renders publish toolbar', () => {
|
||||
expect(findPublishToolbar().exists()).toBe(true);
|
||||
expect(findPublishToolbar().props('returnUrl')).toBe(returnUrl);
|
||||
expect(findPublishToolbar().props('savingChanges')).toBe(savingChanges);
|
||||
expect(findPublishToolbar().props('saveable')).toBe(false);
|
||||
});
|
||||
|
||||
describe('when content changes', () => {
|
||||
beforeEach(() => {
|
||||
findRichContentEditor().vm.$emit('input', newContent);
|
||||
|
||||
return wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('sets publish toolbar as saveable when content changes', () => {
|
||||
expect(findPublishToolbar().props('saveable')).toBe(true);
|
||||
});
|
||||
|
||||
it('sets publish toolbar as not saveable when content changes are rollback', () => {
|
||||
findRichContentEditor().vm.$emit('input', content);
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findPublishToolbar().props('saveable')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,21 +1,20 @@
|
|||
import Vuex from 'vuex';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import { GlSkeletonLoader } from '@gitlab/ui';
|
||||
|
||||
import createState from '~/static_site_editor/store/state';
|
||||
|
||||
import Home from '~/static_site_editor/pages/home.vue';
|
||||
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
|
||||
import EditHeader from '~/static_site_editor/components/edit_header.vue';
|
||||
|
||||
import SkeletonLoader from '~/static_site_editor/components/skeleton_loader.vue';
|
||||
import EditArea from '~/static_site_editor/components/edit_area.vue';
|
||||
import InvalidContentMessage from '~/static_site_editor/components/invalid_content_message.vue';
|
||||
import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue';
|
||||
import SubmitChangesError from '~/static_site_editor/components/submit_changes_error.vue';
|
||||
import SavedChangesMessage from '~/static_site_editor/components/saved_changes_message.vue';
|
||||
|
||||
import {
|
||||
returnUrl,
|
||||
sourceContent,
|
||||
sourceContentTitle,
|
||||
sourceContent as content,
|
||||
sourceContentTitle as title,
|
||||
savedContentMeta,
|
||||
submitChangesError,
|
||||
} from '../mock_data';
|
||||
|
|
@ -27,13 +26,12 @@ localVue.use(Vuex);
|
|||
describe('static_site_editor/pages/home', () => {
|
||||
let wrapper;
|
||||
let store;
|
||||
let loadContentActionMock;
|
||||
let $apollo;
|
||||
let setContentActionMock;
|
||||
let submitChangesActionMock;
|
||||
let dismissSubmitChangesErrorActionMock;
|
||||
|
||||
const buildStore = ({ initialState, getters } = {}) => {
|
||||
loadContentActionMock = jest.fn();
|
||||
setContentActionMock = jest.fn();
|
||||
submitChangesActionMock = jest.fn();
|
||||
dismissSubmitChangesErrorActionMock = jest.fn();
|
||||
|
|
@ -47,53 +45,55 @@ describe('static_site_editor/pages/home', () => {
|
|||
...getters,
|
||||
},
|
||||
actions: {
|
||||
loadContent: loadContentActionMock,
|
||||
setContent: setContentActionMock,
|
||||
submitChanges: submitChangesActionMock,
|
||||
dismissSubmitChangesError: dismissSubmitChangesErrorActionMock,
|
||||
},
|
||||
});
|
||||
};
|
||||
const buildContentLoadedStore = ({ initialState, getters } = {}) => {
|
||||
buildStore({
|
||||
initialState: {
|
||||
isContentLoaded: true,
|
||||
...initialState,
|
||||
|
||||
const buildApollo = (queries = {}) => {
|
||||
$apollo = {
|
||||
queries: {
|
||||
sourceContent: {
|
||||
loading: false,
|
||||
},
|
||||
...queries,
|
||||
},
|
||||
getters: {
|
||||
...getters,
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
const buildWrapper = (data = { appData: { isSupportedContent: true } }) => {
|
||||
const buildWrapper = (data = {}) => {
|
||||
wrapper = shallowMount(Home, {
|
||||
localVue,
|
||||
store,
|
||||
provide: {
|
||||
glFeatures: { richContentEditor: true },
|
||||
mocks: {
|
||||
$apollo,
|
||||
},
|
||||
data() {
|
||||
return data;
|
||||
return {
|
||||
appData: { isSupportedContent: true, returnUrl },
|
||||
...data,
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findRichContentEditor = () => wrapper.find(RichContentEditor);
|
||||
const findEditHeader = () => wrapper.find(EditHeader);
|
||||
const findEditArea = () => wrapper.find(EditArea);
|
||||
const findInvalidContentMessage = () => wrapper.find(InvalidContentMessage);
|
||||
const findPublishToolbar = () => wrapper.find(PublishToolbar);
|
||||
const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
|
||||
const findSkeletonLoader = () => wrapper.find(SkeletonLoader);
|
||||
const findSubmitChangesError = () => wrapper.find(SubmitChangesError);
|
||||
const findSavedChangesMessage = () => wrapper.find(SavedChangesMessage);
|
||||
|
||||
beforeEach(() => {
|
||||
buildApollo();
|
||||
buildStore();
|
||||
buildWrapper();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
$apollo = null;
|
||||
});
|
||||
|
||||
it('renders the saved changes message when changes are submitted successfully', () => {
|
||||
|
|
@ -107,103 +107,69 @@ describe('static_site_editor/pages/home', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when content is not loaded', () => {
|
||||
it('does not render rich content editor', () => {
|
||||
expect(findRichContentEditor().exists()).toBe(false);
|
||||
});
|
||||
it('does not render the saved changes message when changes are not submitted', () => {
|
||||
buildWrapper();
|
||||
|
||||
it('does not render edit header', () => {
|
||||
expect(findEditHeader().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not render toolbar', () => {
|
||||
expect(findPublishToolbar().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not render saved changes message', () => {
|
||||
expect(findSavedChangesMessage().exists()).toBe(false);
|
||||
});
|
||||
expect(findSavedChangesMessage().exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('when content is loaded', () => {
|
||||
const content = sourceContent;
|
||||
const title = sourceContentTitle;
|
||||
|
||||
beforeEach(() => {
|
||||
buildContentLoadedStore({ initialState: { content, title } });
|
||||
buildWrapper();
|
||||
buildStore({ initialState: { isSavingChanges: true } });
|
||||
buildWrapper({ sourceContent: { title, content } });
|
||||
});
|
||||
|
||||
it('renders the rich content editor', () => {
|
||||
expect(findRichContentEditor().exists()).toBe(true);
|
||||
it('renders edit area', () => {
|
||||
expect(findEditArea().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders the edit header', () => {
|
||||
expect(findEditHeader().exists()).toBe(true);
|
||||
it('provides source content to the edit area', () => {
|
||||
expect(findEditArea().props()).toMatchObject({
|
||||
title,
|
||||
content,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render skeleton loader', () => {
|
||||
expect(findSkeletonLoader().exists()).toBe(false);
|
||||
it('provides returnUrl to the edit area', () => {
|
||||
expect(findEditArea().props('returnUrl')).toBe(returnUrl);
|
||||
});
|
||||
|
||||
it('passes page content to the rich content editor', () => {
|
||||
expect(findRichContentEditor().props('value')).toBe(content);
|
||||
});
|
||||
|
||||
it('passes page title to edit header', () => {
|
||||
expect(findEditHeader().props('title')).toBe(title);
|
||||
});
|
||||
|
||||
it('renders toolbar', () => {
|
||||
expect(findPublishToolbar().exists()).toBe(true);
|
||||
it('provides isSavingChanges to the edit area', () => {
|
||||
expect(findEditArea().props('savingChanges')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('sets toolbar as saveable when content changes', () => {
|
||||
buildContentLoadedStore({
|
||||
getters: {
|
||||
contentChanged: () => true,
|
||||
it('does not render edit area when content is not loaded', () => {
|
||||
buildWrapper({ sourceContent: null });
|
||||
|
||||
expect(findEditArea().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('renders skeleton loader when content is not loading', () => {
|
||||
buildApollo({
|
||||
sourceContent: {
|
||||
loading: true,
|
||||
},
|
||||
});
|
||||
buildWrapper();
|
||||
|
||||
expect(findPublishToolbar().props('saveable')).toBe(true);
|
||||
});
|
||||
|
||||
it('displays skeleton loader when loading content', () => {
|
||||
buildStore({ initialState: { isLoadingContent: true } });
|
||||
buildWrapper();
|
||||
|
||||
expect(findSkeletonLoader().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not display submit changes error when an error does not exist', () => {
|
||||
buildContentLoadedStore();
|
||||
buildWrapper();
|
||||
|
||||
expect(findSubmitChangesError().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('sets toolbar as saving when saving changes', () => {
|
||||
buildContentLoadedStore({
|
||||
initialState: {
|
||||
isSavingChanges: true,
|
||||
it('does not render skeleton loader when content is not loading', () => {
|
||||
buildApollo({
|
||||
sourceContent: {
|
||||
loading: false,
|
||||
},
|
||||
});
|
||||
buildWrapper();
|
||||
|
||||
expect(findPublishToolbar().props('savingChanges')).toBe(true);
|
||||
});
|
||||
|
||||
it('displays invalid content message when content is not supported', () => {
|
||||
buildWrapper({ appData: { isSupportedContent: false } });
|
||||
|
||||
expect(findInvalidContentMessage().exists()).toBe(true);
|
||||
expect(findSkeletonLoader().exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('when submitting changes fail', () => {
|
||||
beforeEach(() => {
|
||||
buildContentLoadedStore({
|
||||
buildStore({
|
||||
initialState: {
|
||||
submitChangesError,
|
||||
},
|
||||
|
|
@ -228,24 +194,32 @@ describe('static_site_editor/pages/home', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('dispatches load content action', () => {
|
||||
expect(loadContentActionMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('dispatches setContent action when rich content editor emits input event', () => {
|
||||
buildContentLoadedStore();
|
||||
it('does not display submit changes error when an error does not exist', () => {
|
||||
buildWrapper();
|
||||
|
||||
findRichContentEditor().vm.$emit('input', sourceContent);
|
||||
|
||||
expect(setContentActionMock).toHaveBeenCalledWith(expect.anything(), sourceContent, undefined);
|
||||
expect(findSubmitChangesError().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('dispatches submitChanges action when toolbar emits submit event', () => {
|
||||
buildContentLoadedStore();
|
||||
buildWrapper();
|
||||
findPublishToolbar().vm.$emit('submit');
|
||||
it('displays invalid content message when content is not supported', () => {
|
||||
buildWrapper({ appData: { isSupportedContent: false } });
|
||||
|
||||
expect(submitChangesActionMock).toHaveBeenCalled();
|
||||
expect(findInvalidContentMessage().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('when edit area emits submit event', () => {
|
||||
const newContent = `new ${content}`;
|
||||
|
||||
beforeEach(() => {
|
||||
buildWrapper({ sourceContent: { title, content } });
|
||||
findEditArea().vm.$emit('submit', { content: newContent });
|
||||
});
|
||||
|
||||
it('dispatches setContent property', () => {
|
||||
expect(setContentActionMock).toHaveBeenCalledWith(expect.anything(), newContent, undefined);
|
||||
});
|
||||
|
||||
it('dispatches submitChanges action', () => {
|
||||
expect(submitChangesActionMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import Vuex from 'vuex';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
|
||||
import { GlButton, GlLoadingIcon, GlIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui';
|
||||
import { GlButton, GlLoadingIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui';
|
||||
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
|
||||
import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue';
|
||||
import LabelItem from '~/vue_shared/components/sidebar/labels_select_vue/label_item.vue';
|
||||
|
||||
import defaultState from '~/vue_shared/components/sidebar/labels_select_vue/store/state';
|
||||
import mutations from '~/vue_shared/components/sidebar/labels_select_vue/store/mutations';
|
||||
|
|
@ -78,16 +79,6 @@ describe('DropdownContentsLabelsView', () => {
|
|||
});
|
||||
|
||||
describe('methods', () => {
|
||||
describe('getDropdownLabelBoxStyle', () => {
|
||||
it('returns an object containing `backgroundColor` based on provided `label` param', () => {
|
||||
expect(wrapper.vm.getDropdownLabelBoxStyle(mockRegularLabel)).toEqual(
|
||||
expect.objectContaining({
|
||||
backgroundColor: mockRegularLabel.color,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isLabelSelected', () => {
|
||||
it('returns true when provided `label` param is one of the selected labels', () => {
|
||||
expect(wrapper.vm.isLabelSelected(mockRegularLabel)).toBe(true);
|
||||
|
|
@ -234,16 +225,7 @@ describe('DropdownContentsLabelsView', () => {
|
|||
});
|
||||
|
||||
it('renders label elements for all labels', () => {
|
||||
const labelsEl = wrapper.findAll('.dropdown-content li');
|
||||
const labelItemEl = labelsEl.at(0).find(GlLink);
|
||||
|
||||
expect(labelsEl.length).toBe(mockLabels.length);
|
||||
expect(labelItemEl.exists()).toBe(true);
|
||||
expect(labelItemEl.find(GlIcon).props('name')).toBe('mobile-issue-close');
|
||||
expect(labelItemEl.find('.dropdown-label-box').attributes('style')).toBe(
|
||||
'background-color: rgb(186, 218, 85);',
|
||||
);
|
||||
expect(labelItemEl.find(GlLink).text()).toContain(mockLabels[0].title);
|
||||
expect(wrapper.findAll(LabelItem)).toHaveLength(mockLabels.length);
|
||||
});
|
||||
|
||||
it('renders label element with "is-focused" when value of `currentHighlightItem` is more than -1', () => {
|
||||
|
|
@ -253,9 +235,9 @@ describe('DropdownContentsLabelsView', () => {
|
|||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
const labelsEl = wrapper.findAll('.dropdown-content li');
|
||||
const labelItemEl = labelsEl.at(0).find(GlLink);
|
||||
const labelItemEl = labelsEl.at(0).find(LabelItem);
|
||||
|
||||
expect(labelItemEl.attributes('class')).toContain('is-focused');
|
||||
expect(labelItemEl.props('highlight')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -267,7 +249,7 @@ describe('DropdownContentsLabelsView', () => {
|
|||
return wrapper.vm.$nextTick(() => {
|
||||
const noMatchEl = wrapper.find('.dropdown-content li');
|
||||
|
||||
expect(noMatchEl.exists()).toBe(true);
|
||||
expect(noMatchEl.isVisible()).toBe(true);
|
||||
expect(noMatchEl.text()).toContain('No matching results');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,111 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
|
||||
import { GlIcon, GlLink } from '@gitlab/ui';
|
||||
import LabelItem from '~/vue_shared/components/sidebar/labels_select_vue/label_item.vue';
|
||||
import { mockRegularLabel } from './mock_data';
|
||||
|
||||
const createComponent = ({ label = mockRegularLabel, highlight = true } = {}) =>
|
||||
shallowMount(LabelItem, {
|
||||
propsData: {
|
||||
label,
|
||||
highlight,
|
||||
},
|
||||
});
|
||||
|
||||
describe('LabelItem', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('computed', () => {
|
||||
describe('labelBoxStyle', () => {
|
||||
it('returns an object containing `backgroundColor` based on `label` prop', () => {
|
||||
expect(wrapper.vm.labelBoxStyle).toEqual(
|
||||
expect.objectContaining({
|
||||
backgroundColor: mockRegularLabel.color,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
describe('handleClick', () => {
|
||||
it('sets value of `isSet` data prop to opposite of its current value', () => {
|
||||
wrapper.setData({
|
||||
isSet: true,
|
||||
});
|
||||
|
||||
wrapper.vm.handleClick();
|
||||
expect(wrapper.vm.isSet).toBe(false);
|
||||
wrapper.vm.handleClick();
|
||||
expect(wrapper.vm.isSet).toBe(true);
|
||||
});
|
||||
|
||||
it('emits event `clickLabel` on component with `label` prop as param', () => {
|
||||
wrapper.vm.handleClick();
|
||||
|
||||
expect(wrapper.emitted('clickLabel')).toBeTruthy();
|
||||
expect(wrapper.emitted('clickLabel')[0]).toEqual([mockRegularLabel]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
it('renders gl-link component', () => {
|
||||
expect(wrapper.find(GlLink).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders gl-link component with class `is-focused` when `highlight` prop is true', () => {
|
||||
wrapper.setProps({
|
||||
highlight: true,
|
||||
});
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
expect(wrapper.find(GlLink).classes()).toContain('is-focused');
|
||||
});
|
||||
});
|
||||
|
||||
it('renders visible gl-icon component when `isSet` prop is true', () => {
|
||||
wrapper.setData({
|
||||
isSet: true,
|
||||
});
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
const iconEl = wrapper.find(GlIcon);
|
||||
|
||||
expect(iconEl.isVisible()).toBe(true);
|
||||
expect(iconEl.props('name')).toBe('mobile-issue-close');
|
||||
});
|
||||
});
|
||||
|
||||
it('renders visible span element as placeholder instead of gl-icon when `isSet` prop is false', () => {
|
||||
wrapper.setData({
|
||||
isSet: false,
|
||||
});
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
const placeholderEl = wrapper.find('[data-testid="no-icon"]');
|
||||
|
||||
expect(placeholderEl.isVisible()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders label color element', () => {
|
||||
const colorEl = wrapper.find('[data-testid="label-color-box"]');
|
||||
|
||||
expect(colorEl.exists()).toBe(true);
|
||||
expect(colorEl.attributes('style')).toBe('background-color: rgb(186, 218, 85);');
|
||||
});
|
||||
|
||||
it('renders label title', () => {
|
||||
expect(wrapper.text()).toContain(mockRegularLabel.title);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -155,29 +155,12 @@ describe('LabelsSelect Mutations', () => {
|
|||
describe(`${types.UPDATE_SELECTED_LABELS}`, () => {
|
||||
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
|
||||
|
||||
it('updates `state.labels` to include `touched` and `set` props based on provided `labels` param when `state.allowMultiselect` is `true`', () => {
|
||||
const updatedLabelIds = [2, 4];
|
||||
const state = {
|
||||
labels,
|
||||
allowMultiselect: true,
|
||||
};
|
||||
mutations[types.UPDATE_SELECTED_LABELS](state, { labels });
|
||||
|
||||
state.labels.forEach(label => {
|
||||
if (updatedLabelIds.includes(label.id)) {
|
||||
expect(label.touched).toBe(true);
|
||||
expect(label.set).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('updates `state.labels` to include `touched` and `set` props based on provided `labels` param when `state.allowMultiselect` is `false`', () => {
|
||||
it('updates `state.labels` to include `touched` and `set` props based on provided `labels` param', () => {
|
||||
const updatedLabelIds = [2];
|
||||
const state = {
|
||||
labels,
|
||||
allowMultiselect: false,
|
||||
};
|
||||
mutations[types.UPDATE_SELECTED_LABELS](state, { labels });
|
||||
mutations[types.UPDATE_SELECTED_LABELS](state, { labels: [{ id: 2 }] });
|
||||
|
||||
state.labels.forEach(label => {
|
||||
if (updatedLabelIds.includes(label.id)) {
|
||||
|
|
|
|||
|
|
@ -85,4 +85,19 @@ describe MilestonesHelper do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#group_milestone_route" do
|
||||
let(:group) { build_stubbed(:group) }
|
||||
let(:subgroup) { build_stubbed(:group, parent: group, name: "Test Subgrp") }
|
||||
|
||||
context "when in subgroup" do
|
||||
let(:milestone) { build_stubbed(:group_milestone, group: subgroup) }
|
||||
|
||||
it 'generates correct url despite assigned @group' do
|
||||
assign(:group, group)
|
||||
milestone_path = "/groups/#{subgroup.full_path}/-/milestones/#{milestone.iid}"
|
||||
expect(helper.group_milestone_route(milestone)).to eq(milestone_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ describe Gitlab::BackgroundMigration::BackfillEnvironmentIdDeploymentMergeReques
|
|||
|
||||
expect(deployment_merge_requests.where(environment_id: nil).count).to eq(3)
|
||||
|
||||
migration.perform(1, mr.id)
|
||||
migration.backfill_range(1, mr.id)
|
||||
|
||||
expect(deployment_merge_requests.where(environment_id: nil).count).to be_zero
|
||||
expect(deployment_merge_requests.count).to eq(2)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::CycleAnalytics::Summary::Value do
|
||||
describe Gitlab::CycleAnalytics::Summary::Value::None do
|
||||
it 'returns `-`' do
|
||||
expect(described_class.new.to_s).to eq('-')
|
||||
end
|
||||
end
|
||||
|
||||
describe Gitlab::CycleAnalytics::Summary::Value::Numeric do
|
||||
it 'returns the string representation of the number' do
|
||||
expect(described_class.new(3.2).to_s).to eq('3.2')
|
||||
end
|
||||
end
|
||||
|
||||
describe Gitlab::CycleAnalytics::Summary::Value::PrettyNumeric do
|
||||
describe '#to_s' do
|
||||
it 'returns `-` when the number is 0' do
|
||||
expect(described_class.new(0).to_s).to eq('-')
|
||||
end
|
||||
|
||||
it 'returns the string representation of the number' do
|
||||
expect(described_class.new(100).to_s).to eq('100')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require Rails.root.join('db', 'post_migrate', '20200312134637_backfill_environment_id_on_deployment_merge_requests.rb')
|
||||
|
||||
describe BackfillEnvironmentIdOnDeploymentMergeRequests do
|
||||
let(:environments) { table(:environments) }
|
||||
let(:merge_requests) { table(:merge_requests) }
|
||||
let(:deployments) { table(:deployments) }
|
||||
let(:deployment_merge_requests) { table(:deployment_merge_requests) }
|
||||
let(:namespaces) { table(:namespaces) }
|
||||
let(:projects) { table(:projects) }
|
||||
|
||||
let(:migration_worker) { double('BackgroundMigrationWorker') }
|
||||
|
||||
before do
|
||||
stub_const('BackgroundMigrationWorker', migration_worker)
|
||||
end
|
||||
|
||||
it 'schedules nothing when there are no entries' do
|
||||
expect(migration_worker).not_to receive(:perform_in)
|
||||
|
||||
migrate!
|
||||
end
|
||||
|
||||
it 'batches the workload' do
|
||||
stub_const("#{described_class.name}::BATCH_SIZE", 10)
|
||||
|
||||
namespace = namespaces.create!(name: 'foo', path: 'foo')
|
||||
project = projects.create!(namespace_id: namespace.id)
|
||||
|
||||
environment = environments.create!(project_id: project.id, name: 'staging', slug: 'staging')
|
||||
|
||||
# Batching is based on DeploymentMergeRequest.merge_request_id, in order to test it
|
||||
# we must generate more than described_class::BATCH_SIZE merge requests, deployments,
|
||||
# and deployment_merge_requests entries
|
||||
entries = 13
|
||||
expect(entries).to be > described_class::BATCH_SIZE
|
||||
|
||||
# merge requests and deployments bulk generation
|
||||
mrs_params = []
|
||||
deployments_params = []
|
||||
entries.times do |i|
|
||||
mrs_params << { source_branch: 'x', target_branch: 'master', target_project_id: project.id }
|
||||
|
||||
deployments_params << { environment_id: environment.id, iid: i + 1, project_id: project.id, ref: 'master', tag: false, sha: '123abcdef', status: 1 }
|
||||
end
|
||||
|
||||
all_mrs = merge_requests.insert_all(mrs_params)
|
||||
all_deployments = deployments.insert_all(deployments_params)
|
||||
|
||||
# deployment_merge_requests bulk generation
|
||||
dmr_params = []
|
||||
entries.times do |index|
|
||||
mr_id = all_mrs.rows[index].first
|
||||
deployment_id = all_deployments.rows[index].first
|
||||
|
||||
dmr_params << { deployment_id: deployment_id, merge_request_id: mr_id }
|
||||
end
|
||||
|
||||
deployment_merge_requests.insert_all(dmr_params)
|
||||
|
||||
first_batch_limit = dmr_params[described_class::BATCH_SIZE][:merge_request_id]
|
||||
second_batch_limit = dmr_params.last[:merge_request_id]
|
||||
|
||||
expect(migration_worker).to receive(:perform_in)
|
||||
.with(
|
||||
0,
|
||||
'BackfillEnvironmentIdDeploymentMergeRequests',
|
||||
[1, first_batch_limit]
|
||||
)
|
||||
expect(migration_worker).to receive(:perform_in)
|
||||
.with(
|
||||
described_class::DELAY,
|
||||
'BackfillEnvironmentIdDeploymentMergeRequests',
|
||||
[first_batch_limit + 1, second_batch_limit]
|
||||
)
|
||||
|
||||
migrate!
|
||||
end
|
||||
end
|
||||
|
|
@ -3836,40 +3836,28 @@ describe MergeRequest do
|
|||
end
|
||||
|
||||
describe '#diffable_merge_ref?' do
|
||||
context 'diff_compare_with_head enabled' do
|
||||
context 'merge request can be merged' do
|
||||
context 'merge_to_ref is not calculated' do
|
||||
it 'returns true' do
|
||||
expect(subject.diffable_merge_ref?).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'merge_to_ref is calculated' do
|
||||
before do
|
||||
MergeRequests::MergeToRefService.new(subject.project, subject.author).execute(subject)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(subject.diffable_merge_ref?).to eq(true)
|
||||
end
|
||||
context 'merge request can be merged' do
|
||||
context 'merge_to_ref is not calculated' do
|
||||
it 'returns true' do
|
||||
expect(subject.diffable_merge_ref?).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'merge request cannot be merged' do
|
||||
it 'returns false' do
|
||||
subject.mark_as_unchecked!
|
||||
context 'merge_to_ref is calculated' do
|
||||
before do
|
||||
MergeRequests::MergeToRefService.new(subject.project, subject.author).execute(subject)
|
||||
end
|
||||
|
||||
expect(subject.diffable_merge_ref?).to eq(false)
|
||||
it 'returns true' do
|
||||
expect(subject.diffable_merge_ref?).to eq(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'diff_compare_with_head disabled' do
|
||||
before do
|
||||
stub_feature_flags(diff_compare_with_head: { enabled: false, thing: subject.target_project })
|
||||
end
|
||||
|
||||
context 'merge request cannot be merged' do
|
||||
it 'returns false' do
|
||||
subject.mark_as_unchecked!
|
||||
|
||||
expect(subject.diffable_merge_ref?).to eq(false)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -326,4 +326,26 @@ describe SentNotification do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#position=" do
|
||||
subject { build(:sent_notification, noteable: create(:issue)) }
|
||||
|
||||
it "doesn't accept non-hash JSON passed as a string" do
|
||||
subject.position = "true"
|
||||
|
||||
expect(subject.attributes_before_type_cast["position"]).to be(nil)
|
||||
end
|
||||
|
||||
it "does accept a position hash as a string" do
|
||||
subject.position = '{ "base_sha": "test" }'
|
||||
|
||||
expect(subject.position.base_sha).to eq("test")
|
||||
end
|
||||
|
||||
it "does accept a hash" do
|
||||
subject.position = { "base_sha" => "test" }
|
||||
|
||||
expect(subject.position.base_sha).to eq("test")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -176,15 +176,21 @@ describe Ci::BuildPolicy do
|
|||
end
|
||||
|
||||
context 'when developers can push to the branch' do
|
||||
before do
|
||||
create(:protected_branch, :developers_can_push,
|
||||
name: build.ref, project: project)
|
||||
end
|
||||
|
||||
context 'when the build was created by the developer' do
|
||||
let(:owner) { user }
|
||||
|
||||
it { expect(policy).to be_allowed :erase_build }
|
||||
context 'when the build was created for a protected ref' do
|
||||
before do
|
||||
create(:protected_branch, :developers_can_push,
|
||||
name: build.ref, project: project)
|
||||
end
|
||||
|
||||
it { expect(policy).to be_disallowed :erase_build }
|
||||
end
|
||||
|
||||
context 'when the build was created for an unprotected ref' do
|
||||
it { expect(policy).to be_allowed :erase_build }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the build was created by the other' do
|
||||
|
|
|
|||
|
|
@ -630,7 +630,7 @@ describe API::Branches do
|
|||
post api(route, user), params: { branch: 'new_design3', ref: 'foo' }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(json_response['message']).to eq('Invalid reference name: new_design3')
|
||||
expect(json_response['message']).to eq('Invalid reference name: foo')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ describe 'Creation of a new branch' do
|
|||
let(:ref) { 'unknown' }
|
||||
|
||||
it_behaves_like 'a mutation that returns errors in the response',
|
||||
errors: ['Invalid reference name: new_branch']
|
||||
errors: ['Invalid reference name: unknown']
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -159,6 +159,17 @@ describe RuboCop::Cop::InjectEnterpriseEditionModule do
|
|||
SOURCE
|
||||
end
|
||||
|
||||
it 'does not flag the double use of `X_if_ee` on the last line' do
|
||||
expect_no_offenses(<<~SOURCE)
|
||||
class Foo
|
||||
end
|
||||
|
||||
Foo.extend_if_ee('EE::Foo')
|
||||
Foo.include_if_ee('EE::Foo')
|
||||
Foo.prepend_if_ee('EE::Foo')
|
||||
SOURCE
|
||||
end
|
||||
|
||||
it 'autocorrects offenses by just disabling the Cop' do
|
||||
source = <<~SOURCE
|
||||
class Foo
|
||||
|
|
|
|||
|
|
@ -3,39 +3,45 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Branches::CreateService do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
subject(:service) { described_class.new(project, user) }
|
||||
|
||||
let_it_be(:project) { create(:project_empty_repo) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
describe '#execute' do
|
||||
context 'when repository is empty' do
|
||||
let(:project) { create(:project_empty_repo) }
|
||||
|
||||
it 'creates master branch' do
|
||||
service.execute('my-feature', 'master')
|
||||
|
||||
expect(project.repository.branch_exists?('master')).to be_truthy
|
||||
end
|
||||
|
||||
it 'creates my-feature branch' do
|
||||
service.execute('my-feature', 'master')
|
||||
it 'creates another-feature branch' do
|
||||
service.execute('another-feature', 'master')
|
||||
|
||||
expect(project.repository.branch_exists?('my-feature')).to be_truthy
|
||||
expect(project.repository.branch_exists?('another-feature')).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when creating a branch fails' do
|
||||
let(:project) { create(:project_empty_repo) }
|
||||
context 'when branch already exists' do
|
||||
it 'returns an error' do
|
||||
result = service.execute('master', 'master')
|
||||
|
||||
expect(result[:status]).to eq(:error)
|
||||
expect(result[:message]).to eq('Branch already exists')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when incorrect reference is provided' do
|
||||
before do
|
||||
allow(project.repository).to receive(:add_branch).and_return(false)
|
||||
end
|
||||
|
||||
it 'returns an error with the branch name' do
|
||||
result = service.execute('my-feature', 'master')
|
||||
it 'returns an error with a reference name' do
|
||||
result = service.execute('new-feature', 'unknown')
|
||||
|
||||
expect(result[:status]).to eq(:error)
|
||||
expect(result[:message]).to eq("Invalid reference name: my-feature")
|
||||
expect(result[:message]).to eq('Invalid reference name: unknown')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue