Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-12-04 09:09:36 +00:00
parent a2f16969fa
commit f9fe7cda4b
73 changed files with 800 additions and 355 deletions

View File

@ -1 +1 @@
add5f3dd182c99b4d9e1cf93e45fec1214c00659
9fd57cbd0b63d448f9a9555b53f065ee1c110199

View File

@ -14,7 +14,7 @@ import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue';
import getIssueParticipants from '~/vue_shared/components/sidebar/queries/getIssueParticipants.query.graphql';
import searchUsers from '~/boards/queries/users_search.query.graphql';
import searchUsers from '~/boards/graphql/users_search.query.graphql';
export default {
noSearchDelay: 0,

View File

@ -12,8 +12,8 @@ import {
import httpStatusCodes from '~/lib/utils/http_status';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import projectQuery from '../queries/project_boards.query.graphql';
import groupQuery from '../queries/group_boards.query.graphql';
import projectQuery from '../graphql/project_boards.query.graphql';
import groupQuery from '../graphql/group_boards.query.graphql';
import boardsStore from '../stores/boards_store';
import BoardForm from './board_form.vue';

View File

@ -10,7 +10,7 @@ import {
} from '@gitlab/ui';
import { fetchPolicies } from '~/lib/graphql';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import groupMilestones from '../../queries/group_milestones.query.graphql';
import groupMilestones from '../../graphql/group_milestones.query.graphql';
import createFlash from '~/flash';
import { __, s__ } from '~/locale';

View File

@ -1,4 +1,4 @@
#import "ee_else_ce/boards/queries/board_list.fragment.graphql"
#import "ee_else_ce/boards/graphql/board_list.fragment.graphql"
mutation CreateBoardList(
$boardId: BoardID!

View File

@ -1,4 +1,4 @@
#import "ee_else_ce/boards/queries/board_list.fragment.graphql"
#import "ee_else_ce/boards/graphql/board_list.fragment.graphql"
query ListIssues(
$fullPath: ID!

View File

@ -1,4 +1,4 @@
#import "ee_else_ce/boards/queries/board.fragment.graphql"
#import "ee_else_ce/boards/graphql/board.fragment.graphql"
query group_boards($fullPath: ID!) {
group(fullPath: $fullPath) {

View File

@ -1,4 +1,4 @@
#import "ee_else_ce/boards/queries/issue.fragment.graphql"
#import "ee_else_ce/boards/graphql/issue.fragment.graphql"
mutation CreateIssue($input: CreateIssueInput!) {
createIssue(input: $input) {

View File

@ -1,4 +1,4 @@
#import "ee_else_ce/boards/queries/issue.fragment.graphql"
#import "ee_else_ce/boards/graphql/issue.fragment.graphql"
mutation IssueMoveList(
$projectPath: ID!

View File

@ -1,4 +1,4 @@
#import "ee_else_ce/boards/queries/issue.fragment.graphql"
#import "ee_else_ce/boards/graphql/issue.fragment.graphql"
query ListIssues(
$fullPath: ID!

View File

@ -1,4 +1,4 @@
#import "ee_else_ce/boards/queries/board.fragment.graphql"
#import "ee_else_ce/boards/graphql/board.fragment.graphql"
query project_boards($fullPath: ID!) {
project(fullPath: $fullPath) {

View File

@ -1,6 +1,6 @@
import { pick } from 'lodash';
import boardListsQuery from 'ee_else_ce/boards/queries/board_lists.query.graphql';
import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { BoardType, ListType, inactiveId, DEFAULT_LABELS } from '~/boards/constants';
@ -14,18 +14,18 @@ import {
} from '../boards_util';
import boardStore from '~/boards/stores/boards_store';
import updateAssignees from '~/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql';
import listsIssuesQuery from '../queries/lists_issues.query.graphql';
import boardLabelsQuery from '../queries/board_labels.query.graphql';
import createBoardListMutation from '../queries/board_list_create.mutation.graphql';
import updateBoardListMutation from '../queries/board_list_update.mutation.graphql';
import issueMoveListMutation from '../queries/issue_move_list.mutation.graphql';
import destroyBoardListMutation from '../queries/board_list_destroy.mutation.graphql';
import issueCreateMutation from '../queries/issue_create.mutation.graphql';
import issueSetLabels from '../queries/issue_set_labels.mutation.graphql';
import issueSetDueDate from '../queries/issue_set_due_date.mutation.graphql';
import issueSetSubscriptionMutation from '../graphql/mutations/issue_set_subscription.mutation.graphql';
import issueSetMilestone from '../queries/issue_set_milestone.mutation.graphql';
import updateAssigneesMutation from '~/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql';
import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
import boardLabelsQuery from '../graphql/board_labels.query.graphql';
import createBoardListMutation from '../graphql/board_list_create.mutation.graphql';
import updateBoardListMutation from '../graphql/board_list_update.mutation.graphql';
import issueMoveListMutation from '../graphql/issue_move_list.mutation.graphql';
import destroyBoardListMutation from '../graphql/board_list_destroy.mutation.graphql';
import issueCreateMutation from '../graphql/issue_create.mutation.graphql';
import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql';
import issueSetDueDateMutation from '../graphql/issue_set_due_date.mutation.graphql';
import issueSetSubscriptionMutation from '../graphql/issue_set_subscription.mutation.graphql';
import issueSetMilestoneMutation from '../graphql/issue_set_milestone.mutation.graphql';
const notImplemented = () => {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
@ -324,7 +324,7 @@ export default {
return gqlClient
.mutate({
mutation: updateAssignees,
mutation: updateAssigneesMutation,
variables: {
iid: getters.activeIssue.iid,
projectPath: getters.activeIssue.referencePath.split('#')[0],
@ -350,7 +350,7 @@ export default {
setActiveIssueMilestone: async ({ commit, getters }, input) => {
const { activeIssue } = getters;
const { data } = await gqlClient.mutate({
mutation: issueSetMilestone,
mutation: issueSetMilestoneMutation,
variables: {
input: {
iid: String(activeIssue.iid),
@ -416,7 +416,7 @@ export default {
setActiveIssueLabels: async ({ commit, getters }, input) => {
const { activeIssue } = getters;
const { data } = await gqlClient.mutate({
mutation: issueSetLabels,
mutation: issueSetLabelsMutation,
variables: {
input: {
iid: String(activeIssue.iid),
@ -441,7 +441,7 @@ export default {
setActiveIssueDueDate: async ({ commit, getters }, input) => {
const { activeIssue } = getters;
const { data } = await gqlClient.mutate({
mutation: issueSetDueDate,
mutation: issueSetDueDateMutation,
variables: {
input: {
iid: String(activeIssue.iid),

View File

@ -22,7 +22,7 @@ import ListLabel from '../models/label';
import ListAssignee from '../models/assignee';
import ListMilestone from '../models/milestone';
import createBoardMutation from '../queries/board.mutation.graphql';
import createBoardMutation from '../graphql/board.mutation.graphql';
const PER_PAGE = 20;
export const gqlClient = createDefaultClient();

View File

@ -2,7 +2,6 @@ import { find } from 'lodash';
import { inactiveId } from '../constants';
export default {
labelToggleState: state => (state.isShowingLabels ? 'on' : 'off'),
isSidebarOpen: state => state.activeId !== inactiveId,
isSwimlanesOn: () => false,
getIssueById: state => id => {

View File

@ -10,7 +10,7 @@ import notesEventHub from '../../notes/event_hub';
import DiffFileHeader from './diff_file_header.vue';
import DiffContent from './diff_content.vue';
import { diffViewerErrors } from '~/ide/constants';
import { collapsedType, isCollapsed } from '../diff_file';
import { collapsedType, isCollapsed } from '../utils/diff_file';
import {
DIFF_FILE_AUTOMATIC_COLLAPSE,
DIFF_FILE_MANUAL_COLLAPSE,

View File

@ -19,7 +19,7 @@ import { __, s__, sprintf } from '~/locale';
import { diffViewerModes } from '~/ide/constants';
import DiffStats from './diff_stats.vue';
import { scrollToElement } from '~/lib/utils/common_utils';
import { isCollapsed } from '../diff_file';
import { isCollapsed } from '../utils/diff_file';
import { DIFF_FILE_HEADER } from '../i18n';
export default {

View File

@ -49,7 +49,7 @@ import {
DIFF_FILE_BY_FILE_COOKIE_NAME,
} from '../constants';
import { diffViewerModes } from '~/ide/constants';
import { isCollapsed } from '../diff_file';
import { isCollapsed } from '../utils/diff_file';
export const setBaseConfig = ({ commit }, options) => {
const {

View File

@ -16,7 +16,7 @@ import {
SHOW_WHITESPACE,
NO_SHOW_WHITESPACE,
} from '../constants';
import { prepareRawDiffFile } from '../diff_file';
import { prepareRawDiffFile } from '../utils/diff_file';
export const isAdded = line => ['new', 'new-nonewline'].includes(line.type);
export const isRemoved = line => ['old', 'old-nonewline'].includes(line.type);

View File

@ -3,7 +3,7 @@ import {
DIFF_FILE_DELETED_MODE,
DIFF_FILE_MANUAL_COLLAPSE,
DIFF_FILE_AUTOMATIC_COLLAPSE,
} from './constants';
} from '../constants';
function fileSymlinkInformation(file, fileList) {
const duplicates = fileList.filter(iteratedFile => iteratedFile.file_hash === file.file_hash);

View File

@ -86,7 +86,7 @@ export default {
<form ref="settingsForm" @submit.prevent="updateAlertsIntegrationSettings">
<gl-form-group class="gl-pl-0">
<gl-form-checkbox v-model="createIssueEnabled" data-qa-selector="create_issue_checkbox">
<span>{{ $options.i18n.createIssue.label }}</span>
<span>{{ $options.i18n.createIncident.label }}</span>
</gl-form-checkbox>
</gl-form-group>
@ -96,7 +96,7 @@ export default {
class="col-8 col-md-9 gl-px-6"
>
<label class="gl-display-inline-flex" for="alert-integration-settings-issue-template">
{{ $options.i18n.issueTemplate.label }}
{{ $options.i18n.incidentTemplate.label }}
<gl-link :href="$options.ISSUE_TEMPLATES_DOCS_LINK" target="_blank">
<gl-icon name="question" :size="12" />
</gl-link>

View File

@ -109,7 +109,20 @@ export default {
{{ webhookUpdateAlertMsg }}
</gl-alert>
<p>{{ $options.i18n.introText }}</p>
<p>
<gl-sprintf :message="$options.i18n.introText">
<template #link="{ content }">
<gl-link
:href="$options.CONFIGURE_PAGERDUTY_WEBHOOK_DOCS_LINK"
target="_blank"
class="gl-display-inline-flex"
>
<span>{{ content }}</span>
<gl-icon name="external-link" />
</gl-link>
</template>
</gl-sprintf>
</p>
<form ref="settingsForm" @submit.prevent="updatePagerDutyIntegrationSettings">
<gl-form-group class="col-8 col-md-9 gl-p-0">
<gl-toggle
@ -134,23 +147,9 @@ export default {
</template>
</gl-form-input-group>
<div class="gl-text-gray-200 gl-pt-2">
<gl-sprintf :message="$options.i18n.webhookUrl.helpText">
<template #docsLink>
<gl-link
:href="$options.CONFIGURE_PAGERDUTY_WEBHOOK_DOCS_LINK"
target="_blank"
class="gl-display-inline-flex"
>
<span>{{ $options.i18n.webhookUrl.helpDocsLink }}</span>
<gl-icon name="external-link" />
</gl-link>
</template>
</gl-sprintf>
</div>
<gl-button
v-gl-modal.resetWebhookModal
class="gl-mt-3"
class="gl-mt-5"
:disabled="loading"
:loading="resettingWebhook"
data-testid="webhook-reset-btn"

View File

@ -33,17 +33,17 @@ export const I18N_ALERT_SETTINGS_FORM = {
saveBtnLabel: __('Save changes'),
introText: __('Action to take when receiving an alert. %{docsLink}'),
introLinkText: __('More information.'),
createIssue: {
label: __('Create an issue. Issues are created for each alert triggered.'),
createIncident: {
label: __('Create an incident. Incidents are created for each alert triggered.'),
},
issueTemplate: {
label: __('Issue template (optional)'),
incidentTemplate: {
label: __('Incident template (optional)'),
},
sendEmail: {
label: __('Send a separate email notification to Developers.'),
},
autoCloseIncidents: {
label: __('Automatically close incident issues when the associated Prometheus alert resolves.'),
label: __('Automatically close incidents when the associated Prometheus alert resolves.'),
},
};
@ -57,17 +57,13 @@ export const ISSUE_TEMPLATES_DOCS_LINK =
export const I18N_PAGERDUTY_SETTINGS_FORM = {
introText: s__(
'PagerDutySettings|Setting up a webhook with PagerDuty will automatically create a GitLab issue for each PagerDuty incident.',
'PagerDutySettings|Create a GitLab incident for each PagerDuty incident by %{linkStart}configuring a webhook in PagerDuty%{linkEnd}',
),
activeToggle: {
label: s__('PagerDutySettings|Active'),
},
webhookUrl: {
label: s__('PagerDutySettings|Webhook URL'),
helpText: s__(
'PagerDutySettings|Create a GitLab issue for each PagerDuty incident by %{docsLink}',
),
helpDocsLink: s__('PagerDutySettings|configuring a webhook in PagerDuty'),
resetWebhookUrl: s__('PagerDutySettings|Reset webhook URL'),
copyToClipboard: __('Copy'),
updateErrMsg: s__('PagerDutySettings|Failed to update Webhook URL'),

View File

@ -59,6 +59,9 @@ export default {
showReset() {
return this.isInstanceOrGroupLevel && this.propsSource.resetPath;
},
saveButtonKey() {
return `save-button-${this.isDisabled}`;
},
},
methods: {
...mapActions([
@ -117,6 +120,7 @@ export default {
<div v-if="isEditable" class="footer-block row-content-block">
<template v-if="isInstanceOrGroupLevel">
<gl-button
:key="saveButtonKey"
v-gl-modal.confirmSaveIntegration
category="primary"
variant="success"
@ -130,6 +134,7 @@ export default {
</template>
<gl-button
v-else
:key="saveButtonKey"
category="primary"
variant="success"
type="submit"

View File

@ -7,7 +7,7 @@ import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
import { getDiffMode } from '~/diffs/store/utils';
import { diffViewerModes } from '~/ide/constants';
import { isCollapsed } from '../../diffs/diff_file';
import { isCollapsed } from '../../diffs/utils/diff_file';
const FIRST_CHAR_REGEX = /^(\+|-| )/;

View File

@ -1,7 +1,7 @@
<script>
import $ from 'jquery';
import { camelCase, difference, union } from 'lodash';
import updateIssueLabelsMutation from '~/boards/queries/issue_set_labels.mutation.graphql';
import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
import createFlash from '~/flash';
import { IssuableType } from '~/issue_show/constants';
import { __ } from '~/locale';

View File

@ -2,13 +2,15 @@
import { mapState, mapActions } from 'vuex';
import {
GlDrawer,
GlBadge,
GlIcon,
GlLink,
GlInfiniteScroll,
GlResizeObserverDirective,
GlTabs,
GlTab,
GlBadge,
GlLoadingIcon,
} from '@gitlab/ui';
import SkeletonLoader from './skeleton_loader.vue';
import Feature from './feature.vue';
import Tracking from '~/tracking';
import { getDrawerBodyHeight } from '../utils/get_drawer_body_height';
@ -17,11 +19,13 @@ const trackingMixin = Tracking.mixin();
export default {
components: {
GlDrawer,
GlBadge,
GlIcon,
GlLink,
GlInfiniteScroll,
GlTabs,
GlTab,
SkeletonLoader,
Feature,
GlBadge,
GlLoadingIcon,
},
directives: {
GlResizeObserver: GlResizeObserverDirective,
@ -31,11 +35,19 @@ export default {
storageKey: {
type: String,
required: true,
default: null,
},
versions: {
type: Array,
required: true,
},
gitlabDotCom: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapState(['open', 'features', 'pageInfo', 'drawerBodyHeight']),
...mapState(['open', 'features', 'pageInfo', 'drawerBodyHeight', 'fetching']),
},
mounted() {
this.openDrawer(this.storageKey);
@ -49,14 +61,25 @@ export default {
methods: {
...mapActions(['openDrawer', 'closeDrawer', 'fetchItems', 'setDrawerBodyHeight']),
bottomReached() {
if (this.pageInfo.nextPage) {
this.fetchItems(this.pageInfo.nextPage);
const page = this.pageInfo.nextPage;
if (page) {
this.fetchItems({ page });
}
},
handleResize() {
const height = getDrawerBodyHeight(this.$refs.drawer.$el);
this.setDrawerBodyHeight(height);
},
featuresForVersion(version) {
return this.features.filter(feature => {
return feature.release === parseFloat(version);
});
},
fetchVersion(version) {
if (this.featuresForVersion(version).length === 0) {
this.fetchItems({ version });
}
},
},
};
</script>
@ -73,64 +96,39 @@ export default {
<template #header>
<h4 class="page-title gl-my-2">{{ __("What's new at GitLab") }}</h4>
</template>
<gl-infinite-scroll
v-if="features.length"
:fetched-items="features.length"
:max-list-height="drawerBodyHeight"
class="gl-p-0"
@bottomReached="bottomReached"
>
<template #items>
<div
v-for="feature in features"
:key="feature.title"
class="gl-pb-7 gl-pt-5 gl-px-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
<template v-if="features.length">
<gl-infinite-scroll
v-if="gitlabDotCom"
:fetched-items="features.length"
:max-list-height="drawerBodyHeight"
class="gl-p-0"
@bottomReached="bottomReached"
>
<template #items>
<feature v-for="feature in features" :key="feature.title" :feature="feature" />
</template>
</gl-infinite-scroll>
<gl-tabs v-else :style="{ height: `${drawerBodyHeight}px` }" class="gl-p-0">
<gl-tab
v-for="(version, index) in versions"
:key="version"
@click="fetchVersion(version)"
>
<gl-link
:href="feature.url"
target="_blank"
class="whats-new-item-title-link"
data-track-event="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
>
<h5 class="gl-font-lg">{{ feature.title }}</h5>
</gl-link>
<div v-if="feature.packages" class="gl-mb-3">
<gl-badge
v-for="package_name in feature.packages"
:key="package_name"
size="sm"
class="whats-new-item-badge gl-mr-2"
>
<gl-icon name="license" />{{ package_name }}
</gl-badge>
</div>
<gl-link
:href="feature.url"
target="_blank"
data-track-event="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
>
<img
:alt="feature.title"
:src="feature.image_url"
class="img-thumbnail gl-px-8 gl-py-3 whats-new-item-image"
<template #title>
<span>{{ version }}</span>
<gl-badge v-if="index === 0">{{ __('Your Version') }}</gl-badge>
</template>
<gl-loading-icon v-if="fetching" size="lg" class="text-center" />
<template v-else>
<feature
v-for="feature in featuresForVersion(version)"
:key="feature.title"
:feature="feature"
/>
</gl-link>
<p class="gl-pt-3">{{ feature.body }}</p>
<gl-link
:href="feature.url"
target="_blank"
data-track-event="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
>{{ __('Learn more') }}</gl-link
>
</div>
</template>
</gl-infinite-scroll>
</template>
</gl-tab>
</gl-tabs>
</template>
<div v-else class="gl-mt-5">
<skeleton-loader />
<skeleton-loader />

View File

@ -0,0 +1,64 @@
<script>
import { GlBadge, GlIcon, GlLink } from '@gitlab/ui';
export default {
components: {
GlBadge,
GlIcon,
GlLink,
},
props: {
feature: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div class="gl-pb-7 gl-pt-5 gl-px-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100">
<gl-link
:href="feature.url"
target="_blank"
class="whats-new-item-title-link"
data-track-event="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
>
<h5 class="gl-font-lg" data-test-id="feature-title">{{ feature.title }}</h5>
</gl-link>
<div v-if="feature.packages" class="gl-mb-3">
<gl-badge
v-for="packageName in feature.packages"
:key="packageName"
size="sm"
class="whats-new-item-badge gl-mr-2"
>
<gl-icon name="license" />{{ packageName }}
</gl-badge>
</div>
<gl-link
:href="feature.url"
target="_blank"
data-track-event="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
>
<img
:alt="feature.title"
:src="feature.image_url"
class="img-thumbnail gl-px-8 gl-py-3 whats-new-item-image"
/>
</gl-link>
<p class="gl-pt-3">{{ feature.body }}</p>
<gl-link
:href="feature.url"
target="_blank"
data-track-event="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
>{{ __('Learn more') }}</gl-link
>
</div>
</template>

View File

@ -10,8 +10,6 @@ export default el => {
if (whatsNewApp) {
store.dispatch('openDrawer');
} else {
const storageKey = getStorageKey(el);
whatsNewApp = new Vue({
el,
store,
@ -28,7 +26,11 @@ export default el => {
},
render(createElement) {
return createElement('app', {
props: { storageKey },
props: {
storageKey: getStorageKey(el),
versions: JSON.parse(el.getAttribute('data-versions')),
gitlabDotCom: el.getAttribute('data-gitlab-dot-com'),
},
});
},
});

View File

@ -13,7 +13,7 @@ export default {
localStorage.setItem(storageKey, JSON.stringify(false));
}
},
fetchItems({ commit, state }, page) {
fetchItems({ commit, state }, { page, version } = { page: null, version: null }) {
if (state.fetching) {
return false;
}
@ -24,6 +24,7 @@ export default {
.get('/-/whats_new', {
params: {
page,
version,
},
})
.then(({ data, headers }) => {

View File

@ -6,6 +6,32 @@
.gl-infinite-scroll-legend {
@include gl-display-none;
}
.gl-tabs {
@include gl-overflow-y-auto;
}
.gl-tabs-nav {
flex-wrap: nowrap;
overflow-x: scroll;
align-items: stretch;
.nav-item {
@include gl-flex-shrink-0;
a {
@include gl-h-full;
line-height: 1.5;
}
}
}
.gl-spinner-container {
@include gl-w-full;
@include gl-absolute;
top: 50%;
transform: translateY(-50%);
}
}
.with-performance-bar .whats-new-drawer {

View File

@ -28,3 +28,115 @@
}
}
}
//// Copied from roadmaps.scss - adapted for on-call schedules
$header-item-height: 60px;
$details-cell-width: px-to-rem(150px);
$timeline-cell-height: 32px;
$timeline-cell-width: 180px;
$border-style: 1px solid var(--gray-100, $gray-100);
$gradient-dark-gray: rgba(0, 0, 0, 0.15);
$gradient-gray: rgba(255, 255, 255, 0.001);
$scroll-top-gradient: linear-gradient(to bottom, $gradient-dark-gray 0%, $gradient-gray 100%);
$scroll-bottom-gradient: linear-gradient(to bottom, $gradient-gray 0%, $gradient-dark-gray 100%);
$column-right-gradient: linear-gradient(to right, $gradient-dark-gray 0%, $gradient-gray 100%);
$epic-details-cell-width: 150px;
.schedule-shell {
@include gl-relative;
@include gl-h-full;
@include gl-w-full;
@include gl-overflow-x-auto;
@include gl-border-gray-100;
@include gl-border-1;
@include gl-border-solid;
@include gl-rounded-base;
}
.timeline-section {
@include gl-sticky;
position: -webkit-sticky;
@include gl-top-0;
z-index: 20;
.timeline-header-blank,
.timeline-header-item {
@include float-left;
height: $header-item-height;
border-bottom: $border-style;
background-color: var(--white, $white);
}
.timeline-header-blank {
@include gl-sticky;
position: -webkit-sticky;
@include gl-top-0;
@include gl-left-0;
width: $details-cell-width;
z-index: 2;
&::after {
height: $header-item-height;
@include gl-content-empty;
@include gl-absolute;
@include gl-top-0;
right: -$grid-size;
width: $grid-size;
@include gl-pointer-events-none;
background: $column-right-gradient;
}
}
.timeline-header-item {
// container size minus left panel width divided by 2 week timeframes
width: calc((100% - #{$epic-details-cell-width}) / 2);
&:last-of-type .item-label {
@include gl-border-r-0;
}
.item-label,
.item-sublabel .sublabel-value {
color: var(--gray-400, $gray-400);
@include gl-font-weight-normal;
&.label-dark {
@include gl-text-gray-900;
}
&.label-bold {
@include gl-font-weight-bold;
}
}
.item-label {
padding: $gl-padding-8 $gl-padding;
border-right: $border-style;
border-bottom: $border-style;
}
.item-sublabel {
@include gl-relative;
@include gl-display-flex;
.sublabel-value {
@include gl-flex-grow-1;
@include gl-flex-basis-0;
text-align: center;
font-size: $code-font-size;
line-height: 1.5;
padding: 2px 0;
}
}
.current-day-indicator-header {
@include gl-bottom-0;
height: $gl-vert-padding;
width: $gl-vert-padding;
background-color: var(--red-500, $red-500);
border-radius: 50%;
transform: translateX(-3px);
}
}
}

View File

@ -1,16 +1,19 @@
# frozen_string_literal: true
class WhatsNewController < ApplicationController
include Gitlab::Utils::StrongMemoize
skip_before_action :authenticate_user!
before_action :check_feature_flag, :check_valid_page_param, :set_pagination_headers
before_action :check_feature_flag
before_action :check_valid_page_param, :set_pagination_headers, unless: -> { has_version_param? }
feature_category :navigation
def index
respond_to do |format|
format.js do
render json: most_recent_items
render json: highlight_items
end
end
end
@ -29,15 +32,25 @@ class WhatsNewController < ApplicationController
params[:page]&.to_i || 1
end
def most_recent
@most_recent ||= ReleaseHighlight.paginated(page: current_page)
def highlights
strong_memoize(:highlights) do
if has_version_param?
ReleaseHighlight.for_version(version: params[:version])
else
ReleaseHighlight.paginated(page: current_page)
end
end
end
def most_recent_items
most_recent[:items].map {|item| Gitlab::WhatsNew::ItemPresenter.present(item) }
def highlight_items
highlights.map {|item| Gitlab::WhatsNew::ItemPresenter.present(item) }
end
def set_pagination_headers
response.set_header('X-Next-Page', most_recent[:next_page])
response.set_header('X-Next-Page', highlights.next_page)
end
def has_version_param?
params[:version].present?
end
end

View File

@ -6,10 +6,14 @@ module WhatsNewHelper
end
def whats_new_storage_key
most_recent_version = ReleaseHighlight.most_recent_version
most_recent_version = ReleaseHighlight.versions&.first
return unless most_recent_version
['display-whats-new-notification', most_recent_version].join('-')
end
def whats_new_versions
ReleaseHighlight.versions
end
end

View File

@ -3,6 +3,17 @@
class ReleaseHighlight
CACHE_DURATION = 1.hour
FILES_PATH = Rails.root.join('data', 'whats_new', '*.yml')
RELEASE_VERSIONS_IN_A_YEAR = 12
def self.for_version(version:)
index = self.versions.index(version)
return if index.nil?
page = index + 1
self.paginated(page: page)
end
def self.paginated(page: 1)
Rails.cache.fetch(cache_key(page), expires_in: CACHE_DURATION) do
@ -10,10 +21,7 @@ class ReleaseHighlight
next if items.nil?
{
items: items,
next_page: next_page(current_page: page)
}
QueryResult.new(items: items, next_page: next_page(current_page: page))
end
end
@ -53,15 +61,25 @@ class ReleaseHighlight
next_page if self.file_paths[next_index]
end
def self.most_recent_version
Gitlab::ProcessMemoryCache.cache_backend.fetch('release_highlight:release_version', expires_in: CACHE_DURATION) do
self.paginated&.[](:items)&.first&.[]('release')
def self.most_recent_item_count
Gitlab::ProcessMemoryCache.cache_backend.fetch('release_highlight:recent_item_count', expires_in: CACHE_DURATION) do
self.paginated&.items&.count
end
end
def self.most_recent_item_count
Gitlab::ProcessMemoryCache.cache_backend.fetch('release_highlight:recent_item_count', expires_in: CACHE_DURATION) do
self.paginated&.[](:items)&.count
def self.versions
Gitlab::ProcessMemoryCache.cache_backend.fetch('release_highlight:versions', expires_in: CACHE_DURATION) do
versions = self.file_paths.first(RELEASE_VERSIONS_IN_A_YEAR).map do |path|
/\d*\_(\d*\_\d*)\.yml$/.match(path).captures[0].gsub(/0(?=\d)/, "").tr("_", ".")
end
versions.uniq
end
end
QueryResult = Struct.new(:items, :next_page, keyword_init: true) do
include Enumerable
delegate :each, to: :items
end
end

View File

@ -70,6 +70,7 @@ class Service < ApplicationRecord
scope :by_type, -> (type) { where(type: type) }
scope :by_active_flag, -> (flag) { where(active: flag) }
scope :inherit_from_id, -> (id) { where(inherit_from_id: id) }
scope :inherit, -> { where.not(inherit_from_id: nil) }
scope :for_group, -> (group) { where(group_id: group, type: available_services_types(include_project_specific: false)) }
scope :for_template, -> { where(template: true, type: available_services_types(include_project_specific: false)) }
scope :for_instance, -> { where(instance: true, type: available_services_types(include_project_specific: false)) }
@ -278,7 +279,7 @@ class Service < ApplicationRecord
active.where(instance: true),
active.where(group_id: group_ids, inherit_from_id: nil)
]).order(Arel.sql("type ASC, array_position(#{array}::bigint[], services.group_id), instance DESC")).group_by(&:type).each do |type, records|
build_from_integration(records.first, association => scope.id).save!
build_from_integration(records.first, association => scope.id).save
end
end

View File

@ -28,9 +28,11 @@ module Groups
Group.transaction do
update_group_attributes
ensure_ownership
update_integrations
end
post_update_hooks(@updated_project_ids)
propagate_integrations
true
end
@ -196,6 +198,17 @@ module Groups
raise TransferError, result[:message] unless result[:status] == :success
end
end
def update_integrations
@group.services.inherit.delete_all
Service.create_from_active_default_integrations(@group, :group_id)
end
def propagate_integrations
@group.services.inherit.each do |integration|
PropagateIntegrationWorker.perform_async(integration.id)
end
end
end
end

View File

@ -59,7 +59,7 @@ module Projects
raise TransferError.new(s_("TransferProject|Root namespace can't be updated if project has NPM packages"))
end
attempt_transfer_transaction
proceed_to_transfer
end
# rubocop: enable CodeReuse/ActiveRecord
@ -67,7 +67,7 @@ module Projects
new_namespace.root_ancestor == project.namespace.root_ancestor
end
def attempt_transfer_transaction
def proceed_to_transfer
Project.transaction do
project.expire_caches_before_rename(@old_path)
@ -87,6 +87,8 @@ module Projects
# Move uploads
move_project_uploads(project)
update_integrations
project.old_path_with_namespace = @old_path
update_repository_configuration(@new_path)
@ -214,6 +216,11 @@ module Projects
project.shared_runners_enabled = false
end
end
def update_integrations
project.services.inherit.delete_all
Service.create_from_active_default_integrations(project, :project_id)
end
end
end

View File

@ -102,7 +102,7 @@
= sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left')
- if ::Feature.enabled?(:whats_new_drawer, current_user)
#whats-new-app{ data: { storage_key: whats_new_storage_key } }
#whats-new-app{ data: { storage_key: whats_new_storage_key, versions: whats_new_versions, gitlab_dot_com: Gitlab.dev_env_org_or_com? } }
- if can?(current_user, :update_user_status, current_user)
.js-set-status-modal-wrapper{ data: user_status_data }

View File

@ -0,0 +1,5 @@
---
title: Transfer a project/group to a new namespace inheriting integrations
merge_request: 48621
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Resolve Save button should have a different color on press
merge_request: 48975
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Use incident instead of issue for operation settings
merge_request: 48406
author:
type: fixed

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

View File

@ -5,7 +5,10 @@
# Utility module for A/B testing experimental features. Define your experiments in the `EXPERIMENTS` constant.
# Experiment options:
# - tracking_category (optional, used to set the category when tracking an experiment event)
# - use_backwards_compatible_subject_index (optional, set this to true if you need backwards compatibility)
# - use_backwards_compatible_subject_index (optional, set this to true if you need backwards compatibility -- you likely do not need this, see note in the next paragraph.)
#
# Using the backwards-compatible subject index (use_backwards_compatible_subject_index option):
# This option was added when [the calculation of experimentation_subject_index was changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45733/diffs#41af4a6fa5a10c7068559ce21c5188483751d934_157_173). It is not intended to be used by new experiments, it exists merely for the segmentation integrity of in-flight experiments at the time the change was deployed. That is, we want users who were assigned to the "experimental" group or the "control" group before the change to still be in those same groups after the change. See [the original issue](https://gitlab.com/gitlab-org/gitlab/-/issues/270858) and [this related comment](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48110#note_458223745) for more information.
#
# The experiment is controlled by a Feature Flag (https://docs.gitlab.com/ee/development/feature_flags/controls.html),
# which is named "#{experiment_key}_experiment_percentage" and *must* be set with a percentage and not be used for other purposes.

View File

@ -941,6 +941,9 @@ msgstr ""
msgid "(No changes)"
msgstr ""
msgid "(UTC%{offset}) %{timezone}"
msgstr ""
msgid "(check progress)"
msgstr ""
@ -4169,7 +4172,7 @@ msgstr ""
msgid "Automatic deployment rollbacks"
msgstr ""
msgid "Automatically close incident issues when the associated Prometheus alert resolves."
msgid "Automatically close incidents when the associated Prometheus alert resolves."
msgstr ""
msgid "Automatically create merge requests for vulnerabilities that have fixes available."
@ -7919,7 +7922,7 @@ msgstr ""
msgid "Create an account using:"
msgstr ""
msgid "Create an issue. Issues are created for each alert triggered."
msgid "Create an incident. Incidents are created for each alert triggered."
msgstr ""
msgid "Create and provide your GitHub %{link_start}Personal Access Token%{link_end}. You will need to select the %{code_open}repo%{code_close} scope, so we can display a list of your public and private repositories which are available to import."
@ -14377,6 +14380,9 @@ msgstr ""
msgid "Incident Management Limits"
msgstr ""
msgid "Incident template (optional)"
msgstr ""
msgid "IncidentManagement|%{hours} hours, %{minutes} minutes remaining"
msgstr ""
@ -15207,9 +15213,6 @@ msgstr ""
msgid "Issue published on status page."
msgstr ""
msgid "Issue template (optional)"
msgstr ""
msgid "Issue update failed"
msgstr ""
@ -19112,6 +19115,12 @@ msgstr ""
msgid "OnCallSchedules|Failed to add schedule"
msgstr ""
msgid "OnCallSchedules|On-call schedule"
msgstr ""
msgid "OnCallSchedules|On-call schedule for the %{tzShort}"
msgstr ""
msgid "OnCallSchedules|Rotation length"
msgstr ""
@ -19729,7 +19738,7 @@ msgstr ""
msgid "PagerDutySettings|Active"
msgstr ""
msgid "PagerDutySettings|Create a GitLab issue for each PagerDuty incident by %{docsLink}"
msgid "PagerDutySettings|Create a GitLab incident for each PagerDuty incident by %{linkStart}configuring a webhook in PagerDuty%{linkEnd}"
msgstr ""
msgid "PagerDutySettings|Failed to update Webhook URL"
@ -19741,18 +19750,12 @@ msgstr ""
msgid "PagerDutySettings|Resetting the webhook URL for this project will require updating this integration's settings in PagerDuty."
msgstr ""
msgid "PagerDutySettings|Setting up a webhook with PagerDuty will automatically create a GitLab issue for each PagerDuty incident."
msgstr ""
msgid "PagerDutySettings|Webhook URL"
msgstr ""
msgid "PagerDutySettings|Webhook URL update was successful"
msgstr ""
msgid "PagerDutySettings|configuring a webhook in PagerDuty"
msgstr ""
msgid "Pages"
msgstr ""
@ -25174,6 +25177,9 @@ msgstr ""
msgid "Show file contents"
msgstr ""
msgid "Show labels"
msgstr ""
msgid "Show latest version"
msgstr ""
@ -31711,6 +31717,9 @@ msgstr ""
msgid "Your U2F device was registered!"
msgstr ""
msgid "Your Version"
msgstr ""
msgid "Your WebAuthn device did not send a valid JSON response."
msgstr ""

View File

@ -23,7 +23,7 @@ RSpec.describe 'Projects > Settings > For a forked project', :js do
describe 'Settings > Operations' do
describe 'Incidents' do
let(:create_issue) { 'Create an issue. Issues are created for each alert triggered.' }
let(:create_issue) { 'Create an incident. Incidents are created for each alert triggered.' }
let(:send_email) { 'Send a separate email notification to Developers.' }
before do

View File

@ -14,7 +14,7 @@ import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dro
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import store from '~/boards/stores';
import getIssueParticipants from '~/vue_shared/components/sidebar/queries/getIssueParticipants.query.graphql';
import searchUsers from '~/boards/queries/users_search.query.graphql';
import searchUsers from '~/boards/graphql/users_search.query.graphql';
import { participants } from '../mock_data';
const localVue = createLocalVue();

View File

@ -14,8 +14,8 @@ import {
import actions, { gqlClient } from '~/boards/stores/actions';
import * as types from '~/boards/stores/mutation_types';
import { inactiveId } from '~/boards/constants';
import issueMoveListMutation from '~/boards/queries/issue_move_list.mutation.graphql';
import destroyBoardListMutation from '~/boards/queries/board_list_destroy.mutation.graphql';
import issueMoveListMutation from '~/boards/graphql/issue_move_list.mutation.graphql';
import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutation.graphql';
import updateAssignees from '~/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql';
import { fullBoardId, formatListIssues, formatBoardLists } from '~/boards/boards_util';

View File

@ -10,24 +10,6 @@ import {
} from '../mock_data';
describe('Boards - Getters', () => {
describe('labelToggleState', () => {
it('should return "on" when isShowingLabels is true', () => {
const state = {
isShowingLabels: true,
};
expect(getters.labelToggleState(state)).toBe('on');
});
it('should return "off" when isShowingLabels is false', () => {
const state = {
isShowingLabels: false,
};
expect(getters.labelToggleState(state)).toBe('off');
});
});
describe('isSidebarOpen', () => {
it('returns true when activeId is not equal to 0', () => {
const state = {

View File

@ -1,4 +1,4 @@
import { prepareRawDiffFile } from '~/diffs/diff_file';
import { prepareRawDiffFile } from '~/diffs/utils/diff_file';
const DIFF_FILES = [
{

View File

@ -17,7 +17,7 @@ exports[`Alert integration settings form default state should match the default
data-qa-selector="create_issue_checkbox"
>
<span>
Create an issue. Issues are created for each alert triggered.
Create an incident. Incidents are created for each alert triggered.
</span>
</gl-form-checkbox-stub>
</gl-form-group-stub>
@ -32,7 +32,7 @@ exports[`Alert integration settings form default state should match the default
for="alert-integration-settings-issue-template"
>
Issue template (optional)
Incident template (optional)
<gl-link-stub
href="/help/user/project/description_templates#creating-issue-templates"
@ -89,7 +89,7 @@ exports[`Alert integration settings form default state should match the default
checked="true"
>
<span>
Automatically close incident issues when the associated Prometheus alert resolves.
Automatically close incidents when the associated Prometheus alert resolves.
</span>
</gl-form-checkbox-stub>
</gl-form-group-stub>

View File

@ -5,7 +5,9 @@ exports[`Alert integration settings form should match the default snapshot 1`] =
<!---->
<p>
Setting up a webhook with PagerDuty will automatically create a GitLab issue for each PagerDuty incident.
<gl-sprintf-stub
message="Create a GitLab incident for each PagerDuty incident by %{linkStart}configuring a webhook in PagerDuty%{linkEnd}"
/>
</p>
<form>
@ -33,18 +35,10 @@ exports[`Alert integration settings form should match the default snapshot 1`] =
value="pagerduty.webhook.com"
/>
<div
class="gl-text-gray-200 gl-pt-2"
>
<gl-sprintf-stub
message="Create a GitLab issue for each PagerDuty incident by %{docsLink}"
/>
</div>
<gl-button-stub
buttontextclasses=""
category="primary"
class="gl-mt-3"
class="gl-mt-5"
data-testid="webhook-reset-btn"
icon=""
role="button"

View File

@ -3,7 +3,7 @@ import {
mockLabels,
mockRegularLabel,
} from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data';
import updateIssueLabelsMutation from '~/boards/queries/issue_set_labels.mutation.graphql';
import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
import { MutationOperationMode } from '~/graphql_shared/utils';
import { IssuableType } from '~/issue_show/constants';
import SidebarLabels from '~/sidebar/components/labels/sidebar_labels.vue';

View File

@ -1,6 +1,6 @@
import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
import { GlDrawer, GlInfiniteScroll } from '@gitlab/ui';
import { GlDrawer, GlInfiniteScroll, GlTabs } from '@gitlab/ui';
import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import App from '~/whats_new/components/app.vue';
@ -16,12 +16,18 @@ const localVue = createLocalVue();
localVue.use(Vuex);
describe('App', () => {
const propsData = { storageKey: 'storage-key' };
let wrapper;
let store;
let actions;
let state;
let trackingSpy;
let gitlabDotCom = true;
const buildProps = () => ({
storageKey: 'storage-key',
versions: ['3.11', '3.10'],
gitlabDotCom,
});
const buildWrapper = () => {
actions = {
@ -45,7 +51,7 @@ describe('App', () => {
wrapper = mount(App, {
localVue,
store,
propsData,
propsData: buildProps(),
directives: {
GlResizeObserver: createMockDirective(),
},
@ -53,112 +59,171 @@ describe('App', () => {
};
const findInfiniteScroll = () => wrapper.find(GlInfiniteScroll);
const emitBottomReached = () => findInfiniteScroll().vm.$emit('bottomReached');
beforeEach(async () => {
const setup = async () => {
document.body.dataset.page = 'test-page';
document.body.dataset.namespaceId = 'namespace-840';
trackingSpy = mockTracking('_category_', null, jest.spyOn);
buildWrapper();
wrapper.vm.$store.state.features = [{ title: 'Whats New Drawer', url: 'www.url.com' }];
wrapper.vm.$store.state.features = [
{ title: 'Whats New Drawer', url: 'www.url.com', release: 3.11 },
];
wrapper.vm.$store.state.drawerBodyHeight = MOCK_DRAWER_BODY_HEIGHT;
await wrapper.vm.$nextTick();
});
};
afterEach(() => {
wrapper.destroy();
unmockTracking();
});
const getDrawer = () => wrapper.find(GlDrawer);
it('contains a drawer', () => {
expect(getDrawer().exists()).toBe(true);
});
it('dispatches openDrawer and tracking calls when mounted', () => {
expect(actions.openDrawer).toHaveBeenCalledWith(expect.any(Object), 'storage-key');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_whats_new_drawer', {
label: 'namespace_id',
value: 'namespace-840',
});
});
it('dispatches closeDrawer when clicking close', () => {
getDrawer().vm.$emit('close');
expect(actions.closeDrawer).toHaveBeenCalled();
});
it.each([true, false])('passes open property', async openState => {
wrapper.vm.$store.state.open = openState;
await wrapper.vm.$nextTick();
expect(getDrawer().props('open')).toBe(openState);
});
it('renders features when provided via ajax', () => {
expect(actions.fetchItems).toHaveBeenCalled();
expect(wrapper.find('h5').text()).toBe('Whats New Drawer');
});
it('send an event when feature item is clicked', () => {
trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
const link = wrapper.find('.whats-new-item-title-link');
triggerEvent(link.element);
expect(trackingSpy.mock.calls[1]).toMatchObject([
'_category_',
'click_whats_new_item',
{
label: 'Whats New Drawer',
property: 'www.url.com',
},
]);
});
it('renders infinite scroll', () => {
const scroll = findInfiniteScroll();
expect(scroll.props()).toMatchObject({
fetchedItems: wrapper.vm.$store.state.features.length,
maxListHeight: MOCK_DRAWER_BODY_HEIGHT,
});
});
describe('bottomReached', () => {
describe('gitlab.com', () => {
beforeEach(() => {
actions.fetchItems.mockClear();
setup();
});
it('when nextPage exists it calls fetchItems', () => {
wrapper.vm.$store.state.pageInfo = { nextPage: 840 };
emitBottomReached();
const getDrawer = () => wrapper.find(GlDrawer);
expect(actions.fetchItems).toHaveBeenCalledWith(expect.anything(), 840);
it('contains a drawer', () => {
expect(getDrawer().exists()).toBe(true);
});
it('when nextPage does not exist it does not call fetchItems', () => {
wrapper.vm.$store.state.pageInfo = { nextPage: null };
emitBottomReached();
it('dispatches openDrawer and tracking calls when mounted', () => {
expect(actions.openDrawer).toHaveBeenCalledWith(expect.any(Object), 'storage-key');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_whats_new_drawer', {
label: 'namespace_id',
value: 'namespace-840',
});
});
expect(actions.fetchItems).not.toHaveBeenCalled();
it('dispatches closeDrawer when clicking close', () => {
getDrawer().vm.$emit('close');
expect(actions.closeDrawer).toHaveBeenCalled();
});
it.each([true, false])('passes open property', async openState => {
wrapper.vm.$store.state.open = openState;
await wrapper.vm.$nextTick();
expect(getDrawer().props('open')).toBe(openState);
});
it('renders features when provided via ajax', () => {
expect(actions.fetchItems).toHaveBeenCalled();
expect(wrapper.find('[data-test-id="feature-title"]').text()).toBe('Whats New Drawer');
});
it('send an event when feature item is clicked', () => {
trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
const link = wrapper.find('.whats-new-item-title-link');
triggerEvent(link.element);
expect(trackingSpy.mock.calls[1]).toMatchObject([
'_category_',
'click_whats_new_item',
{
label: 'Whats New Drawer',
property: 'www.url.com',
},
]);
});
it('renders infinite scroll', () => {
const scroll = findInfiniteScroll();
expect(scroll.props()).toMatchObject({
fetchedItems: wrapper.vm.$store.state.features.length,
maxListHeight: MOCK_DRAWER_BODY_HEIGHT,
});
});
describe('bottomReached', () => {
const emitBottomReached = () => findInfiniteScroll().vm.$emit('bottomReached');
beforeEach(() => {
actions.fetchItems.mockClear();
});
it('when nextPage exists it calls fetchItems', () => {
wrapper.vm.$store.state.pageInfo = { nextPage: 840 };
emitBottomReached();
expect(actions.fetchItems).toHaveBeenCalledWith(expect.anything(), { page: 840 });
});
it('when nextPage does not exist it does not call fetchItems', () => {
wrapper.vm.$store.state.pageInfo = { nextPage: null };
emitBottomReached();
expect(actions.fetchItems).not.toHaveBeenCalled();
});
});
it('calls getDrawerBodyHeight and setDrawerBodyHeight when resize directive is triggered', () => {
const { value } = getBinding(getDrawer().element, 'gl-resize-observer');
value();
expect(getDrawerBodyHeight).toHaveBeenCalledWith(wrapper.find(GlDrawer).element);
expect(actions.setDrawerBodyHeight).toHaveBeenCalledWith(
expect.any(Object),
MOCK_DRAWER_BODY_HEIGHT,
);
});
});
it('calls getDrawerBodyHeight and setDrawerBodyHeight when resize directive is triggered', () => {
const { value } = getBinding(getDrawer().element, 'gl-resize-observer');
describe('self managed', () => {
const findTabs = () => wrapper.find(GlTabs);
value();
const clickSecondTab = async () => {
const secondTab = wrapper.findAll('.nav-link').at(1);
await secondTab.trigger('click');
await new Promise(resolve => requestAnimationFrame(resolve));
};
expect(getDrawerBodyHeight).toHaveBeenCalledWith(wrapper.find(GlDrawer).element);
beforeEach(() => {
gitlabDotCom = false;
setup();
});
expect(actions.setDrawerBodyHeight).toHaveBeenCalledWith(
expect.any(Object),
MOCK_DRAWER_BODY_HEIGHT,
);
it('renders tabs with drawer body height and content', () => {
const scroll = findInfiniteScroll();
const tabs = findTabs();
expect(scroll.exists()).toBe(false);
expect(tabs.attributes().style).toBe(`height: ${MOCK_DRAWER_BODY_HEIGHT}px;`);
expect(wrapper.find('h5').text()).toBe('Whats New Drawer');
});
describe('fetchVersion', () => {
beforeEach(() => {
actions.fetchItems.mockClear();
});
it('when version isnt fetched, clicking a tab calls fetchItems', async () => {
const fetchVersionSpy = jest.spyOn(wrapper.vm, 'fetchVersion');
await clickSecondTab();
expect(fetchVersionSpy).toHaveBeenCalledWith('3.10');
expect(actions.fetchItems).toHaveBeenCalledWith(expect.anything(), { version: '3.10' });
});
it('when version has been fetched, clicking a tab calls fetchItems', async () => {
wrapper.vm.$store.state.features.push({ title: 'GitLab Stories', release: 3.1 });
await wrapper.vm.$nextTick();
const fetchVersionSpy = jest.spyOn(wrapper.vm, 'fetchVersion');
await clickSecondTab();
expect(fetchVersionSpy).toHaveBeenCalledWith('3.10');
expect(actions.fetchItems).not.toHaveBeenCalled();
expect(wrapper.find('.tab-pane.active h5').text()).toBe('GitLab Stories');
});
});
});
});

View File

@ -41,6 +41,23 @@ describe('whats new actions', () => {
axiosMock.restore();
});
it('passes arguments', () => {
axiosMock.reset();
axiosMock
.onGet('/-/whats_new', { params: { page: 8, version: 40 } })
.replyOnce(200, [{ title: 'GitLab Stories' }]);
testAction(
actions.fetchItems,
{ page: 8, version: 40 },
{},
expect.arrayContaining([
{ type: types.ADD_FEATURES, payload: [{ title: 'GitLab Stories' }] },
]),
);
});
it('if already fetching, does not fetch', () => {
testAction(actions.fetchItems, {}, { fetching: true }, []);
});

View File

@ -10,7 +10,7 @@ RSpec.describe WhatsNewHelper do
let(:release_item) { double(:item) }
before do
allow(ReleaseHighlight).to receive(:most_recent_version).and_return(84.0)
allow(ReleaseHighlight).to receive(:versions).and_return([84.0])
end
it { is_expected.to eq('display-whats-new-notification-84.0') }
@ -18,7 +18,7 @@ RSpec.describe WhatsNewHelper do
context 'when most recent release highlights do NOT exist' do
before do
allow(ReleaseHighlight).to receive(:most_recent_version).and_return(nil)
allow(ReleaseHighlight).to receive(:versions).and_return(nil)
end
it { is_expected.to be_nil }
@ -44,4 +44,14 @@ RSpec.describe WhatsNewHelper do
end
end
end
describe '#whats_new_versions' do
let(:versions) { [84.0] }
it 'returns ReleaseHighlight.versions' do
expect(ReleaseHighlight).to receive(:versions).and_return(versions)
expect(helper.whats_new_versions).to eq(versions)
end
end
end

View File

@ -3,21 +3,44 @@
require 'spec_helper'
RSpec.describe ReleaseHighlight do
describe '#paginated' do
let(:fixture_dir_glob) { Dir.glob(File.join('spec', 'fixtures', 'whats_new', '*.yml')) }
let(:cache_mock) { double(:cache_mock) }
let(:fixture_dir_glob) { Dir.glob(File.join('spec', 'fixtures', 'whats_new', '*.yml')) }
let(:cache_mock) { double(:cache_mock) }
before do
allow(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob)
allow(cache_mock).to receive(:fetch).with('release_highlight:file_paths', expires_in: 1.hour).and_yield
end
after do
ReleaseHighlight.instance_variable_set(:@file_paths, nil)
end
describe '.for_version' do
subject { ReleaseHighlight.for_version(version: version) }
let(:version) { '1.1' }
context 'with version param that exists' do
it 'returns items from that version' do
expect(subject.items.first['title']).to eq("It's gonna be a bright")
end
end
context 'with version param that does NOT exist' do
let(:version) { '84.0' }
it 'returns nil' do
expect(subject).to be_nil
end
end
end
describe '.paginated' do
let(:dot_com) { false }
before do
allow(Gitlab).to receive(:com?).and_return(dot_com)
allow(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob)
expect(Rails).to receive(:cache).twice.and_return(cache_mock)
expect(cache_mock).to receive(:fetch).with('release_highlight:file_paths', expires_in: 1.hour).and_yield
end
after do
ReleaseHighlight.instance_variable_set(:@file_paths, nil)
end
context 'with page param' do
@ -90,35 +113,12 @@ RSpec.describe ReleaseHighlight do
end
end
describe '.most_recent_version' do
subject { ReleaseHighlight.most_recent_version }
context 'when version exist' do
let(:release_item) { double(:item) }
before do
allow(ReleaseHighlight).to receive(:paginated).and_return({ items: [release_item] })
allow(release_item).to receive(:[]).with('release').and_return(84.0)
end
it { is_expected.to eq(84.0) }
end
context 'when most recent release highlights do NOT exist' do
before do
allow(ReleaseHighlight).to receive(:paginated).and_return(nil)
end
it { is_expected.to be_nil }
end
end
describe '#most_recent_item_count' do
describe '.most_recent_item_count' do
subject { ReleaseHighlight.most_recent_item_count }
context 'when recent release items exist' do
it 'returns the count from the most recent file' do
allow(ReleaseHighlight).to receive(:paginated).and_return({ items: [double(:item)] })
allow(ReleaseHighlight).to receive(:paginated).and_return(double(:paginated, items: [double(:item)]))
expect(subject).to eq(1)
end
@ -132,4 +132,32 @@ RSpec.describe ReleaseHighlight do
end
end
end
describe '.versions' do
it 'returns versions from the file paths' do
expect(ReleaseHighlight.versions).to eq(['1.5', '1.2', '1.1'])
end
context 'when there are more than 12 versions' do
let(:file_paths) do
i = 0
Array.new(20) { "20201225_01_#{i += 1}.yml" }
end
it 'limits to 12 versions' do
allow(ReleaseHighlight).to receive(:file_paths).and_return(file_paths)
expect(ReleaseHighlight.versions.count).to eq(12)
end
end
end
describe 'QueryResult' do
subject { ReleaseHighlight::QueryResult.new(items: items, next_page: 2) }
let(:items) { [:item] }
it 'responds to map' do
expect(subject.map(&:to_s)).to eq(items.map(&:to_s))
end
end
end

View File

@ -4,22 +4,22 @@ require 'spec_helper'
RSpec.describe WhatsNewController do
describe 'whats_new_path' do
let(:item) { double(:item) }
let(:highlights) { double(:highlight, items: [item], map: [item].map, next_page: 2) }
context 'with whats_new_drawer feature enabled' do
before do
stub_feature_flags(whats_new_drawer: true)
end
context 'with no page param' do
let(:most_recent) { { items: [item], next_page: 2 } }
let(:item) { double(:item) }
it 'responds with paginated data and headers' do
allow(ReleaseHighlight).to receive(:paginated).with(page: 1).and_return(most_recent)
allow(ReleaseHighlight).to receive(:paginated).with(page: 1).and_return(highlights)
allow(Gitlab::WhatsNew::ItemPresenter).to receive(:present).with(item).and_return(item)
get whats_new_path, xhr: true
expect(response.body).to eq(most_recent[:items].to_json)
expect(response.body).to eq(highlights.items.to_json)
expect(response.headers['X-Next-Page']).to eq(2)
end
end
@ -37,6 +37,18 @@ RSpec.describe WhatsNewController do
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'with version param' do
it 'returns items without pagination headers' do
allow(ReleaseHighlight).to receive(:for_version).with(version: '42').and_return(highlights)
allow(Gitlab::WhatsNew::ItemPresenter).to receive(:present).with(item).and_return(item)
get whats_new_path(version: 42), xhr: true
expect(response.body).to eq(highlights.items.to_json)
expect(response.headers['X-Next-Page']).to be_nil
end
end
end
context 'with whats_new_drawer feature disabled' do

View File

@ -3,15 +3,15 @@
require 'spec_helper'
RSpec.describe Groups::TransferService do
let(:user) { create(:user) }
let(:new_parent_group) { create(:group, :public) }
let_it_be(:user) { create(:user) }
let_it_be(:new_parent_group) { create(:group, :public) }
let!(:group_member) { create(:group_member, :owner, group: group, user: user) }
let(:transfer_service) { described_class.new(group, user) }
context 'handling packages' do
let_it_be(:group) { create(:group, :public) }
let_it_be(:new_group) { create(:group, :public) }
let(:project) { create(:project, :public, namespace: group) }
let(:new_group) { create(:group, :public) }
before do
group.add_owner(user)
@ -35,8 +35,8 @@ RSpec.describe Groups::TransferService do
it_behaves_like 'transfer not allowed'
context 'with a project within subgroup' do
let(:root_group) { create(:group) }
let(:group) { create(:group, parent: root_group) }
let_it_be(:root_group) { create(:group) }
let_it_be(:group) { create(:group, parent: root_group) }
before do
root_group.add_owner(user)
@ -79,8 +79,6 @@ RSpec.describe Groups::TransferService do
shared_examples 'ensuring allowed transfer for a group' do
context "when there's an exception on GitLab shell directories" do
let(:new_parent_group) { create(:group, :public) }
before do
allow_next_instance_of(described_class) do |instance|
allow(instance).to receive(:update_group_attributes).and_raise(Gitlab::UpdatePathError, 'namespace directory cannot be moved')
@ -101,7 +99,7 @@ RSpec.describe Groups::TransferService do
describe '#execute' do
context 'when transforming a group into a root group' do
let!(:group) { create(:group, :public, :nested) }
let_it_be_with_reload(:group) { create(:group, :public, :nested) }
it_behaves_like 'ensuring allowed transfer for a group'
@ -115,7 +113,7 @@ RSpec.describe Groups::TransferService do
end
context 'when the user does not have the right policies' do
let!(:group_member) { create(:group_member, :guest, group: group, user: user) }
let_it_be(:group_member) { create(:group_member, :guest, group: group, user: user) }
it "returns false" do
expect(transfer_service.execute(nil)).to be_falsy
@ -128,7 +126,7 @@ RSpec.describe Groups::TransferService do
end
context 'when there is a group with the same path' do
let!(:group) { create(:group, :public, :nested, path: 'not-unique') }
let_it_be(:group) { create(:group, :public, :nested, path: 'not-unique') }
before do
create(:group, path: 'not-unique')
@ -145,9 +143,9 @@ RSpec.describe Groups::TransferService do
end
context 'when the group is a subgroup and the transfer is valid' do
let!(:subgroup1) { create(:group, :private, parent: group) }
let!(:subgroup2) { create(:group, :internal, parent: group) }
let!(:project1) { create(:project, :repository, :private, namespace: group) }
let_it_be(:subgroup1) { create(:group, :private, parent: group) }
let_it_be(:subgroup2) { create(:group, :internal, parent: group) }
let_it_be(:project1) { create(:project, :repository, :private, namespace: group) }
before do
transfer_service.execute(nil)
@ -173,12 +171,12 @@ RSpec.describe Groups::TransferService do
end
context 'when transferring a subgroup into another group' do
let(:group) { create(:group, :public, :nested) }
let_it_be_with_reload(:group) { create(:group, :public, :nested) }
it_behaves_like 'ensuring allowed transfer for a group'
context 'when the new parent group is the same as the previous parent group' do
let(:group) { create(:group, :public, :nested, parent: new_parent_group) }
let_it_be(:group) { create(:group, :public, :nested, parent: new_parent_group) }
it 'returns false' do
expect(transfer_service.execute(new_parent_group)).to be_falsy
@ -191,7 +189,7 @@ RSpec.describe Groups::TransferService do
end
context 'when the user does not have the right policies' do
let!(:group_member) { create(:group_member, :guest, group: group, user: user) }
let_it_be(:group_member) { create(:group_member, :guest, group: group, user: user) }
it "returns false" do
expect(transfer_service.execute(new_parent_group)).to be_falsy
@ -221,7 +219,7 @@ RSpec.describe Groups::TransferService do
end
context 'when the parent group has a project with the same path' do
let!(:group) { create(:group, :public, :nested, path: 'foo') }
let_it_be_with_reload(:group) { create(:group, :public, :nested, path: 'foo') }
before do
create(:group_member, :owner, group: new_parent_group, user: user)
@ -240,8 +238,13 @@ RSpec.describe Groups::TransferService do
end
context 'when the group is allowed to be transferred' do
let_it_be(:new_parent_group_integration) { create(:slack_service, group: new_parent_group, project: nil, webhook: 'http://new-group.slack.com') }
before do
allow(PropagateIntegrationWorker).to receive(:perform_async)
create(:group_member, :owner, group: new_parent_group, user: user)
transfer_service.execute(new_parent_group)
end
@ -267,6 +270,30 @@ RSpec.describe Groups::TransferService do
end
end
context 'with a group integration' do
let_it_be(:instance_integration) { create(:slack_service, :instance, webhook: 'http://project.slack.com') }
let(:new_created_integration) { Service.find_by(group: group) }
context 'with an inherited integration' do
let_it_be(:group_integration) { create(:slack_service, group: group, project: nil, webhook: 'http://group.slack.com', inherit_from_id: instance_integration.id) }
it 'replaces inherited integrations', :aggregate_failures do
expect(new_created_integration.webhook).to eq(new_parent_group_integration.webhook)
expect(PropagateIntegrationWorker).to have_received(:perform_async).with(new_created_integration.id)
expect(Service.count).to eq(3)
end
end
context 'with a custom integration' do
let_it_be(:group_integration) { create(:slack_service, group: group, project: nil, webhook: 'http://group.slack.com') }
it 'does not updates the integrations', :aggregate_failures do
expect { transfer_service.execute(new_parent_group) }.not_to change { group_integration.webhook }
expect(PropagateIntegrationWorker).not_to have_received(:perform_async)
end
end
end
it 'updates visibility for the group based on the parent group' do
expect(group.visibility_level).to eq(new_parent_group.visibility_level)
end
@ -464,7 +491,7 @@ RSpec.describe Groups::TransferService do
end
context 'updated paths' do
let(:group) { create(:group, :public) }
let_it_be_with_reload(:group) { create(:group, :public) }
before do
transfer_service.execute(new_parent_group)
@ -500,10 +527,10 @@ RSpec.describe Groups::TransferService do
end
context 'resets project authorizations' do
let(:old_parent_group) { create(:group) }
let(:group) { create(:group, :private, parent: old_parent_group) }
let(:new_group_member) { create(:user) }
let(:old_group_member) { create(:user) }
let_it_be(:old_parent_group) { create(:group) }
let_it_be_with_reload(:group) { create(:group, :private, parent: old_parent_group) }
let_it_be(:new_group_member) { create(:user) }
let_it_be(:old_group_member) { create(:user) }
before do
new_parent_group.add_maintainer(new_group_member)

View File

@ -7,6 +7,7 @@ RSpec.describe Projects::TransferService do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:group_integration) { create(:slack_service, group: group, project: nil, webhook: 'http://group.slack.com') }
let(:project) { create(:project, :repository, :legacy_storage, namespace: user.namespace) }
subject(:execute_transfer) { described_class.new(project, user).execute(group).tap { project.reload } }
@ -117,6 +118,30 @@ RSpec.describe Projects::TransferService do
shard_name: project.repository_storage
)
end
context 'with a project integration' do
let_it_be_with_reload(:project) { create(:project, namespace: user.namespace) }
let_it_be(:instance_integration) { create(:slack_service, :instance, webhook: 'http://project.slack.com') }
context 'with an inherited integration' do
let_it_be(:project_integration) { create(:slack_service, project: project, webhook: 'http://project.slack.com', inherit_from_id: instance_integration.id) }
it 'replaces inherited integrations', :aggregate_failures do
execute_transfer
expect(project.slack_service.webhook).to eq(group_integration.webhook)
expect(Service.count).to eq(3)
end
end
context 'with a custom integration' do
let_it_be(:project_integration) { create(:slack_service, project: project, webhook: 'http://project.slack.com') }
it 'does not updates the integrations' do
expect { execute_transfer }.not_to change { project.slack_service.webhook }
end
end
end
end
context 'when transfer fails' do
@ -527,7 +552,7 @@ RSpec.describe Projects::TransferService do
group.add_owner(user)
end
it 'schedules a job when pages are deployed' do
it 'schedules a job when pages are deployed' do
project.mark_pages_as_deployed
expect(PagesTransferWorker).to receive(:perform_async)