Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-10-05 09:08:17 +00:00
parent 34cd22d1a9
commit 2fe341d705
46 changed files with 391 additions and 223 deletions

View file

@ -1 +0,0 @@
<svg width="17" height="17" viewBox="0 0 17 17" xmlns="http://www.w3.org/2000/svg"><path d="M.147 15.496l2.146-2.146-1.286-1.286a.55.55 0 0 1-.125-.616c.101-.238.277-.357.527-.357h4a.55.55 0 0 1 .402.17.55.55 0 0 1 .17.401v4c0 .25-.12.426-.358.527-.232.101-.437.06-.616-.125l-1.286-1.286-2.146 2.146-1.428-1.428zM14.996.646l1.428 1.43-2.146 2.145 1.286 1.286c.185.179.226.384.125.616-.101.238-.277.357-.527.357h-4a.55.55 0 0 1-.402-.17.55.55 0 0 1-.17-.401v-4c0-.25.12-.426.358-.527a.553.553 0 0 1 .616.125l1.286 1.286L14.996.647zm-13.42 0L3.72 2.794l1.286-1.286a.55.55 0 0 1 .616-.125c.238.101.357.277.357.527v4a.55.55 0 0 1-.17.402.55.55 0 0 1-.401.17h-4c-.25 0-.426-.12-.527-.358-.101-.232-.06-.437.125-.616l1.286-1.286L.147 2.075 1.575.647zm14.848 14.85l-1.428 1.428-2.146-2.146-1.286 1.286c-.179.185-.384.226-.616.125-.238-.101-.357-.277-.357-.527v-4a.55.55 0 0 1 .17-.402.55.55 0 0 1 .401-.17h4c.25 0 .426.12.527.358a.553.553 0 0 1-.125.616l-1.286 1.286 2.146 2.146z" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 1,002 B

View file

@ -1 +0,0 @@
<svg width="15" height="15" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg"><path d="M8.591 5.056l2.147-2.146-1.286-1.286a.55.55 0 0 1-.125-.616c.101-.238.277-.357.527-.357h4a.55.55 0 0 1 .402.17.55.55 0 0 1 .17.401v4c0 .25-.12.426-.358.527-.232.101-.437.06-.616-.125l-1.286-1.286-2.146 2.147-1.429-1.43zM5.018 8.553l1.429 1.43L4.3 12.127l1.286 1.286c.185.179.226.384.125.616-.101.238-.277.357-.527.357h-4a.55.55 0 0 1-.402-.17.55.55 0 0 1-.17-.401v-4c0-.25.12-.426.358-.527a.553.553 0 0 1 .616.125L2.872 10.7l2.146-2.147zm4.964 0l2.146 2.147 1.286-1.286a.55.55 0 0 1 .616-.125c.238.101.357.277.357.527v4a.55.55 0 0 1-.17.402.55.55 0 0 1-.401.17h-4c-.25 0-.426-.12-.527-.358-.101-.232-.06-.437.125-.616l1.286-1.286-2.147-2.146 1.43-1.429zM6.447 5.018l-1.43 1.429L2.873 4.3 1.586 5.586c-.179.185-.384.226-.616.125-.238-.101-.357-.277-.357-.527v-4a.55.55 0 0 1 .17-.402.55.55 0 0 1 .401-.17h4c.25 0 .426.12.527.358a.553.553 0 0 1-.125.616L4.3 2.872l2.147 2.146z" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 996 B

View file

@ -10,11 +10,19 @@ const notImplemented = () => {
throw new Error('Not implemented!'); throw new Error('Not implemented!');
}; };
const removeIssueFromList = (state, listId, issueId) => { const getListById = ({ state, listId }) => {
Vue.set(state.issuesByListId, listId, pull(state.issuesByListId[listId], issueId)); const listIndex = state.boardLists.findIndex(l => l.id === listId);
const list = state.boardLists[listIndex];
return { listIndex, list };
}; };
const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfterId, atIndex }) => { export const removeIssueFromList = ({ state, listId, issueId }) => {
Vue.set(state.issuesByListId, listId, pull(state.issuesByListId[listId], issueId));
const { listIndex, list } = getListById({ state, listId });
Vue.set(state.boardLists, listIndex, { ...list, issuesSize: list.issuesSize - 1 });
};
export const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfterId, atIndex }) => {
const listIssues = state.issuesByListId[listId]; const listIssues = state.issuesByListId[listId];
let newIndex = atIndex || 0; let newIndex = atIndex || 0;
if (moveBeforeId) { if (moveBeforeId) {
@ -24,6 +32,8 @@ const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfterId, atI
} }
listIssues.splice(newIndex, 0, issueId); listIssues.splice(newIndex, 0, issueId);
Vue.set(state.issuesByListId, listId, listIssues); Vue.set(state.issuesByListId, listId, listIssues);
const { listIndex, list } = getListById({ state, listId });
Vue.set(state.boardLists, listIndex, { ...list, issuesSize: list.issuesSize + 1 });
}; };
export default { export default {
@ -142,7 +152,7 @@ export default {
const issue = moveIssueListHelper(originalIssue, fromList, toList); const issue = moveIssueListHelper(originalIssue, fromList, toList);
Vue.set(state.issues, issue.id, issue); Vue.set(state.issues, issue.id, issue);
removeIssueFromList(state, fromListId, issue.id); removeIssueFromList({ state, listId: fromListId, issueId: issue.id });
addIssueToList({ state, listId: toListId, issueId: issue.id, moveBeforeId, moveAfterId }); addIssueToList({ state, listId: toListId, issueId: issue.id, moveBeforeId, moveAfterId });
}, },
@ -157,7 +167,7 @@ export default {
) => { ) => {
state.error = s__('Boards|An error occurred while moving the issue. Please try again.'); state.error = s__('Boards|An error occurred while moving the issue. Please try again.');
Vue.set(state.issues, originalIssue.id, originalIssue); Vue.set(state.issues, originalIssue.id, originalIssue);
removeIssueFromList(state, toListId, originalIssue.id); removeIssueFromList({ state, listId: toListId, issueId: originalIssue.id });
addIssueToList({ addIssueToList({
state, state,
listId: fromListId, listId: fromListId,
@ -187,7 +197,7 @@ export default {
[mutationTypes.ADD_ISSUE_TO_LIST_FAILURE]: (state, { list, issue }) => { [mutationTypes.ADD_ISSUE_TO_LIST_FAILURE]: (state, { list, issue }) => {
state.error = s__('Boards|An error occurred while creating the issue. Please try again.'); state.error = s__('Boards|An error occurred while creating the issue. Please try again.');
removeIssueFromList(state, list.id, issue.id); removeIssueFromList({ state, listId: list.id, issueId: issue.id });
}, },
[mutationTypes.SET_CURRENT_PAGE]: () => { [mutationTypes.SET_CURRENT_PAGE]: () => {

View file

@ -1,13 +1,15 @@
import $ from 'jquery'; import $ from 'jquery';
import Vue from 'vue'; import Vue from 'vue';
import collapseIcon from './icons/fullscreen_collapse.svg'; import { GlIcon } from '@gitlab/ui';
import expandIcon from './icons/fullscreen_expand.svg';
export default (ModalStore, boardsStore) => { export default (ModalStore, boardsStore) => {
const issueBoardsContent = document.querySelector('.content-wrapper > .js-focus-mode-board'); const issueBoardsContent = document.querySelector('.content-wrapper > .js-focus-mode-board');
return new Vue({ return new Vue({
el: document.getElementById('js-toggle-focus-btn'), el: document.getElementById('js-toggle-focus-btn'),
components: {
GlIcon,
},
data: { data: {
modal: ModalStore.store, modal: ModalStore.store,
store: boardsStore.state, store: boardsStore.state,
@ -32,12 +34,7 @@ export default (ModalStore, boardsStore) => {
title="Toggle focus mode" title="Toggle focus mode"
ref="toggleFocusModeButton" ref="toggleFocusModeButton"
@click="toggleFocusMode"> @click="toggleFocusMode">
<span v-show="isFullscreen"> <gl-icon :name="isFullscreen ? 'minimize' : 'maximize'" />
${collapseIcon}
</span>
<span v-show="!isFullscreen">
${expandIcon}
</span>
</a> </a>
</div> </div>
`, `,

View file

@ -71,7 +71,7 @@ export default {
</gl-sprintf> </gl-sprintf>
</p> </p>
<a <a
href="https://docs.gitlab.com/ee/ci/environments.html#stopping-an-environment" href="https://docs.gitlab.com/ee/ci/environments/#stopping-an-environment"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
>{{ s__('Environments|Learn more about stopping environments') }}</a >{{ s__('Environments|Learn more about stopping environments') }}</a

View file

@ -61,8 +61,8 @@ export const getFormatter = (format = SUPPORTED_FORMATS.engineering) => {
* @function * @function
* @param {Number} value - Number to format * @param {Number} value - Number to format
* @param {Number} fractionDigits - precision decimals * @param {Number} fractionDigits - precision decimals
* @param {Number} maxLength - Max lenght of formatted number * @param {Number} maxLength - Max length of formatted number
* if lenght is exceeded, exponential format is used. * if length is exceeded, exponential format is used.
*/ */
return numberFormatter(); return numberFormatter();
} }
@ -73,8 +73,8 @@ export const getFormatter = (format = SUPPORTED_FORMATS.engineering) => {
* @function * @function
* @param {Number} value - Number to format, `1` is rendered as `100%` * @param {Number} value - Number to format, `1` is rendered as `100%`
* @param {Number} fractionDigits - number of precision decimals * @param {Number} fractionDigits - number of precision decimals
* @param {Number} maxLength - Max lenght of formatted number * @param {Number} maxLength - Max length of formatted number
* if lenght is exceeded, exponential format is used. * if length is exceeded, exponential format is used.
*/ */
return numberFormatter('percent'); return numberFormatter('percent');
} }
@ -85,8 +85,8 @@ export const getFormatter = (format = SUPPORTED_FORMATS.engineering) => {
* @function * @function
* @param {Number} value - Number to format, `100` is rendered as `100%` * @param {Number} value - Number to format, `100` is rendered as `100%`
* @param {Number} fractionDigits - number of precision decimals * @param {Number} fractionDigits - number of precision decimals
* @param {Number} maxLength - Max lenght of formatted number * @param {Number} maxLength - Max length of formatted number
* if lenght is exceeded, exponential format is used. * if length is exceeded, exponential format is used.
*/ */
return numberFormatter('percent', 1 / 100); return numberFormatter('percent', 1 / 100);
} }
@ -100,8 +100,8 @@ export const getFormatter = (format = SUPPORTED_FORMATS.engineering) => {
* @function * @function
* @param {Number} value - Number to format, `1` is rendered as `1s` * @param {Number} value - Number to format, `1` is rendered as `1s`
* @param {Number} fractionDigits - number of precision decimals * @param {Number} fractionDigits - number of precision decimals
* @param {Number} maxLength - Max lenght of formatted number * @param {Number} maxLength - Max length of formatted number
* if lenght is exceeded, exponential format is used. * if length is exceeded, exponential format is used.
*/ */
return suffixFormatter(s__('Units|s')); return suffixFormatter(s__('Units|s'));
} }
@ -112,8 +112,8 @@ export const getFormatter = (format = SUPPORTED_FORMATS.engineering) => {
* @function * @function
* @param {Number} value - Number to format, `1` is formatted as `1ms` * @param {Number} value - Number to format, `1` is formatted as `1ms`
* @param {Number} fractionDigits - number of precision decimals * @param {Number} fractionDigits - number of precision decimals
* @param {Number} maxLength - Max lenght of formatted number * @param {Number} maxLength - Max length of formatted number
* if lenght is exceeded, exponential format is used. * if length is exceeded, exponential format is used.
*/ */
return suffixFormatter(s__('Units|ms')); return suffixFormatter(s__('Units|ms'));
} }

View file

@ -1,6 +1,6 @@
<script> <script>
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui'; import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import { isEqual } from 'lodash'; import { isEqual, get } from 'lodash';
import expirationPolicyQuery from '../graphql/queries/get_expiration_policy.graphql'; import expirationPolicyQuery from '../graphql/queries/get_expiration_policy.graphql';
import { FETCH_SETTINGS_ERROR_MESSAGE } from '../../shared/constants'; import { FETCH_SETTINGS_ERROR_MESSAGE } from '../../shared/constants';
@ -35,7 +35,7 @@ export default {
}, },
update: data => data.project?.containerExpirationPolicy, update: data => data.project?.containerExpirationPolicy,
result({ data }) { result({ data }) {
this.workingCopy = { ...data.project?.containerExpirationPolicy }; this.workingCopy = { ...get(data, 'project.containerExpirationPolicy', {}) };
}, },
error(e) { error(e) {
this.fetchSettingsError = e; this.fetchSettingsError = e;
@ -74,7 +74,7 @@ export default {
<template> <template>
<div> <div>
<settings-form <settings-form
v-if="containerExpirationPolicy" v-if="!isDisabled"
v-model="workingCopy" v-model="workingCopy"
:is-loading="$apollo.queries.containerExpirationPolicy.loading" :is-loading="$apollo.queries.containerExpirationPolicy.loading"
:is-edited="isEdited" :is-edited="isEdited"

View file

@ -55,6 +55,14 @@ export default {
}; };
}, },
computed: { computed: {
prefilledForm() {
return {
...this.value,
cadence: this.findDefaultOption('cadence'),
keepN: this.findDefaultOption('keepN'),
olderThan: this.findDefaultOption('olderThan'),
};
},
showLoadingIcon() { showLoadingIcon() {
return this.isLoading || this.mutationLoading; return this.isLoading || this.mutationLoading;
}, },
@ -77,6 +85,9 @@ export default {
}, },
}, },
methods: { methods: {
findDefaultOption(option) {
return this.value[option] || this.$options.formOptions[option].find(f => f.default)?.key;
},
reset() { reset() {
this.track('reset_form'); this.track('reset_form');
this.apiErrors = null; this.apiErrors = null;
@ -135,7 +146,7 @@ export default {
</template> </template>
<template #default> <template #default>
<expiration-policy-fields <expiration-policy-fields
:value="value" :value="prefilledForm"
:form-options="$options.formOptions" :form-options="$options.formOptions"
:is-loading="isLoading" :is-loading="isLoading"
:api-errors="apiErrors" :api-errors="apiErrors"

View file

@ -1,6 +1,7 @@
import Vue from 'vue'; import Vue from 'vue';
import { GlToast } from '@gitlab/ui'; import { GlToast } from '@gitlab/ui';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import { parseBoolean } from '~/lib/utils/common_utils';
import RegistrySettingsApp from './components/registry_settings_app.vue'; import RegistrySettingsApp from './components/registry_settings_app.vue';
import { apolloProvider } from './graphql/index'; import { apolloProvider } from './graphql/index';
@ -21,9 +22,9 @@ export default () => {
}, },
provide: { provide: {
projectPath, projectPath,
isAdmin, isAdmin: parseBoolean(isAdmin),
adminSettingsPath, adminSettingsPath,
enableHistoricEntries, enableHistoricEntries: parseBoolean(enableHistoricEntries),
}, },
render(createElement) { render(createElement) {
return createElement('registry-settings-app', {}); return createElement('registry-settings-app', {});

View file

@ -1,24 +1,34 @@
import initNotes from '~/init_notes';
import loadAwardsHandler from '~/awards_handler';
if (!gon.features.snippetsVue) { if (!gon.features.snippetsVue) {
const LineHighlighterModule = import('~/line_highlighter'); const LineHighlighterModule = import('~/line_highlighter');
const BlobViewerModule = import('~/blob/viewer'); const BlobViewerModule = import('~/blob/viewer');
const ZenModeModule = import('~/zen_mode'); const ZenModeModule = import('~/zen_mode');
const SnippetEmbedModule = import('~/snippet/snippet_embed'); const SnippetEmbedModule = import('~/snippet/snippet_embed');
const initNotesModule = import('~/init_notes');
const loadAwardsHandlerModule = import('~/awards_handler');
Promise.all([LineHighlighterModule, BlobViewerModule, ZenModeModule, SnippetEmbedModule]) Promise.all([
LineHighlighterModule,
BlobViewerModule,
ZenModeModule,
SnippetEmbedModule,
initNotesModule,
loadAwardsHandlerModule,
])
.then( .then(
([ ([
{ default: LineHighlighter }, { default: LineHighlighter },
{ default: BlobViewer }, { default: BlobViewer },
{ default: ZenMode }, { default: ZenMode },
{ default: SnippetEmbed }, { default: SnippetEmbed },
{ default: initNotes },
{ default: loadAwardsHandler },
]) => { ]) => {
new LineHighlighter(); // eslint-disable-line no-new new LineHighlighter(); // eslint-disable-line no-new
new BlobViewer(); // eslint-disable-line no-new new BlobViewer(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new
SnippetEmbed(); SnippetEmbed();
initNotes();
loadAwardsHandler();
}, },
) )
.catch(() => {}); .catch(() => {});
@ -27,7 +37,12 @@ if (!gon.features.snippetsVue) {
.then(({ SnippetShowInit }) => { .then(({ SnippetShowInit }) => {
SnippetShowInit(); SnippetShowInit();
}) })
.then(() => {
return Promise.all([import('~/init_notes'), import('~/awards_handler')]);
})
.then(([{ default: initNotes }, { default: loadAwardsHandler }]) => {
initNotes();
loadAwardsHandler();
})
.catch(() => {}); .catch(() => {});
} }
initNotes();
loadAwardsHandler();

View file

@ -143,7 +143,7 @@ export default function simulateDrag(options) {
const dragInterval = setInterval(() => { const dragInterval = setInterval(() => {
const progress = (new Date().getTime() - startTime) / duration; const progress = (new Date().getTime() - startTime) / duration;
const x = fromRect.cx + (toRect.cx - fromRect.cx) * progress; const x = fromRect.cx + (toRect.cx - fromRect.cx) * progress;
const y = fromRect.cy + (toRect.cy - fromRect.cy) * progress; const y = fromRect.cy + (toRect.cy - fromRect.cy + options.extraHeight) * progress;
const overEl = fromEl.ownerDocument.elementFromPoint(x, y); const overEl = fromEl.ownerDocument.elementFromPoint(x, y);
simulateEvent(overEl, 'pointermove', { simulateEvent(overEl, 'pointermove', {

View file

@ -26,7 +26,6 @@
@import './pages/incident_management_list'; @import './pages/incident_management_list';
@import './pages/issuable'; @import './pages/issuable';
@import './pages/issues/issue_count_badge'; @import './pages/issues/issue_count_badge';
@import './pages/issues/issues_list';
@import './pages/issues'; @import './pages/issues';
@import './pages/labels'; @import './pages/labels';
@import './pages/login'; @import './pages/login';

View file

@ -1,8 +0,0 @@
.user-can-drag {
cursor: grab;
}
.is-ghost {
opacity: 0.3;
pointer-events: none;
}

View file

@ -0,0 +1,61 @@
@import 'mixins_and_variables_and_functions';
.issues-list {
&.manual-ordering {
background-color: var(--gray-10, $gray-10);
border-radius: $border-radius-default;
padding: $gl-padding-8;
.issue {
background-color: var(--white, $white);
margin-bottom: $gl-padding-8;
border-radius: $border-radius-default;
border: 1px solid var(--border-color, $border-color);
box-shadow: 0 1px 2px $issue-boards-card-shadow;
}
}
.issue {
padding: 10px $gl-padding;
position: relative;
.title {
margin-bottom: 2px;
}
.issue-labels,
.author-link {
display: inline-block;
}
.icon-merge-request-unmerged {
height: 13px;
margin-bottom: 3px;
}
}
}
.issuable-list-root {
.gl-label-link {
text-decoration: none;
&:hover {
color: inherit;
}
}
}
.svg-container.jira-logo-container {
svg {
vertical-align: text-bottom;
}
}
.user-can-drag {
cursor: grab;
}
.is-ghost {
opacity: 0.3;
pointer-events: none;
}

View file

@ -1,38 +1,3 @@
.issues-list {
&.manual-ordering {
background-color: $gray-light;
border-radius: $border-radius-default;
padding: $gl-padding-8;
.issue {
background-color: $white;
margin-bottom: $gl-padding-8;
border-radius: $border-radius-default;
border: 1px solid $gray-100;
box-shadow: 0 1px 2px $issue-boards-card-shadow;
}
}
.issue {
padding: 10px $gl-padding;
position: relative;
.title {
margin-bottom: 2px;
}
.issue-labels,
.author-link {
display: inline-block;
}
.icon-merge-request-unmerged {
height: 13px;
margin-bottom: 3px;
}
}
}
.issue-realtime-pre-pulse { .issue-realtime-pre-pulse {
opacity: 0; opacity: 0;
} }
@ -369,13 +334,3 @@ ul.related-merge-requests > li {
.issuable-header-slide-leave-to { .issuable-header-slide-leave-to {
transform: translateY(-100%); transform: translateY(-100%);
} }
.issuable-list-root {
.gl-label-link {
text-decoration: none;
&:hover {
color: inherit;
}
}
}

View file

@ -1,5 +0,0 @@
.svg-container.jira-logo-container {
svg {
vertical-align: text-bottom;
}
}

View file

@ -6,7 +6,7 @@ require 'uri'
module ApplicationHelper module ApplicationHelper
include StartupCssHelper include StartupCssHelper
# See https://docs.gitlab.com/ee/development/ee_features.html#code-in-app-views # See https://docs.gitlab.com/ee/development/ee_features.html#code-in-appviews
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
# We allow partial to be nil so that collection views can be passed in # We allow partial to be nil so that collection views can be passed in
# `render partial: 'some/view', collection: @some_collection` # `render partial: 'some/view', collection: @some_collection`

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# The usage of the ReactiveCaching module is documented here: # The usage of the ReactiveCaching module is documented here:
# https://docs.gitlab.com/ee/development/utilities.html#reactivecaching # https://docs.gitlab.com/ee/development/reactive_caching.md
module ReactiveCaching module ReactiveCaching
extend ActiveSupport::Concern extend ActiveSupport::Concern

View file

@ -27,7 +27,7 @@ module Ci
rescue => e rescue => e
# Tracks this error with application logs, Sentry, and Prometheus. # Tracks this error with application logs, Sentry, and Prometheus.
# If `archive!` keeps failing for over a week, that could incur data loss. # If `archive!` keeps failing for over a week, that could incur data loss.
# (See more https://docs.gitlab.com/ee/administration/job_traces.html#new-live-trace-architecture) # (See more https://docs.gitlab.com/ee/administration/job_logs.html#new-incremental-logging-architecture)
# In order to avoid interrupting the system, we do not raise an exception here. # In order to avoid interrupting the system, we do not raise an exception here.
archive_error(e, job, worker_name) archive_error(e, job, worker_name)
end end

View file

@ -18,7 +18,7 @@ module Metrics
# Determines whether the provided params are sufficient # Determines whether the provided params are sufficient
# to uniquely identify a panel from a yml-defined dashboard. # to uniquely identify a panel from a yml-defined dashboard.
# #
# See https://docs.gitlab.com/ee/operations/metrics/dashboards/index.html#defining-custom-dashboards-per-project # See https://docs.gitlab.com/ee/operations/metrics/dashboards/index.html
# for additional info on defining custom dashboards. # for additional info on defining custom dashboards.
def valid_params?(params) def valid_params?(params)
[ [

View file

@ -1,6 +1,7 @@
- @can_bulk_update = can?(current_user, :admin_issue, @group) && @group.feature_available?(:group_bulk_edit) - @can_bulk_update = can?(current_user, :admin_issue, @group) && @group.feature_available?(:group_bulk_edit)
- page_title _("Issues") - page_title _("Issues")
- add_page_specific_style 'page_bundles/issues_list'
= content_for :meta_tags do = content_for :meta_tags do
= auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@group.name} issues") = auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@group.name} issues")

View file

@ -2,7 +2,7 @@
- page_title _("Issues") - page_title _("Issues")
- new_issue_email = @project.new_issuable_address(current_user, 'issue') - new_issue_email = @project.new_issuable_address(current_user, 'issue')
- add_page_specific_style 'page_bundles/issues' - add_page_specific_style 'page_bundles/issues_list'
= content_for :meta_tags do = content_for :meta_tags do
= auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@project.name} issues") = auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@project.name} issues")

View file

@ -1,7 +1,7 @@
- @can_bulk_update = false - @can_bulk_update = false
- page_title _("Service Desk") - page_title _("Service Desk")
- add_page_specific_style 'page_bundles/issues_list'
- content_for :breadcrumbs_extra do - content_for :breadcrumbs_extra do
= render "projects/issues/nav_btns", show_export_button: false, show_rss_button: false = render "projects/issues/nav_btns", show_export_button: false, show_rss_button: false

View file

@ -12,7 +12,6 @@
- can_report_spam = @issue.submittable_as_spam_by?(current_user) - can_report_spam = @issue.submittable_as_spam_by?(current_user)
- can_create_issue = show_new_issue_link?(@project) - can_create_issue = show_new_issue_link?(@project)
- related_branches_path = related_branches_project_issue_path(@project, @issue) - related_branches_path = related_branches_project_issue_path(@project, @issue)
- add_page_specific_style 'page_bundles/issues'
= render_if_exists "projects/issues/alert_blocked", issue: @issue, current_user: current_user = render_if_exists "projects/issues/alert_blocked", issue: @issue, current_user: current_user
= render "projects/issues/alert_moved_from_service_desk", issue: @issue = render "projects/issues/alert_moved_from_service_desk", issue: @issue

View file

@ -0,0 +1,5 @@
---
title: Update toggle focus mode icon to gl-icon
merge_request: 43888
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Fix the ability to assign labels based on license feature availability.
merge_request: 44171
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Update doc links in app
merge_request: 44134
author:
type: other

View file

@ -0,0 +1,5 @@
---
title: Fix GraphQL backward pagination when merge requests are ordered by merged_at
merge_request: 43701
author:
type: fixed

View file

@ -189,7 +189,7 @@ module Gitlab
config.assets.precompile << "page_bundles/boards.css" config.assets.precompile << "page_bundles/boards.css"
config.assets.precompile << "page_bundles/cycle_analytics.css" config.assets.precompile << "page_bundles/cycle_analytics.css"
config.assets.precompile << "page_bundles/ide.css" config.assets.precompile << "page_bundles/ide.css"
config.assets.precompile << "page_bundles/issues.css" config.assets.precompile << "page_bundles/issues_list.css"
config.assets.precompile << "page_bundles/jira_connect.css" config.assets.precompile << "page_bundles/jira_connect.css"
config.assets.precompile << "page_bundles/milestone.css" config.assets.precompile << "page_bundles/milestone.css"
config.assets.precompile << "page_bundles/todos.css" config.assets.precompile << "page_bundles/todos.css"

View file

@ -73,18 +73,3 @@ The **Merge Request Analytics** feature can be accessed only:
- On [Starter](https://about.gitlab.com/pricing/) and above. - On [Starter](https://about.gitlab.com/pricing/) and above.
- By users with [Reporter access](../permissions.md) and above. - By users with [Reporter access](../permissions.md) and above.
## Enable and disable related feature flags
Merge Request Analytics is disabled by default but can be enabled using the following
[feature flag](../../development/feature_flags/development.md#enabling-a-feature-flag-locally-in-development):
- `project_merge_request_analytics`
A GitLab administrator can:
- Enable this feature by running the following command in a Rails console:
```ruby
Feature.enable(:project_merge_request_analytics)
```

View file

@ -62,29 +62,3 @@ The **Productivity Analytics** dashboard can be accessed only:
- On [Premium or Silver tier](https://about.gitlab.com/pricing/) and above. - On [Premium or Silver tier](https://about.gitlab.com/pricing/) and above.
- By users with [Reporter access](../permissions.md) and above. - By users with [Reporter access](../permissions.md) and above.
## Enabling and disabling using feature flags
Productivity Analytics is:
- [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18754) from GitLab 12.4,
but can be disabled using the following feature flags:
- `productivity_analytics`.
- `productivity_analytics_scatterplot_enabled`.
- Disabled by default in GitLab 12.3, but can be enabled using the following feature flag:
- `productivity_analytics`.
A GitLab administrator can:
- Disable this feature from GitLab 12.4 by running the follow in a Rails console:
```ruby
Feature.disable(:productivity_analytics)
Feature.disable(:productivity_analytics_scatterplot_enabled)
```
- Enable this feature in GitLab 12.3 by running the following in a Rails console:
```ruby
Feature.enable(:productivity_analytics)
```

View file

@ -110,8 +110,7 @@ module Gitlab
end end
if last if last
# grab one more than we need paginated_nodes = LastItems.take_items(sliced_nodes, limit_value + 1)
paginated_nodes = sliced_nodes.last(limit_value + 1)
# there is an extra node, so there is a previous page # there is an extra node, so there is a previous page
@has_previous_page = paginated_nodes.count > limit_value @has_previous_page = paginated_nodes.count > limit_value

View file

@ -0,0 +1,57 @@
# frozen_string_literal: true
module Gitlab
module Graphql
module Pagination
module Keyset
# This class handles the last(N) ActiveRecord call even if a special ORDER BY configuration is present.
# For the last(N) call, ActiveRecord calls reverse_order, however for some cases it raises
# ActiveRecord::IrreversibleOrderError error.
class LastItems
# rubocop: disable CodeReuse/ActiveRecord
def self.take_items(scope, count)
if custom_order = lookup_custom_reverse_order(scope.order_values)
items = scope.reorder(*custom_order).first(count) # returns a single record when count is nil
items.is_a?(Array) ? items.reverse : items
else
scope.last(count)
end
end
# rubocop: enable CodeReuse/ActiveRecord
# Detect special ordering and provide the reversed order
def self.lookup_custom_reverse_order(order_values)
if ordering_by_merged_at_and_mr_id_desc?(order_values)
[
Gitlab::Database.nulls_first_order('merge_request_metrics.merged_at', 'ASC'), # reversing the order
MergeRequest.arel_table[:id].asc
]
elsif ordering_by_merged_at_and_mr_id_asc?(order_values)
[
Gitlab::Database.nulls_first_order('merge_request_metrics.merged_at', 'DESC'),
MergeRequest.arel_table[:id].asc
]
end
end
def self.ordering_by_merged_at_and_mr_id_desc?(order_values)
order_values.size == 2 &&
order_values.first.to_s == Gitlab::Database.nulls_last_order('merge_request_metrics.merged_at', 'DESC') &&
order_values.last.is_a?(Arel::Nodes::Descending) &&
order_values.last.to_sql == MergeRequest.arel_table[:id].desc.to_sql
end
def self.ordering_by_merged_at_and_mr_id_asc?(order_values)
order_values.size == 2 &&
order_values.first.to_s == Gitlab::Database.nulls_last_order('merge_request_metrics.merged_at', 'ASC') &&
order_values.last.is_a?(Arel::Nodes::Descending) &&
order_values.last.to_sql == MergeRequest.arel_table[:id].desc.to_sql
end
private_class_method :ordering_by_merged_at_and_mr_id_desc?
private_class_method :ordering_by_merged_at_and_mr_id_asc?
end
end
end
end
end

View file

@ -22,7 +22,7 @@ end
def rename_image(file, milestone) def rename_image(file, milestone)
path = File.dirname(file) path = File.dirname(file)
basename = File.basename(file) basename = File.basename(file, ".*")
final_name = File.join(path, "#{basename}_v#{milestone}.png") final_name = File.join(path, "#{basename}_v#{milestone}.png")
FileUtils.mv(file, final_name) FileUtils.mv(file, final_name)
end end

View file

@ -42,7 +42,7 @@ RSpec.describe LabelsFinder do
finder = described_class.new(user) finder = described_class.new(user)
expect(finder.execute).to eq [group_label_2, group_label_3, project_label_1, group_label_1, project_label_2, project_label_4] expect(finder.execute).to match_array([group_label_2, group_label_3, project_label_1, group_label_1, project_label_2, project_label_4])
end end
it 'returns labels available if nil title is supplied' do it 'returns labels available if nil title is supplied' do
@ -50,7 +50,7 @@ RSpec.describe LabelsFinder do
# params[:title] will return `nil` regardless whether it is specified # params[:title] will return `nil` regardless whether it is specified
finder = described_class.new(user, title: nil) finder = described_class.new(user, title: nil)
expect(finder.execute).to eq [group_label_2, group_label_3, project_label_1, group_label_1, project_label_2, project_label_4] expect(finder.execute).to match_array([group_label_2, group_label_3, project_label_1, group_label_1, project_label_2, project_label_4])
end end
end end
@ -60,7 +60,7 @@ RSpec.describe LabelsFinder do
::Projects::UpdateService.new(project_1, user, archived: true).execute ::Projects::UpdateService.new(project_1, user, archived: true).execute
finder = described_class.new(user, **group_params(group_1)) finder = described_class.new(user, **group_params(group_1))
expect(finder.execute).to eq [group_label_2, group_label_1, project_label_5] expect(finder.execute).to match_array([group_label_2, group_label_1, project_label_5])
end end
context 'when only_group_labels is true' do context 'when only_group_labels is true' do
@ -69,7 +69,7 @@ RSpec.describe LabelsFinder do
finder = described_class.new(user, only_group_labels: true, **group_params(group_1)) finder = described_class.new(user, only_group_labels: true, **group_params(group_1))
expect(finder.execute).to eq [group_label_2, group_label_1] expect(finder.execute).to match_array([group_label_2, group_label_1])
end end
end end
@ -86,7 +86,7 @@ RSpec.describe LabelsFinder do
it 'returns group labels' do it 'returns group labels' do
finder = described_class.new(user, **group_params(empty_group)) finder = described_class.new(user, **group_params(empty_group))
expect(finder.execute).to eq [empty_group_label_1, empty_group_label_2] expect(finder.execute).to match_array([empty_group_label_1, empty_group_label_2])
end end
end end
end end
@ -98,7 +98,7 @@ RSpec.describe LabelsFinder do
finder = described_class.new(user, **group_params(private_subgroup_1), only_group_labels: true, include_ancestor_groups: true) finder = described_class.new(user, **group_params(private_subgroup_1), only_group_labels: true, include_ancestor_groups: true)
expect(finder.execute).to eq [private_group_label_1, private_subgroup_label_1] expect(finder.execute).to match_array([private_group_label_1, private_subgroup_label_1])
end end
it 'ignores labels from groups which user can not read' do it 'ignores labels from groups which user can not read' do
@ -106,7 +106,7 @@ RSpec.describe LabelsFinder do
finder = described_class.new(user, **group_params(private_subgroup_1), only_group_labels: true, include_ancestor_groups: true) finder = described_class.new(user, **group_params(private_subgroup_1), only_group_labels: true, include_ancestor_groups: true)
expect(finder.execute).to eq [private_subgroup_label_1] expect(finder.execute).to match_array([private_subgroup_label_1])
end end
end end
@ -117,7 +117,7 @@ RSpec.describe LabelsFinder do
finder = described_class.new(user, **group_params(private_group_1), only_group_labels: true, include_descendant_groups: true) finder = described_class.new(user, **group_params(private_group_1), only_group_labels: true, include_descendant_groups: true)
expect(finder.execute).to eq [private_group_label_1, private_subgroup_label_1] expect(finder.execute).to match_array([private_group_label_1, private_subgroup_label_1])
end end
it 'ignores labels from groups which user can not read' do it 'ignores labels from groups which user can not read' do
@ -125,7 +125,7 @@ RSpec.describe LabelsFinder do
finder = described_class.new(user, **group_params(private_group_1), only_group_labels: true, include_descendant_groups: true) finder = described_class.new(user, **group_params(private_group_1), only_group_labels: true, include_descendant_groups: true)
expect(finder.execute).to eq [private_subgroup_label_1] expect(finder.execute).to match_array([private_subgroup_label_1])
end end
end end
@ -140,13 +140,13 @@ RSpec.describe LabelsFinder do
shared_examples 'with full visibility' do shared_examples 'with full visibility' do
it 'returns all projects labels' do it 'returns all projects labels' do
expect(finder.execute).to eq [group_label_1, limited_visibility_label, visible_label] expect(finder.execute).to match_array([group_label_1, limited_visibility_label, visible_label])
end end
end end
shared_examples 'with limited visibility' do shared_examples 'with limited visibility' do
it 'returns only authorized projects labels' do it 'returns only authorized projects labels' do
expect(finder.execute).to eq [group_label_1, visible_label] expect(finder.execute).to match_array([group_label_1, visible_label])
end end
end end
@ -249,7 +249,7 @@ RSpec.describe LabelsFinder do
it 'returns labels available for the project' do it 'returns labels available for the project' do
finder = described_class.new(user, project_id: project_1.id) finder = described_class.new(user, project_id: project_1.id)
expect(finder.execute).to eq [group_label_2, project_label_1, group_label_1] expect(finder.execute).to match_array([group_label_2, project_label_1, group_label_1])
end end
context 'as an administrator' do context 'as an administrator' do
@ -272,13 +272,13 @@ RSpec.describe LabelsFinder do
it 'returns label with that title' do it 'returns label with that title' do
finder = described_class.new(user, title: 'Group Label 2') finder = described_class.new(user, title: 'Group Label 2')
expect(finder.execute).to eq [group_label_2] expect(finder.execute).to match_array([group_label_2])
end end
it 'returns label with title alias' do it 'returns label with title alias' do
finder = described_class.new(user, name: 'Group Label 2') finder = described_class.new(user, name: 'Group Label 2')
expect(finder.execute).to eq [group_label_2] expect(finder.execute).to match_array([group_label_2])
end end
it 'returns no labels if empty title is supplied' do it 'returns no labels if empty title is supplied' do
@ -304,19 +304,19 @@ RSpec.describe LabelsFinder do
it 'returns labels with a partially matching title' do it 'returns labels with a partially matching title' do
finder = described_class.new(user, search: '(group)') finder = described_class.new(user, search: '(group)')
expect(finder.execute).to eq [group_label_1] expect(finder.execute).to match_array([group_label_1])
end end
it 'returns labels with a partially matching description' do it 'returns labels with a partially matching description' do
finder = described_class.new(user, search: 'awesome') finder = described_class.new(user, search: 'awesome')
expect(finder.execute).to eq [project_label_1] expect(finder.execute).to match_array([project_label_1])
end end
it 'returns labels matching a single character' do it 'returns labels matching a single character' do
finder = described_class.new(user, search: '(') finder = described_class.new(user, search: '(')
expect(finder.execute).to eq [group_label_1] expect(finder.execute).to match_array([group_label_1])
end end
end end
@ -326,7 +326,7 @@ RSpec.describe LabelsFinder do
finder = described_class.new(user, subscribed: 'true') finder = described_class.new(user, subscribed: 'true')
expect(finder.execute).to eq [project_label_1] expect(finder.execute).to match_array([project_label_1])
end end
end end

View file

@ -322,6 +322,7 @@ describe('Board Store Mutations', () => {
state = { state = {
...state, ...state,
issuesByListId: listIssues, issuesByListId: listIssues,
boardLists: mockListsWithModel,
}; };
mutations.MOVE_ISSUE_FAILURE(state, { mutations.MOVE_ISSUE_FAILURE(state, {
@ -389,6 +390,7 @@ describe('Board Store Mutations', () => {
...state, ...state,
issuesByListId: listIssues, issuesByListId: listIssues,
issues, issues,
boardLists: mockListsWithModel,
}; };
mutations.ADD_ISSUE_TO_LIST_FAILURE(state, { list: mockLists[0], issue: mockIssue2 }); mutations.ADD_ISSUE_TO_LIST_FAILURE(state, { list: mockLists[0], issue: mockIssue2 });

View file

@ -11,7 +11,7 @@ import {
UNAVAILABLE_USER_FEATURE_TEXT, UNAVAILABLE_USER_FEATURE_TEXT,
} from '~/registry/settings/constants'; } from '~/registry/settings/constants';
import { expirationPolicyPayload } from '../mock_data'; import { expirationPolicyPayload, emptyExpirationPolicyPayload } from '../mock_data';
const localVue = createLocalVue(); const localVue = createLocalVue();
@ -115,4 +115,23 @@ describe('Registry Settings App', () => {
expect(findAlert().html()).toContain(FETCH_SETTINGS_ERROR_MESSAGE); expect(findAlert().html()).toContain(FETCH_SETTINGS_ERROR_MESSAGE);
}); });
}); });
describe('empty API response', () => {
it.each`
enableHistoricEntries | isShown
${true} | ${true}
${false} | ${false}
`('is $isShown that the form is shown', async ({ enableHistoricEntries, isShown }) => {
const requests = mountComponentWithApollo({
provide: {
...defaultProvidedValues,
enableHistoricEntries,
},
resolver: jest.fn().mockResolvedValue(emptyExpirationPolicyPayload()),
});
await Promise.all(requests);
expect(findSettingsComponent().exists()).toBe(isShown);
});
});
}); });

View file

@ -123,6 +123,15 @@ describe('Settings Form', () => {
findFields().vm.$emit('input', { newValue: 'foo', modified: 'baz' }); findFields().vm.$emit('input', { newValue: 'foo', modified: 'baz' });
expect(findFields().props('apiErrors')).toEqual({}); expect(findFields().props('apiErrors')).toEqual({});
}); });
it('shows the default option when none are selected', () => {
mountComponent({ props: { value: {} } });
expect(findFields().props('value')).toEqual({
cadence: 'EVERY_DAY',
keepN: 'TEN_TAGS',
olderThan: 'NINETY_DAYS',
});
});
}); });
describe('form', () => { describe('form', () => {

View file

@ -14,6 +14,14 @@ export const expirationPolicyPayload = override => ({
}, },
}); });
export const emptyExpirationPolicyPayload = () => ({
data: {
project: {
containerExpirationPolicy: {},
},
},
});
export const expirationPolicyMutationPayload = ({ override, errors = [] } = {}) => ({ export const expirationPolicyMutationPayload = ({ override, errors = [] } = {}) => ({
data: { data: {
updateContainerExpirationPolicy: { updateContainerExpirationPolicy: {

View file

@ -5,19 +5,19 @@ require "spec_helper"
RSpec.describe NotesHelper do RSpec.describe NotesHelper do
include RepoHelpers include RepoHelpers
let(:owner) { create(:owner) } let_it_be(:owner) { create(:owner) }
let(:group) { create(:group) } let_it_be(:group) { create(:group) }
let(:project) { create(:project, namespace: group) } let_it_be(:project) { create(:project, namespace: group) }
let(:maintainer) { create(:user) } let_it_be(:maintainer) { create(:user) }
let(:reporter) { create(:user) } let_it_be(:reporter) { create(:user) }
let(:guest) { create(:user) } let_it_be(:guest) { create(:user) }
let(:owner_note) { create(:note, author: owner, project: project) } let_it_be(:owner_note) { create(:note, author: owner, project: project) }
let(:maintainer_note) { create(:note, author: maintainer, project: project) } let_it_be(:maintainer_note) { create(:note, author: maintainer, project: project) }
let(:reporter_note) { create(:note, author: reporter, project: project) } let_it_be(:reporter_note) { create(:note, author: reporter, project: project) }
let!(:notes) { [owner_note, maintainer_note, reporter_note] } let!(:notes) { [owner_note, maintainer_note, reporter_note] }
before do before_all do
group.add_owner(owner) group.add_owner(owner)
project.add_maintainer(maintainer) project.add_maintainer(maintainer)
project.add_reporter(reporter) project.add_reporter(reporter)
@ -72,14 +72,14 @@ RSpec.describe NotesHelper do
end end
describe '#discussion_path' do describe '#discussion_path' do
let(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
let(:anchor) { discussion.line_code } let(:anchor) { discussion.line_code }
context 'for a merge request discusion' do context 'for a merge request discusion' do
let(:merge_request) { create(:merge_request, source_project: project, target_project: project, importing: true) } let_it_be(:merge_request) { create(:merge_request, source_project: project, target_project: project, importing: true) }
let!(:merge_request_diff1) { merge_request.merge_request_diffs.create!(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') } let_it_be(:merge_request_diff1) { merge_request.merge_request_diffs.create!(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
let!(:merge_request_diff2) { merge_request.merge_request_diffs.create!(head_commit_sha: nil) } let_it_be(:merge_request_diff2) { merge_request.merge_request_diffs.create!(head_commit_sha: nil) }
let!(:merge_request_diff3) { merge_request.merge_request_diffs.create!(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') } let_it_be(:merge_request_diff3) { merge_request.merge_request_diffs.create!(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
context 'for a diff discussion' do context 'for a diff discussion' do
context 'when the discussion is active' do context 'when the discussion is active' do
@ -229,20 +229,18 @@ RSpec.describe NotesHelper do
end end
it 'return project notes path for project snippet' do it 'return project notes path for project snippet' do
namespace = create(:namespace, path: 'nm') @project = project
@project = create(:project, path: 'test', namespace: namespace)
@snippet = create(:project_snippet, project: @project) @snippet = create(:project_snippet, project: @project)
@noteable = @snippet @noteable = @snippet
expect(helper.notes_url).to eq("/nm/test/noteable/project_snippet/#{@noteable.id}/notes") expect(helper.notes_url).to eq("/#{project.full_path}/noteable/project_snippet/#{@noteable.id}/notes")
end end
it 'return project notes path for other noteables' do it 'return project notes path for other noteables' do
namespace = create(:namespace, path: 'nm') @project = project
@project = create(:project, path: 'test', namespace: namespace)
@noteable = create(:issue, project: @project) @noteable = create(:issue, project: @project)
expect(helper.notes_url).to eq("/nm/test/noteable/issue/#{@noteable.id}/notes") expect(helper.notes_url).to eq("/#{@project.full_path}/noteable/issue/#{@noteable.id}/notes")
end end
end end
@ -254,19 +252,17 @@ RSpec.describe NotesHelper do
end end
it 'return project notes path for project snippet' do it 'return project notes path for project snippet' do
namespace = create(:namespace, path: 'nm') @project = project
@project = create(:project, path: 'test', namespace: namespace)
note = create(:note_on_project_snippet, project: @project) note = create(:note_on_project_snippet, project: @project)
expect(helper.note_url(note)).to eq("/nm/test/notes/#{note.id}") expect(helper.note_url(note)).to eq("/#{project.full_path}/notes/#{note.id}")
end end
it 'return project notes path for other noteables' do it 'return project notes path for other noteables' do
namespace = create(:namespace, path: 'nm') @project = project
@project = create(:project, path: 'test', namespace: namespace)
note = create(:note_on_issue, project: @project) note = create(:note_on_issue, project: @project)
expect(helper.note_url(note)).to eq("/nm/test/notes/#{note.id}") expect(helper.note_url(note)).to eq("/#{project.full_path}/notes/#{note.id}")
end end
end end
@ -279,8 +275,7 @@ RSpec.describe NotesHelper do
end end
it 'returns namespace, project and note for project snippet' do it 'returns namespace, project and note for project snippet' do
namespace = create(:namespace, path: 'nm') @project = project
@project = create(:project, path: 'test', namespace: namespace)
@snippet = create(:project_snippet, project: @project) @snippet = create(:project_snippet, project: @project)
@note = create(:note_on_personal_snippet) @note = create(:note_on_personal_snippet)
@ -288,8 +283,7 @@ RSpec.describe NotesHelper do
end end
it 'returns namespace, project and note path for other noteables' do it 'returns namespace, project and note path for other noteables' do
namespace = create(:namespace, path: 'nm') @project = project
@project = create(:project, path: 'test', namespace: namespace)
@note = create(:note_on_issue, project: @project) @note = create(:note_on_issue, project: @project)
expect(helper.form_resources).to eq([@project, @note]) expect(helper.form_resources).to eq([@project, @note])
@ -297,7 +291,6 @@ RSpec.describe NotesHelper do
end end
describe '#noteable_note_url' do describe '#noteable_note_url' do
let(:project) { create(:project) }
let(:issue) { create(:issue, project: project) } let(:issue) { create(:issue, project: project) }
let(:note) { create(:note_on_issue, noteable: issue, project: project) } let(:note) { create(:note_on_issue, noteable: issue, project: project) }

View file

@ -0,0 +1,26 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Graphql::Pagination::Keyset::LastItems do
let_it_be(:merge_request) { create(:merge_request) }
let(:scope) { MergeRequest.order_merged_at_asc.with_order_id_desc }
subject { described_class.take_items(*args) }
context 'when the `count` parameter is nil' do
let(:args) { [scope, nil] }
it 'returns a single record' do
expect(subject).to eq(merge_request)
end
end
context 'when the `count` parameter is given' do
let(:args) { [scope, 1] }
it 'returns an array' do
expect(subject).to eq([merge_request])
end
end
end

View file

@ -13,6 +13,7 @@ RSpec.describe 'getting merge request listings nested in a project' do
let_it_be(:merge_request_b) { create(:merge_request, :closed, :unique_branches, source_project: project) } let_it_be(:merge_request_b) { create(:merge_request, :closed, :unique_branches, source_project: project) }
let_it_be(:merge_request_c) { create(:labeled_merge_request, :closed, :unique_branches, source_project: project, labels: [label]) } let_it_be(:merge_request_c) { create(:labeled_merge_request, :closed, :unique_branches, source_project: project, labels: [label]) }
let_it_be(:merge_request_d) { create(:merge_request, :locked, :unique_branches, source_project: project) } let_it_be(:merge_request_d) { create(:merge_request, :locked, :unique_branches, source_project: project) }
let_it_be(:merge_request_e) { create(:merge_request, :unique_branches, source_project: project) }
let(:results) { graphql_data.dig('project', 'mergeRequests', 'nodes') } let(:results) { graphql_data.dig('project', 'mergeRequests', 'nodes') }
@ -118,7 +119,7 @@ RSpec.describe 'getting merge request listings nested in a project' do
context 'there are no search params' do context 'there are no search params' do
let(:search_params) { nil } let(:search_params) { nil }
let(:mrs) { [merge_request_a, merge_request_b, merge_request_c, merge_request_d] } let(:mrs) { [merge_request_a, merge_request_b, merge_request_c, merge_request_d, merge_request_e] }
it_behaves_like 'searching with parameters' it_behaves_like 'searching with parameters'
end end
@ -241,16 +242,50 @@ RSpec.describe 'getting merge request listings nested in a project' do
let(:expected_results) do let(:expected_results) do
[ [
merge_request_b, merge_request_b,
merge_request_c,
merge_request_d, merge_request_d,
merge_request_c,
merge_request_e,
merge_request_a merge_request_a
].map(&:to_gid).map(&:to_s) ].map(&:to_gid).map(&:to_s)
end end
before do before do
merge_request_c.metrics.update!(merged_at: 5.days.ago) five_days_ago = 5.days.ago
merge_request_d.metrics.update!(merged_at: five_days_ago)
# same merged_at, the second order column will decide (merge_request.id)
merge_request_c.metrics.update!(merged_at: five_days_ago)
merge_request_b.metrics.update!(merged_at: 1.day.ago) merge_request_b.metrics.update!(merged_at: 1.day.ago)
end end
context 'when paginating backwards' do
let(:params) { 'first: 2, sort: MERGED_AT_DESC' }
let(:page_info) { 'pageInfo { startCursor endCursor }' }
before do
post_graphql(pagination_query(params, page_info), current_user: current_user)
end
it 'paginates backwards correctly' do
# first page
first_page_response_data = graphql_dig_at(Gitlab::Json.parse(response.body), :data, *data_path, :edges)
end_cursor = graphql_dig_at(Gitlab::Json.parse(response.body), :data, :project, :mergeRequests, :pageInfo, :endCursor)
# second page
params = "first: 2, after: \"#{end_cursor}\", sort: MERGED_AT_DESC"
post_graphql(pagination_query(params, page_info), current_user: current_user)
start_cursor = graphql_dig_at(Gitlab::Json.parse(response.body), :data, :project, :mergeRequests, :pageInfo, :start_cursor)
# going back to the first page
params = "last: 2, before: \"#{start_cursor}\", sort: MERGED_AT_DESC"
post_graphql(pagination_query(params, page_info), current_user: current_user)
backward_paginated_response_data = graphql_dig_at(Gitlab::Json.parse(response.body), :data, *data_path, :edges)
expect(first_page_response_data).to eq(backward_paginated_response_data)
end
end
end end
end end
end end

View file

@ -3,25 +3,32 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Ci::RetryBuildService do RSpec.describe Ci::RetryBuildService do
let_it_be(:user) { create(:user) } let_it_be(:reporter) { create(:user) }
let_it_be(:developer) { create(:user) }
let_it_be(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
let_it_be(:pipeline) do let_it_be(:pipeline) do
create(:ci_pipeline, project: project, create(:ci_pipeline, project: project,
sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0') sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0')
end end
let(:stage) do let_it_be(:stage) do
create(:ci_stage_entity, project: project, create(:ci_stage_entity, project: project,
pipeline: pipeline, pipeline: pipeline,
name: 'test') name: 'test')
end end
let(:build) { create(:ci_build, pipeline: pipeline, stage_id: stage.id) } let_it_be_with_refind(:build) { create(:ci_build, pipeline: pipeline, stage_id: stage.id) }
let(:user) { developer }
let(:service) do let(:service) do
described_class.new(project, user) described_class.new(project, user)
end end
before_all do
project.add_developer(developer)
project.add_reporter(reporter)
end
clone_accessors = described_class.clone_accessors clone_accessors = described_class.clone_accessors
reject_accessors = reject_accessors =
@ -53,9 +60,9 @@ RSpec.describe Ci::RetryBuildService do
pipeline_id report_results pending_state pages_deployments].freeze pipeline_id report_results pending_state pages_deployments].freeze
shared_examples 'build duplication' do shared_examples 'build duplication' do
let(:another_pipeline) { create(:ci_empty_pipeline, project: project) } let_it_be(:another_pipeline) { create(:ci_empty_pipeline, project: project) }
let(:build) do let_it_be(:build) do
create(:ci_build, :failed, :expired, :erased, :queued, :coverage, :tags, create(:ci_build, :failed, :expired, :erased, :queued, :coverage, :tags,
:allowed_to_fail, :on_tag, :triggered, :teardown_environment, :resource_group, :allowed_to_fail, :on_tag, :triggered, :teardown_environment, :resource_group,
description: 'my-job', stage: 'test', stage_id: stage.id, description: 'my-job', stage: 'test', stage_id: stage.id,
@ -63,7 +70,7 @@ RSpec.describe Ci::RetryBuildService do
scheduled_at: 10.seconds.since) scheduled_at: 10.seconds.since)
end end
before do before_all do
# Test correctly behaviour of deprecated artifact because it can be still in use # Test correctly behaviour of deprecated artifact because it can be still in use
stub_feature_flags(drop_license_management_artifact: false) stub_feature_flags(drop_license_management_artifact: false)
@ -81,8 +88,6 @@ RSpec.describe Ci::RetryBuildService do
create(:ci_job_variable, job: build) create(:ci_job_variable, job: build)
create(:ci_build_need, build: build) create(:ci_build_need, build: build)
build.reload
end end
describe 'clone accessors' do describe 'clone accessors' do
@ -162,8 +167,6 @@ RSpec.describe Ci::RetryBuildService do
context 'when user has ability to execute build' do context 'when user has ability to execute build' do
before do before do
stub_not_protect_default_branch stub_not_protect_default_branch
project.add_developer(user)
end end
it_behaves_like 'build duplication' it_behaves_like 'build duplication'
@ -235,7 +238,6 @@ RSpec.describe Ci::RetryBuildService do
context 'when the pipeline is a child pipeline and the bridge is depended' do context 'when the pipeline is a child pipeline and the bridge is depended' do
let!(:parent_pipeline) { create(:ci_pipeline, project: project) } let!(:parent_pipeline) { create(:ci_pipeline, project: project) }
let!(:pipeline) { create(:ci_pipeline, project: project) }
let!(:bridge) { create(:ci_bridge, :strategy_depend, pipeline: parent_pipeline, status: 'success') } let!(:bridge) { create(:ci_bridge, :strategy_depend, pipeline: parent_pipeline, status: 'success') }
let!(:source_pipeline) { create(:ci_sources_pipeline, pipeline: pipeline, source_job: bridge) } let!(:source_pipeline) { create(:ci_sources_pipeline, pipeline: pipeline, source_job: bridge) }
@ -248,6 +250,8 @@ RSpec.describe Ci::RetryBuildService do
end end
context 'when user does not have ability to execute build' do context 'when user does not have ability to execute build' do
let(:user) { reporter }
it 'raises an error' do it 'raises an error' do
expect { service.execute(build) } expect { service.execute(build) }
.to raise_error Gitlab::Access::AccessDeniedError .to raise_error Gitlab::Access::AccessDeniedError
@ -265,8 +269,6 @@ RSpec.describe Ci::RetryBuildService do
context 'when user has ability to execute build' do context 'when user has ability to execute build' do
before do before do
stub_not_protect_default_branch stub_not_protect_default_branch
project.add_developer(user)
end end
it_behaves_like 'build duplication' it_behaves_like 'build duplication'
@ -316,6 +318,8 @@ RSpec.describe Ci::RetryBuildService do
end end
context 'when user does not have ability to execute build' do context 'when user does not have ability to execute build' do
let(:user) { reporter }
it 'raises an error' do it 'raises an error' do
expect { service.reprocess!(build) } expect { service.reprocess!(build) }
.to raise_error Gitlab::Access::AccessDeniedError .to raise_error Gitlab::Access::AccessDeniedError

View file

@ -138,7 +138,7 @@ RSpec.describe Projects::AutocompleteService do
def expect_labels_to_equal(labels, expected_labels) def expect_labels_to_equal(labels, expected_labels)
expect(labels.size).to eq(expected_labels.size) expect(labels.size).to eq(expected_labels.size)
extract_title = lambda { |label| label['title'] } extract_title = lambda { |label| label['title'] }
expect(labels.map(&extract_title)).to eq(expected_labels.map(&extract_title)) expect(labels.map(&extract_title)).to match_array(expected_labels.map(&extract_title))
end end
let(:user) { create(:user) } let(:user) { create(:user) }

View file

@ -1,7 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
module DragTo module DragTo
def drag_to(list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0, selector: '', scrollable: 'body', duration: 1000, perform_drop: true) # rubocop:disable Metrics/ParameterLists
def drag_to(list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0, selector: '', scrollable: 'body', duration: 1000, perform_drop: true, extra_height: 0)
js = <<~JS js = <<~JS
simulateDrag({ simulateDrag({
scrollable: document.querySelector('#{scrollable}'), scrollable: document.querySelector('#{scrollable}'),
@ -14,7 +15,8 @@ module DragTo
el: document.querySelectorAll('#{selector}')[#{list_to_index}], el: document.querySelectorAll('#{selector}')[#{list_to_index}],
index: #{to_index} index: #{to_index}
}, },
performDrop: #{perform_drop} performDrop: #{perform_drop},
extraHeight: #{extra_height}
}); });
JS JS
evaluate_script(js) evaluate_script(js)
@ -23,6 +25,7 @@ module DragTo
loop while drag_active? loop while drag_active?
end end
end end
# rubocop:enable Metrics/ParameterLists
def drag_active? def drag_active?
page.evaluate_script('window.SIMULATE_DRAG_ACTIVE').nonzero? page.evaluate_script('window.SIMULATE_DRAG_ACTIVE').nonzero?

View file

@ -29,8 +29,8 @@ module StubbedFeature
end end
# Replace #enabled? method with the optional stubbed/unstubbed version. # Replace #enabled? method with the optional stubbed/unstubbed version.
def enabled?(*args) def enabled?(*args, **kwargs)
feature_flag = super(*args) feature_flag = super
return feature_flag unless stub? return feature_flag unless stub?
# If feature flag is not persisted we mark the feature flag as enabled # If feature flag is not persisted we mark the feature flag as enabled