Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
3256c55b0f
commit
c658e2d292
|
@ -164,21 +164,6 @@ Layout/HashAlignment:
|
|||
- 'app/models/user_status.rb'
|
||||
- 'app/models/wiki.rb'
|
||||
- 'app/models/work_items/type.rb'
|
||||
- 'db/migrate/20210601080039_group_protected_environments_add_index_and_constraint.rb'
|
||||
- 'db/migrate/20210804150320_create_base_work_item_types.rb'
|
||||
- 'db/migrate/20210831203408_upsert_base_work_item_types.rb'
|
||||
- 'db/migrate/20210901065504_add_index_on_name_and_id_to_public_groups.rb'
|
||||
- 'db/post_migrate/20210311120156_backfill_push_event_payload_event_id_for_bigint_conversion.rb'
|
||||
- 'db/post_migrate/20210622045705_finalize_events_bigint_conversion.rb'
|
||||
- 'db/post_migrate/20210701141346_finalize_ci_builds_stage_id_bigint_conversion.rb'
|
||||
- 'db/post_migrate/20210707210916_finalize_ci_stages_bigint_conversion.rb'
|
||||
- 'db/post_migrate/20210708011426_finalize_ci_builds_metadata_bigint_conversion.rb'
|
||||
- 'db/post_migrate/20210802043253_finalize_push_event_payloads_bigint_conversion_3.rb'
|
||||
- 'db/post_migrate/20210804151444_prepare_indexes_for_ci_job_artifact_bigint_conversion.rb'
|
||||
- 'db/post_migrate/20210804153307_prepare_indexes_for_tagging_bigint_conversion.rb'
|
||||
- 'db/post_migrate/20210804154407_prepare_indexes_for_ci_stage_bigint_conversion.rb'
|
||||
- 'db/post_migrate/20210817024335_prepare_indexes_for_events_bigint_conversion.rb'
|
||||
- 'db/post_migrate/20210824174615_prepare_ci_builds_metadata_and_ci_build_async_indexes.rb'
|
||||
- 'ee/app/controllers/ee/search_controller.rb'
|
||||
- 'ee/app/controllers/projects/integrations/zentao/issues_controller.rb'
|
||||
- 'ee/app/graphql/mutations/iterations/cadences/create.rb'
|
||||
|
|
|
@ -1 +1 @@
|
|||
157e6b6ad8fd7aa0ebdd43727f00b81f34b100a1
|
||||
0a03f045e065b9e7a157322fbe486e1f02fd8617
|
||||
|
|
|
@ -285,7 +285,7 @@ export default {
|
|||
:shape="$options.AVATAR_SHAPE_OPTION_RECT"
|
||||
/>
|
||||
<div>
|
||||
<div data-testid="project-name">{{ project.name }}</div>
|
||||
<div data-testid="project-name" data-qa-selector="project_name">{{ project.name }}</div>
|
||||
<div class="gl-text-gray-500" data-testid="project-full-path">
|
||||
{{ project.fullPath }}
|
||||
</div>
|
||||
|
|
|
@ -7,6 +7,7 @@ import { loadingIconForLegacyJS } from '~/loading_icon_for_legacy_js';
|
|||
import { s__, __, sprintf } from '~/locale';
|
||||
import { isUserBusy } from '~/set_status_modal/utils';
|
||||
import SidebarMediator from '~/sidebar/sidebar_mediator';
|
||||
import { state } from '~/sidebar/components/reviewers/sidebar_reviewers.vue';
|
||||
import AjaxCache from './lib/utils/ajax_cache';
|
||||
import { spriteIcon } from './lib/utils/common_utils';
|
||||
import { parsePikadayDate } from './lib/utils/datetime_utility';
|
||||
|
@ -352,8 +353,7 @@ class GfmAutoComplete {
|
|||
// Cache assignees & reviewers list for easier filtering later
|
||||
assignees =
|
||||
SidebarMediator.singleton?.store?.assignees?.map(createMemberSearchString) || [];
|
||||
reviewers =
|
||||
SidebarMediator.singleton?.store?.reviewers?.map(createMemberSearchString) || [];
|
||||
reviewers = state.issuable?.reviewers?.nodes?.map(createMemberSearchString) || [];
|
||||
|
||||
const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
|
||||
return match && match.length ? match[1] : null;
|
||||
|
|
|
@ -133,7 +133,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
workItemsEnabled() {
|
||||
return this.glFeatures.workItems;
|
||||
return this.glFeatures.workItemsCreateFromMarkdown;
|
||||
},
|
||||
taskWorkItemType() {
|
||||
return this.workItemTypes.find((type) => type.name === TASK_TYPE_NAME)?.id;
|
||||
|
|
|
@ -256,6 +256,7 @@ export default {
|
|||
:error-message="i18n.branchesErrorMessage"
|
||||
:show-header="showSectionHeaders"
|
||||
data-testid="branches-section"
|
||||
data-qa-selector="branches_section"
|
||||
@selected="selectRef($event)"
|
||||
/>
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ export default {
|
|||
return this.users.length > 2;
|
||||
},
|
||||
allReviewersCanMerge() {
|
||||
return this.users.every((user) => user.can_merge);
|
||||
return this.users.every((user) => user.mergeRequestInteraction?.canMerge);
|
||||
},
|
||||
sidebarAvatarCounter() {
|
||||
if (this.users.length > DEFAULT_MAX_COUNTER) {
|
||||
|
@ -48,7 +48,7 @@ export default {
|
|||
return this.users.slice(0, collapsedLength);
|
||||
},
|
||||
tooltipTitleMergeStatus() {
|
||||
const mergeLength = this.users.filter((u) => u.can_merge).length;
|
||||
const mergeLength = this.users.filter((u) => u.mergeRequestInteraction?.canMerge).length;
|
||||
|
||||
if (mergeLength === this.users.length) {
|
||||
return '';
|
||||
|
|
|
@ -23,10 +23,10 @@ export default {
|
|||
return sprintf(__("%{userName}'s avatar"), { userName: this.user.name });
|
||||
},
|
||||
avatarUrl() {
|
||||
return this.user.avatar || this.user.avatar_url || gon.default_avatar_url;
|
||||
return this.user.avatarUrl || this.user.avatar_url || gon.default_avatar_url;
|
||||
},
|
||||
hasMergeIcon() {
|
||||
return !this.user.can_merge;
|
||||
return !this.user.mergeRequestInteraction?.canMerge;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -40,7 +40,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
cannotMerge() {
|
||||
return this.issuableType === 'merge_request' && !this.user.can_merge;
|
||||
return this.issuableType === 'merge_request' && !this.user.mergeRequestInteraction?.canMerge;
|
||||
},
|
||||
tooltipTitle() {
|
||||
if (this.cannotMerge && this.tooltipHasName) {
|
||||
|
@ -59,7 +59,7 @@ export default {
|
|||
};
|
||||
},
|
||||
reviewerUrl() {
|
||||
return this.user.web_url;
|
||||
return this.user.webUrl;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -36,8 +36,8 @@ export default {
|
|||
return !this.users.length;
|
||||
},
|
||||
sortedReviewers() {
|
||||
const canMergeUsers = this.users.filter((user) => user.can_merge);
|
||||
const canNotMergeUsers = this.users.filter((user) => !user.can_merge);
|
||||
const canMergeUsers = this.users.filter((user) => user.mergeRequestInteraction?.canMerge);
|
||||
const canNotMergeUsers = this.users.filter((user) => !user.mergeRequestInteraction?.canMerge);
|
||||
|
||||
return [...canMergeUsers, ...canNotMergeUsers];
|
||||
},
|
||||
|
|
|
@ -1,15 +1,23 @@
|
|||
<script>
|
||||
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
|
||||
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
|
||||
import Vue from 'vue';
|
||||
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
|
||||
import createFlash from '~/flash';
|
||||
import { __ } from '~/locale';
|
||||
import eventHub from '~/sidebar/event_hub';
|
||||
import Store from '~/sidebar/stores/sidebar_store';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import getMergeRequestReviewersQuery from '~/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql';
|
||||
import ReviewerTitle from './reviewer_title.vue';
|
||||
import Reviewers from './reviewers.vue';
|
||||
|
||||
export const state = Vue.observable({
|
||||
issuable: {},
|
||||
loading: false,
|
||||
initialLoading: true,
|
||||
});
|
||||
|
||||
export default {
|
||||
name: 'SidebarReviewers',
|
||||
components: {
|
||||
|
@ -40,18 +48,49 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
issuable: {
|
||||
query: getMergeRequestReviewersQuery,
|
||||
variables() {
|
||||
return {
|
||||
iid: this.issuableIid,
|
||||
fullPath: this.projectPath,
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
return data.workspace?.issuable;
|
||||
},
|
||||
result() {
|
||||
this.initialLoading = false;
|
||||
},
|
||||
error() {
|
||||
createFlash({ message: __('An error occurred while fetching reviewers.') });
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
store: new Store(),
|
||||
loading: false,
|
||||
};
|
||||
return state;
|
||||
},
|
||||
computed: {
|
||||
relativeUrlRoot() {
|
||||
return gon.relative_url_root ?? '';
|
||||
},
|
||||
reviewers() {
|
||||
return this.issuable.reviewers?.nodes || [];
|
||||
},
|
||||
graphqlFetching() {
|
||||
return this.$apollo.queries.issuable.loading;
|
||||
},
|
||||
isLoading() {
|
||||
return this.loading || this.$apollo.queries.issuable.loading;
|
||||
},
|
||||
canUpdate() {
|
||||
return this.issuable.userPermissions?.updateMergeRequest || false;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.store = new Store();
|
||||
|
||||
this.removeReviewer = this.store.removeReviewer.bind(this.store);
|
||||
this.addReviewer = this.store.addReviewer.bind(this.store);
|
||||
this.removeAllReviewers = this.store.removeAllReviewers.bind(this.store);
|
||||
|
@ -77,6 +116,7 @@ export default {
|
|||
.then(() => {
|
||||
this.loading = false;
|
||||
refreshUserMergeRequestCounts();
|
||||
this.$apollo.queries.issuable.refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
this.loading = false;
|
||||
|
@ -95,15 +135,15 @@ export default {
|
|||
<template>
|
||||
<div>
|
||||
<reviewer-title
|
||||
:number-of-reviewers="store.reviewers.length"
|
||||
:loading="loading || store.isFetching.reviewers"
|
||||
:editable="store.editable"
|
||||
:number-of-reviewers="reviewers.length"
|
||||
:loading="isLoading"
|
||||
:editable="canUpdate"
|
||||
/>
|
||||
<reviewers
|
||||
v-if="!store.isFetching.reviewers"
|
||||
v-if="!initialLoading"
|
||||
:root-path="relativeUrlRoot"
|
||||
:users="store.reviewers"
|
||||
:editable="store.editable"
|
||||
:users="reviewers"
|
||||
:editable="canUpdate"
|
||||
:issuable-type="issuableType"
|
||||
@request-review="requestReview"
|
||||
/>
|
||||
|
|
|
@ -105,7 +105,7 @@ export default {
|
|||
</div>
|
||||
</reviewer-avatar-link>
|
||||
<gl-icon
|
||||
v-if="user.approved"
|
||||
v-if="user.mergeRequestInteraction.approved"
|
||||
v-gl-tooltip.left
|
||||
:size="16"
|
||||
:title="approvedByTooltipTitle(user)"
|
||||
|
@ -121,7 +121,7 @@ export default {
|
|||
data-testid="re-request-success"
|
||||
/>
|
||||
<gl-button
|
||||
v-else-if="user.can_update_merge_request && user.reviewed"
|
||||
v-else-if="user.mergeRequestInteraction.canUpdate && user.mergeRequestInteraction.reviewed"
|
||||
v-gl-tooltip.left
|
||||
:title="$options.i18n.reRequestReview"
|
||||
:aria-label="$options.i18n.reRequestReview"
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
#import "~/graphql_shared/fragments/user.fragment.graphql"
|
||||
#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
|
||||
|
||||
query mergeRequestReviewers($fullPath: ID!, $iid: String!) {
|
||||
workspace: project(fullPath: $fullPath) {
|
||||
__typename
|
||||
id
|
||||
issuable: mergeRequest(iid: $iid) {
|
||||
__typename
|
||||
id
|
||||
reviewers {
|
||||
nodes {
|
||||
...User
|
||||
...UserAvailability
|
||||
mergeRequestInteraction {
|
||||
canMerge
|
||||
canUpdate
|
||||
approved
|
||||
reviewed
|
||||
}
|
||||
}
|
||||
}
|
||||
userPermissions {
|
||||
updateMergeRequest
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -95,8 +95,14 @@ export default {
|
|||
toggle() {
|
||||
this.isOpen = !this.isOpen;
|
||||
},
|
||||
toggleAddForm() {
|
||||
this.isShownAddForm = !this.isShownAddForm;
|
||||
showAddForm() {
|
||||
this.isShownAddForm = true;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.wiLinksForm.$refs.wiTitleInput?.$el.focus();
|
||||
});
|
||||
},
|
||||
hideAddForm() {
|
||||
this.isShownAddForm = false;
|
||||
},
|
||||
addChild(child) {
|
||||
this.children = [child, ...this.children];
|
||||
|
@ -122,10 +128,10 @@ export default {
|
|||
>
|
||||
<h5 class="gl-m-0 gl-line-height-32 gl-flex-grow-1">{{ $options.i18n.title }}</h5>
|
||||
<gl-button
|
||||
v-if="!isShownAddForm && canUpdate"
|
||||
v-if="canUpdate"
|
||||
category="secondary"
|
||||
data-testid="toggle-add-form"
|
||||
@click="toggleAddForm"
|
||||
@click="showAddForm"
|
||||
>
|
||||
{{ $options.i18n.addChildButtonLabel }}
|
||||
</gl-button>
|
||||
|
@ -154,10 +160,11 @@ export default {
|
|||
</div>
|
||||
<work-item-links-form
|
||||
v-if="isShownAddForm"
|
||||
ref="wiLinksForm"
|
||||
data-testid="add-links-form"
|
||||
:issuable-gid="issuableGid"
|
||||
:children-ids="childrenIds"
|
||||
@cancel="toggleAddForm"
|
||||
@cancel="hideAddForm"
|
||||
@addWorkItemChild="addChild"
|
||||
/>
|
||||
<div
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
<script>
|
||||
import { GlAlert, GlForm, GlFormCombobox, GlButton } from '@gitlab/ui';
|
||||
import { GlAlert, GlFormGroup, GlForm, GlFormCombobox, GlButton, GlFormInput } from '@gitlab/ui';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { __, s__ } from '~/locale';
|
||||
import projectWorkItemsQuery from '../../graphql/project_work_items.query.graphql';
|
||||
import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql';
|
||||
import createWorkItemMutation from '../../graphql/create_work_item.mutation.graphql';
|
||||
import { WORK_ITEM_TYPE_IDS } from '../../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -11,6 +13,8 @@ export default {
|
|||
GlForm,
|
||||
GlFormCombobox,
|
||||
GlButton,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
},
|
||||
inject: ['projectPath'],
|
||||
props: {
|
||||
|
@ -28,6 +32,7 @@ export default {
|
|||
apollo: {
|
||||
availableWorkItems: {
|
||||
query: projectWorkItemsQuery,
|
||||
debounce: 200,
|
||||
variables() {
|
||||
return {
|
||||
projectPath: this.projectPath,
|
||||
|
@ -50,8 +55,29 @@ export default {
|
|||
availableWorkItems: [],
|
||||
search: '',
|
||||
error: null,
|
||||
childToCreateTitle: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
actionsList() {
|
||||
return [
|
||||
{
|
||||
label: this.$options.i18n.createChildOptionLabel,
|
||||
fn: () => {
|
||||
this.childToCreateTitle = this.search?.title || this.search;
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
addOrCreateButtonLabel() {
|
||||
return this.childToCreateTitle
|
||||
? this.$options.i18n.createChildOptionLabel
|
||||
: this.$options.i18n.addTaskButtonLabel;
|
||||
},
|
||||
addOrCreateMethod() {
|
||||
return this.childToCreateTitle ? this.createChild : this.addChild;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getIdFromGraphQLId,
|
||||
unsetError() {
|
||||
|
@ -79,35 +105,77 @@ export default {
|
|||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.error = this.$options.i18n.errorMessage;
|
||||
this.error = this.$options.i18n.addChildErrorMessage;
|
||||
})
|
||||
.finally(() => {
|
||||
this.search = '';
|
||||
});
|
||||
},
|
||||
createChild() {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: createWorkItemMutation,
|
||||
variables: {
|
||||
input: {
|
||||
title: this.search?.title || this.search,
|
||||
projectPath: this.projectPath,
|
||||
workItemTypeId: WORK_ITEM_TYPE_IDS.TASK,
|
||||
hierarchyWidget: {
|
||||
parentId: this.issuableGid,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(({ data }) => {
|
||||
if (data.workItemCreate?.errors?.length) {
|
||||
[this.error] = data.workItemCreate.errors;
|
||||
} else {
|
||||
this.unsetError();
|
||||
this.$emit('addWorkItemChild', data.workItemCreate.workItem);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.error = this.$options.i18n.createChildErrorMessage;
|
||||
})
|
||||
.finally(() => {
|
||||
this.search = '';
|
||||
this.childToCreateTitle = null;
|
||||
});
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
inputLabel: __('Children'),
|
||||
errorMessage: s__(
|
||||
inputLabel: __('Title'),
|
||||
addTaskButtonLabel: s__('WorkItem|Add task'),
|
||||
addChildErrorMessage: s__(
|
||||
'WorkItem|Something went wrong when trying to add a child. Please try again.',
|
||||
),
|
||||
createChildOptionLabel: s__('WorkItem|Create task'),
|
||||
createChildErrorMessage: s__(
|
||||
'WorkItem|Something went wrong when trying to create a child. Please try again.',
|
||||
),
|
||||
placeholder: s__('WorkItem|Add a title'),
|
||||
fieldValidationMessage: __('Maximum of 255 characters'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-form
|
||||
class="gl-mb-3 gl-bg-white gl-mb-3 gl-py-3 gl-px-4 gl-border gl-border-gray-100 gl-rounded-base"
|
||||
class="gl-bg-white gl-mb-3 gl-p-4 gl-border gl-border-gray-100 gl-rounded-base"
|
||||
@submit.prevent="createChild"
|
||||
>
|
||||
<gl-alert v-if="error" variant="danger" class="gl-mb-3" @dismiss="unsetError">
|
||||
{{ error }}
|
||||
</gl-alert>
|
||||
<!-- Follow up issue to turn this functionality back on https://gitlab.com/gitlab-org/gitlab/-/issues/368757 -->
|
||||
<gl-form-combobox
|
||||
v-if="false"
|
||||
v-model="search"
|
||||
:token-list="availableWorkItems"
|
||||
match-value-to-attr="title"
|
||||
class="gl-mb-4"
|
||||
:label-text="$options.i18n.inputLabel"
|
||||
:action-list="actionsList"
|
||||
label-sr-only
|
||||
autofocus
|
||||
>
|
||||
|
@ -117,11 +185,34 @@ export default {
|
|||
<div>{{ item.title }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #action="{ item }">
|
||||
<span class="gl-text-blue-500">{{ item.label }}</span>
|
||||
</template>
|
||||
</gl-form-combobox>
|
||||
<gl-button category="secondary" data-testid="add-child-button" @click="addChild">
|
||||
{{ s__('WorkItem|Add task') }}
|
||||
<gl-form-group
|
||||
:label="$options.i18n.inputLabel"
|
||||
:description="$options.i18n.fieldValidationMessage"
|
||||
>
|
||||
<gl-form-input
|
||||
ref="wiTitleInput"
|
||||
v-model="search"
|
||||
:placeholder="$options.i18n.placeholder"
|
||||
maxlength="255"
|
||||
class="gl-mb-3"
|
||||
autofocus
|
||||
/>
|
||||
</gl-form-group>
|
||||
<gl-button
|
||||
category="primary"
|
||||
variant="confirm"
|
||||
size="small"
|
||||
type="submit"
|
||||
:disabled="search.length === 0"
|
||||
data-testid="add-child-button"
|
||||
>
|
||||
{{ $options.i18n.createChildOptionLabel }}
|
||||
</gl-button>
|
||||
<gl-button category="tertiary" @click="$emit('cancel')">
|
||||
<gl-button category="secondary" size="small" @click="$emit('cancel')">
|
||||
{{ s__('WorkItem|Cancel') }}
|
||||
</gl-button>
|
||||
</gl-form>
|
||||
|
|
|
@ -35,3 +35,7 @@ export const WORK_ITEM_STATUS_TEXT = {
|
|||
CLOSED: s__('WorkItem|Closed'),
|
||||
OPEN: s__('WorkItem|Open'),
|
||||
};
|
||||
|
||||
export const WORK_ITEM_TYPE_IDS = {
|
||||
TASK: 'gid://gitlab/WorkItems::Type/5',
|
||||
};
|
||||
|
|
|
@ -5,5 +5,6 @@ mutation createWorkItem($input: WorkItemCreateInput!) {
|
|||
workItem {
|
||||
...WorkItem
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
push_frontend_feature_flag(:realtime_labels, project)
|
||||
push_force_frontend_feature_flag(:work_items_mvc_2, project&.work_items_mvc_2_feature_flag_enabled?)
|
||||
push_frontend_feature_flag(:work_items_hierarchy, project)
|
||||
push_force_frontend_feature_flag(:work_items_create_from_markdown, project&.work_items_create_from_markdown_feature_flag_enabled?)
|
||||
end
|
||||
|
||||
around_action :allow_gitaly_ref_name_caching, only: [:discussions]
|
||||
|
|
|
@ -859,6 +859,10 @@ class Group < Namespace
|
|||
feature_flag_enabled_for_self_or_ancestor?(:work_items_mvc_2)
|
||||
end
|
||||
|
||||
def work_items_create_from_markdown_feature_flag_enabled?
|
||||
feature_flag_enabled_for_self_or_ancestor?(:work_items_create_from_markdown)
|
||||
end
|
||||
|
||||
# Check for enabled features, similar to `Project#feature_available?`
|
||||
# NOTE: We still want to keep this after removing `Namespace#feature_available?`.
|
||||
override :feature_available?
|
||||
|
|
|
@ -2995,6 +2995,10 @@ class Project < ApplicationRecord
|
|||
group&.work_items_mvc_2_feature_flag_enabled? || Feature.enabled?(:work_items_mvc_2)
|
||||
end
|
||||
|
||||
def work_items_create_from_markdown_feature_flag_enabled?
|
||||
work_items_feature_flag_enabled? && (group&.work_items_create_from_markdown_feature_flag_enabled? || Feature.enabled?(:work_items_create_from_markdown))
|
||||
end
|
||||
|
||||
def enqueue_record_project_target_platforms
|
||||
return unless Gitlab.com?
|
||||
return unless Feature.enabled?(:record_projects_target_platforms, self)
|
||||
|
|
|
@ -34,8 +34,8 @@ class EnvironmentSerializer < BaseSerializer
|
|||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def itemize(resource)
|
||||
items = resource.order('folder ASC')
|
||||
.group('COALESCE(environment_type, name)')
|
||||
.select('COALESCE(environment_type, name) AS folder',
|
||||
.group('COALESCE(environment_type, id::text)', 'COALESCE(environment_type, name)')
|
||||
.select('COALESCE(environment_type, id::text), COALESCE(environment_type, name) AS folder',
|
||||
'COUNT(*) AS size', 'MAX(id) AS last_id')
|
||||
|
||||
# It makes a difference when you call `paginate` method, because
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: work_items_create_from_markdown
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
milestone: '15.3'
|
||||
type: development
|
||||
group: group::project management
|
||||
default_enabled: false
|
|
@ -9,7 +9,7 @@ class GroupProtectedEnvironmentsAddIndexAndConstraint < ActiveRecord::Migration[
|
|||
|
||||
def up
|
||||
add_concurrent_index :protected_environments, [:group_id, :name], unique: true,
|
||||
name: INDEX_NAME, where: 'group_id IS NOT NULL'
|
||||
name: INDEX_NAME, where: 'group_id IS NOT NULL'
|
||||
add_concurrent_foreign_key :protected_environments, :namespaces, column: :group_id, on_delete: :cascade
|
||||
|
||||
add_check_constraint :protected_environments,
|
||||
|
|
|
@ -8,9 +8,9 @@ class CreateBaseWorkItemTypes < ActiveRecord::Migration[6.1]
|
|||
self.table_name = 'work_item_types'
|
||||
|
||||
enum base_type: {
|
||||
issue: 0,
|
||||
incident: 1,
|
||||
test_case: 2,
|
||||
issue: 0,
|
||||
incident: 1,
|
||||
test_case: 2,
|
||||
requirement: 3
|
||||
}
|
||||
|
||||
|
|
|
@ -6,9 +6,9 @@ class UpsertBaseWorkItemTypes < ActiveRecord::Migration[6.1]
|
|||
self.table_name = 'work_item_types'
|
||||
|
||||
enum base_type: {
|
||||
issue: 0,
|
||||
incident: 1,
|
||||
test_case: 2,
|
||||
issue: 0,
|
||||
incident: 1,
|
||||
test_case: 2,
|
||||
requirement: 3
|
||||
}
|
||||
end
|
||||
|
|
|
@ -7,8 +7,9 @@ class AddIndexOnNameAndIdToPublicGroups < Gitlab::Database::Migration[1.0]
|
|||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :namespaces, [:name, :id], name: INDEX_NAME,
|
||||
where: "type = 'Group' AND visibility_level = #{PUBLIC_VISIBILITY_LEVEL}"
|
||||
add_concurrent_index :namespaces, [:name, :id],
|
||||
name: INDEX_NAME,
|
||||
where: "type = 'Group' AND visibility_level = #{PUBLIC_VISIBILITY_LEVEL}"
|
||||
end
|
||||
|
||||
def down
|
||||
|
|
|
@ -11,7 +11,7 @@ class BackfillPushEventPayloadEventIdForBigintConversion < ActiveRecord::Migrati
|
|||
return unless should_run?
|
||||
|
||||
backfill_conversion_of_integer_to_bigint :push_event_payloads, :event_id, primary_key: :event_id,
|
||||
batch_size: 15000, sub_batch_size: 100
|
||||
batch_size: 15000, sub_batch_size: 100
|
||||
end
|
||||
|
||||
def down
|
||||
|
|
|
@ -31,7 +31,7 @@ class FinalizeEventsBigintConversion < ActiveRecord::Migration[6.1]
|
|||
add_concurrent_index TABLE_NAME, [:project_id, :id_convert_to_bigint], name: 'index_events_on_project_id_and_id_convert_to_bigint'
|
||||
# This is to replace the existing "index_events_on_project_id_and_id_desc_on_merged_action" btree (project_id, id DESC) WHERE action = 7
|
||||
add_concurrent_index TABLE_NAME, [:project_id, :id_convert_to_bigint], order: { id_convert_to_bigint: :desc },
|
||||
where: "action = 7", name: 'index_events_on_project_id_and_id_bigint_desc_on_merged_action'
|
||||
where: "action = 7", name: 'index_events_on_project_id_and_id_bigint_desc_on_merged_action'
|
||||
|
||||
# Add a FK on `push_event_payloads(event_id)` to `id_convert_to_bigint`, the old FK (fk_36c74129da)
|
||||
# will be removed when events_pkey constraint is droppped.
|
||||
|
|
|
@ -30,7 +30,7 @@ class FinalizeCiBuildsStageIdBigintConversion < ActiveRecord::Migration[6.1]
|
|||
|
||||
# Create a copy of the original column's FK on the new column
|
||||
add_concurrent_foreign_key TABLE_NAME, :ci_stages, column: :stage_id_convert_to_bigint, on_delete: :cascade,
|
||||
reverse_lock_order: true
|
||||
reverse_lock_order: true
|
||||
|
||||
with_lock_retries(raise_on_exhaustion: true) do
|
||||
quoted_table_name = quote_table_name(TABLE_NAME)
|
||||
|
|
|
@ -37,10 +37,10 @@ class FinalizeCiStagesBigintConversion < ActiveRecord::Migration[6.1]
|
|||
fk_stage_id = concurrent_foreign_key_name(:ci_builds, :stage_id)
|
||||
fk_stage_id_tmp = "#{fk_stage_id}_tmp"
|
||||
add_concurrent_foreign_key :ci_builds, :ci_stages, column: :stage_id,
|
||||
target_column: :id_convert_to_bigint,
|
||||
name: fk_stage_id_tmp,
|
||||
on_delete: :cascade,
|
||||
reverse_lock_order: true
|
||||
target_column: :id_convert_to_bigint,
|
||||
name: fk_stage_id_tmp,
|
||||
on_delete: :cascade,
|
||||
reverse_lock_order: true
|
||||
|
||||
# Now it's time to do things in a transaction
|
||||
with_lock_retries(raise_on_exhaustion: true) do
|
||||
|
|
|
@ -40,7 +40,7 @@ class FinalizeCiBuildsMetadataBigintConversion < Gitlab::Database::Migration[1.0
|
|||
# rubocop:enable Migration/PreventIndexCreation
|
||||
|
||||
add_concurrent_foreign_key TABLE_NAME, :ci_builds, column: :build_id_convert_to_bigint, on_delete: :cascade,
|
||||
reverse_lock_order: true
|
||||
reverse_lock_order: true
|
||||
|
||||
with_lock_retries(raise_on_exhaustion: true) do
|
||||
execute "LOCK TABLE ci_builds, #{TABLE_NAME} IN ACCESS EXCLUSIVE MODE"
|
||||
|
|
|
@ -40,7 +40,7 @@ class FinalizePushEventPayloadsBigintConversion3 < ActiveRecord::Migration[6.1]
|
|||
|
||||
# Add a foreign key on `event_id_convert_to_bigint` before we swap the columns and drop the old FK (fk_36c74129da)
|
||||
add_concurrent_foreign_key TABLE_NAME, :events, column: :event_id_convert_to_bigint,
|
||||
on_delete: :cascade, reverse_lock_order: true
|
||||
on_delete: :cascade, reverse_lock_order: true
|
||||
|
||||
with_lock_retries(raise_on_exhaustion: true) do
|
||||
# We'll need ACCESS EXCLUSIVE lock on the related tables,
|
||||
|
|
|
@ -5,19 +5,23 @@ class PrepareIndexesForCiJobArtifactBigintConversion < ActiveRecord::Migration[6
|
|||
|
||||
def up
|
||||
prepare_async_index :ci_job_artifacts, :id_convert_to_bigint, unique: true,
|
||||
name: :index_ci_job_artifact_on_id_convert_to_bigint
|
||||
name: :index_ci_job_artifact_on_id_convert_to_bigint
|
||||
|
||||
prepare_async_index :ci_job_artifacts, [:project_id, :id_convert_to_bigint], where: 'file_type = 18',
|
||||
name: :index_ci_job_artifacts_for_terraform_reports_bigint
|
||||
prepare_async_index :ci_job_artifacts,
|
||||
[:project_id, :id_convert_to_bigint],
|
||||
where: 'file_type = 18', name: :index_ci_job_artifacts_for_terraform_reports_bigint
|
||||
|
||||
prepare_async_index :ci_job_artifacts, :id_convert_to_bigint, where: 'file_type = 18',
|
||||
name: :index_ci_job_artifacts_id_for_terraform_reports_bigint
|
||||
prepare_async_index :ci_job_artifacts, :id_convert_to_bigint,
|
||||
where: 'file_type = 18',
|
||||
name: :index_ci_job_artifacts_id_for_terraform_reports_bigint
|
||||
|
||||
prepare_async_index :ci_job_artifacts, [:expire_at, :job_id_convert_to_bigint],
|
||||
name: :index_ci_job_artifacts_on_expire_at_and_job_id_bigint
|
||||
prepare_async_index :ci_job_artifacts,
|
||||
[:expire_at, :job_id_convert_to_bigint],
|
||||
name: :index_ci_job_artifacts_on_expire_at_and_job_id_bigint
|
||||
|
||||
prepare_async_index :ci_job_artifacts, [:job_id_convert_to_bigint, :file_type], unique: true,
|
||||
name: :index_ci_job_artifacts_on_job_id_and_file_type_bigint
|
||||
prepare_async_index :ci_job_artifacts,
|
||||
[:job_id_convert_to_bigint, :file_type],
|
||||
unique: true, name: :index_ci_job_artifacts_on_job_id_and_file_type_bigint
|
||||
end
|
||||
|
||||
def down
|
||||
|
|
|
@ -5,7 +5,7 @@ class PrepareIndexesForTaggingBigintConversion < ActiveRecord::Migration[6.1]
|
|||
|
||||
def up
|
||||
prepare_async_index :taggings, :id_convert_to_bigint, unique: true,
|
||||
name: :index_taggings_on_id_convert_to_bigint
|
||||
name: :index_taggings_on_id_convert_to_bigint
|
||||
|
||||
prepare_async_index :taggings, [:taggable_id_convert_to_bigint, :taggable_type],
|
||||
name: :i_taggings_on_taggable_id_convert_to_bigint_and_taggable_type
|
||||
|
|
|
@ -5,14 +5,16 @@ class PrepareIndexesForCiStageBigintConversion < ActiveRecord::Migration[6.1]
|
|||
|
||||
def up
|
||||
prepare_async_index :ci_stages, :id_convert_to_bigint, unique: true,
|
||||
name: :index_ci_stages_on_id_convert_to_bigint
|
||||
name: :index_ci_stages_on_id_convert_to_bigint
|
||||
|
||||
prepare_async_index :ci_stages, [:pipeline_id, :id_convert_to_bigint], where: 'status in (0, 1, 2, 8, 9, 10)',
|
||||
name: :index_ci_stages_on_pipeline_id_and_id_convert_to_bigint
|
||||
prepare_async_index :ci_stages, [:pipeline_id, :id_convert_to_bigint],
|
||||
where: 'status in (0, 1, 2, 8, 9, 10)',
|
||||
name: :index_ci_stages_on_pipeline_id_and_id_convert_to_bigint
|
||||
end
|
||||
|
||||
def down
|
||||
unprepare_async_index_by_name :ci_stages, :index_ci_stages_on_pipeline_id_and_id_convert_to_bigint
|
||||
unprepare_async_index_by_name :ci_stages,
|
||||
:index_ci_stages_on_pipeline_id_and_id_convert_to_bigint
|
||||
|
||||
unprepare_async_index_by_name :ci_stages, :index_ci_stages_on_id_convert_to_bigint
|
||||
end
|
||||
|
|
|
@ -7,13 +7,14 @@ class PrepareIndexesForEventsBigintConversion < ActiveRecord::Migration[6.1]
|
|||
|
||||
def up
|
||||
prepare_async_index TABLE_NAME, :id_convert_to_bigint, unique: true,
|
||||
name: :index_events_on_id_convert_to_bigint
|
||||
name: :index_events_on_id_convert_to_bigint
|
||||
|
||||
prepare_async_index TABLE_NAME, [:project_id, :id_convert_to_bigint],
|
||||
name: :index_events_on_project_id_and_id_convert_to_bigint
|
||||
|
||||
prepare_async_index TABLE_NAME, [:project_id, :id_convert_to_bigint], order: { id_convert_to_bigint: :desc },
|
||||
where: 'action = 7', name: :index_events_on_project_id_and_id_bigint_desc_on_merged_action
|
||||
prepare_async_index TABLE_NAME, [:project_id, :id_convert_to_bigint],
|
||||
order: { id_convert_to_bigint: :desc },
|
||||
where: 'action = 7', name: :index_events_on_project_id_and_id_bigint_desc_on_merged_action
|
||||
end
|
||||
|
||||
def down
|
||||
|
|
|
@ -5,13 +5,13 @@ class PrepareCiBuildsMetadataAndCiBuildAsyncIndexes < ActiveRecord::Migration[6.
|
|||
|
||||
def up
|
||||
prepare_async_index :ci_builds_metadata, :id_convert_to_bigint, unique: true,
|
||||
name: :index_ci_builds_metadata_on_id_convert_to_bigint
|
||||
name: :index_ci_builds_metadata_on_id_convert_to_bigint
|
||||
|
||||
prepare_async_index :ci_builds_metadata, :build_id_convert_to_bigint, unique: true,
|
||||
name: :index_ci_builds_metadata_on_build_id_convert_to_bigint
|
||||
name: :index_ci_builds_metadata_on_build_id_convert_to_bigint
|
||||
|
||||
prepare_async_index :ci_builds_metadata, :build_id_convert_to_bigint, where: 'has_exposed_artifacts IS TRUE',
|
||||
name: :index_ci_builds_metadata_on_build_id_int8_and_exposed_artifacts
|
||||
name: :index_ci_builds_metadata_on_build_id_int8_and_exposed_artifacts
|
||||
|
||||
prepare_async_index_from_sql(:ci_builds_metadata, :index_ci_builds_metadata_on_build_id_int8_where_interruptible, <<~SQL.squish)
|
||||
CREATE INDEX CONCURRENTLY "index_ci_builds_metadata_on_build_id_int8_where_interruptible"
|
||||
|
@ -20,7 +20,7 @@ class PrepareCiBuildsMetadataAndCiBuildAsyncIndexes < ActiveRecord::Migration[6.
|
|||
SQL
|
||||
|
||||
prepare_async_index :ci_builds, :id_convert_to_bigint, unique: true,
|
||||
name: :index_ci_builds_on_converted_id
|
||||
name: :index_ci_builds_on_converted_id
|
||||
end
|
||||
|
||||
def down
|
||||
|
|
|
@ -205,10 +205,10 @@ field populated.
|
|||
|
||||
### Timeline events
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/344059) in GitLab 15.2 [with a flag](../../administration/feature_flags.md) named `incident_timeline`. Enabled on GitLab.com. Disabled on self-managed.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/344059) in GitLab 15.2 [with a flag](../../administration/feature_flags.md) named `incident_timeline`. Enabled by default.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `incident_timeline`.
|
||||
On self-managed GitLab, by default this feature is available. To hide the feature, ask an administrator to [disable the feature flag](../../administration/feature_flags.md) named `incident_timeline`.
|
||||
On GitLab.com, this feature is available.
|
||||
|
||||
Incident timelines are an important part of record keeping for incidents.
|
||||
|
|
|
@ -28,25 +28,26 @@ module Gitlab
|
|||
private
|
||||
|
||||
def validate
|
||||
pgrp = nil
|
||||
pgrps = nil
|
||||
valid_archive = true
|
||||
|
||||
validate_archive_path
|
||||
|
||||
Timeout.timeout(TIMEOUT_LIMIT) do
|
||||
stdin, stdout, stderr, wait_thr = Open3.popen3(command, pgroup: true)
|
||||
stdin.close
|
||||
stderr_r, stderr_w = IO.pipe
|
||||
stdout, wait_threads = Open3.pipeline_r(*command, pgroup: true, err: stderr_w )
|
||||
|
||||
# When validation is performed on a small archive (e.g. 100 bytes)
|
||||
# `wait_thr` finishes before we can get process group id. Do not
|
||||
# raise exception in this scenario.
|
||||
pgrp = begin
|
||||
pgrps = wait_threads.map do |wait_thr|
|
||||
Process.getpgid(wait_thr[:pid])
|
||||
rescue Errno::ESRCH
|
||||
nil
|
||||
end
|
||||
pgrps.compact!
|
||||
|
||||
status = wait_thr.value
|
||||
status = wait_threads.last.value
|
||||
|
||||
if status.success?
|
||||
result = stdout.readline
|
||||
|
@ -64,20 +65,21 @@ module Gitlab
|
|||
|
||||
ensure
|
||||
stdout.close
|
||||
stderr.close
|
||||
stderr_w.close
|
||||
stderr_r.close
|
||||
end
|
||||
|
||||
valid_archive
|
||||
rescue Timeout::Error
|
||||
log_error('Timeout reached during archive decompression')
|
||||
|
||||
Process.kill(-1, pgrp) if pgrp
|
||||
pgrps.each { |pgrp| Process.kill(-1, pgrp) } if pgrps
|
||||
|
||||
false
|
||||
rescue StandardError => e
|
||||
log_error(e.message)
|
||||
|
||||
Process.kill(-1, pgrp) if pgrp
|
||||
pgrps.each { |pgrp| Process.kill(-1, pgrp) } if pgrps
|
||||
|
||||
false
|
||||
end
|
||||
|
@ -91,7 +93,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def command
|
||||
"gzip -dc #{@archive_path} | wc -c"
|
||||
[['gzip', '-dc', @archive_path], ['wc', '-c']]
|
||||
end
|
||||
|
||||
def log_error(error)
|
||||
|
|
|
@ -4101,6 +4101,9 @@ msgstr ""
|
|||
msgid "An error occurred while fetching reference"
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while fetching reviewers."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while fetching tags. Retry the search."
|
||||
msgstr ""
|
||||
|
||||
|
@ -7972,9 +7975,6 @@ msgstr ""
|
|||
msgid "Child issues and epics"
|
||||
msgstr ""
|
||||
|
||||
msgid "Children"
|
||||
msgstr ""
|
||||
|
||||
msgid "Chinese language support using"
|
||||
msgstr ""
|
||||
|
||||
|
@ -27438,6 +27438,9 @@ msgstr ""
|
|||
msgid "OperationsDashboard|Operations Dashboard"
|
||||
msgstr ""
|
||||
|
||||
msgid "OperationsDashboard|The Operations and Environments dashboards share the same list of projects. When you add or remove a project from one, GitLab adds or removes the project from the other. %{linkStart}More information%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "OperationsDashboard|The operations dashboard provides a summary of each project's operational health, including pipeline and alert statuses."
|
||||
msgstr ""
|
||||
|
||||
|
@ -44165,6 +44168,9 @@ msgstr ""
|
|||
msgid "WorkItem|Add a task"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Add a title"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Add assignee"
|
||||
msgstr ""
|
||||
|
||||
|
@ -44251,6 +44257,9 @@ msgstr ""
|
|||
msgid "WorkItem|Something went wrong when trying to add a child. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Something went wrong when trying to create a child. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Something went wrong while updating the work item. Please try again."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -6,6 +6,16 @@ module QA
|
|||
extend self
|
||||
|
||||
def perform_before_hooks
|
||||
if QA::Runtime::Env.admin_personal_access_token.present?
|
||||
QA::Resource::PersonalAccessTokenCache.set_token_for_username(QA::Runtime::User.admin_username,
|
||||
QA::Runtime::Env.admin_personal_access_token)
|
||||
end
|
||||
|
||||
if QA::Runtime::Env.personal_access_token.present? && QA::Runtime::Env.user_username.present?
|
||||
QA::Resource::PersonalAccessTokenCache.set_token_for_username(QA::Runtime::Env.user_username,
|
||||
QA::Runtime::Env.personal_access_token)
|
||||
end
|
||||
|
||||
# The login page could take some time to load the first time it is visited.
|
||||
# We visit the login page and wait for it to properly load only once before the tests.
|
||||
QA::Runtime::Logger.info("Performing sanity check for environment!")
|
||||
|
|
|
@ -17,20 +17,17 @@ module QA
|
|||
# If Runtime::Env.admin_personal_access_token is provided, fabricate via the API,
|
||||
# else, fabricate via the browser.
|
||||
def fabricate_via_api!
|
||||
QA::Resource::PersonalAccessTokenCache.get_token_for_username(user.username).tap do |cached_token|
|
||||
@token = cached_token if cached_token
|
||||
end
|
||||
return if @token
|
||||
return if find_and_set_value
|
||||
|
||||
resource = if Runtime::Env.admin_personal_access_token && !@user.nil?
|
||||
self.api_client = Runtime::API::Client.as_admin
|
||||
|
||||
super
|
||||
else
|
||||
fabricate!
|
||||
end
|
||||
|
||||
QA::Resource::PersonalAccessTokenCache.set_token_for_username(user.username, token) if @user
|
||||
self.token = api_response[:token] unless api_response.nil?
|
||||
cache_token
|
||||
resource
|
||||
end
|
||||
|
||||
|
@ -60,7 +57,17 @@ module QA
|
|||
# this particular resource does not expose a web_url property
|
||||
end
|
||||
|
||||
def find_and_set_value
|
||||
@token ||= QA::Resource::PersonalAccessTokenCache.get_token_for_username(user.username)
|
||||
end
|
||||
|
||||
def cache_token
|
||||
QA::Resource::PersonalAccessTokenCache.set_token_for_username(user.username, self.token) if @user && self.token
|
||||
end
|
||||
|
||||
def fabricate!
|
||||
return if find_and_set_value
|
||||
|
||||
Flow::Login.sign_in_unless_signed_in(user: user)
|
||||
|
||||
Page::Main::Menu.perform(&:click_edit_profile_link)
|
||||
|
@ -74,6 +81,10 @@ module QA
|
|||
token_page.click_create_token_button
|
||||
|
||||
self.token = Page::Profile::PersonalAccessTokens.perform(&:created_access_token)
|
||||
|
||||
cache_token
|
||||
|
||||
self.token
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,7 +6,17 @@ module QA
|
|||
@personal_access_tokens = {}
|
||||
|
||||
def self.get_token_for_username(username)
|
||||
@personal_access_tokens[username]
|
||||
token = @personal_access_tokens[username]
|
||||
|
||||
log_message = if token
|
||||
%Q[Retrieved cached token for username: #{username}, last six chars of token:#{token[-6..]}]
|
||||
else
|
||||
%Q[No cached token found for username: #{username}]
|
||||
end
|
||||
|
||||
QA::Runtime::Logger.info(log_message)
|
||||
|
||||
token
|
||||
end
|
||||
|
||||
def self.set_token_for_username(username, token)
|
||||
|
|
|
@ -11,7 +11,9 @@ class StaticAnalysis
|
|||
# https://github.com/browserslist/browserslist/blob/d0ec62eb48c41c218478cd3ac28684df051cc865/node.js#L329
|
||||
# warns if caniuse-lite package is older than 6 months. Ignore this
|
||||
# warning message so that GitLab backports don't fail.
|
||||
"Browserslist: caniuse-lite is outdated. Please run next command `yarn upgrade`"
|
||||
"Browserslist: caniuse-lite is outdated. Please run next command `yarn upgrade`",
|
||||
# https://github.com/mime-types/mime-types-data/pull/50#issuecomment-1060908930
|
||||
"Type application/netcdf is already registered as a variant of application/netcdf"
|
||||
].freeze
|
||||
|
||||
Task = Struct.new(:command, :duration) do
|
||||
|
|
|
@ -504,6 +504,20 @@ FactoryBot.define do
|
|||
artifacts_expire_at { 1.minute.ago }
|
||||
end
|
||||
|
||||
trait :with_artifacts_paths do
|
||||
options do
|
||||
{
|
||||
artifacts: {
|
||||
name: 'artifacts_file',
|
||||
untracked: false,
|
||||
paths: ['out/'],
|
||||
when: 'always',
|
||||
expire_in: '7d'
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
trait :with_commit do
|
||||
after(:build) do |build|
|
||||
commit = build(:commit, :without_author)
|
||||
|
|
|
@ -266,7 +266,7 @@ describe('Description component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('with work items feature flag is enabled', () => {
|
||||
describe('with work_items_create_from_markdown feature flag enabled', () => {
|
||||
describe('empty description', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
|
@ -275,7 +275,7 @@ describe('Description component', () => {
|
|||
},
|
||||
provide: {
|
||||
glFeatures: {
|
||||
workItems: true,
|
||||
workItemsCreateFromMarkdown: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -295,7 +295,7 @@ describe('Description component', () => {
|
|||
},
|
||||
provide: {
|
||||
glFeatures: {
|
||||
workItems: true,
|
||||
workItemsCreateFromMarkdown: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -344,7 +344,7 @@ describe('Description component', () => {
|
|||
descriptionHtml: descriptionHtmlWithTask,
|
||||
},
|
||||
provide: {
|
||||
glFeatures: { workItems: true },
|
||||
glFeatures: { workItemsCreateFromMarkdown: true },
|
||||
},
|
||||
});
|
||||
return nextTick();
|
||||
|
@ -406,7 +406,7 @@ describe('Description component', () => {
|
|||
|
||||
createComponent({
|
||||
props: { descriptionHtml: descriptionHtmlWithTask },
|
||||
provide: { glFeatures: { workItems: true } },
|
||||
provide: { glFeatures: { workItemsCreateFromMarkdown: true } },
|
||||
});
|
||||
|
||||
expect(showDetailsModal).toHaveBeenCalledTimes(modalOpened);
|
||||
|
@ -422,7 +422,7 @@ describe('Description component', () => {
|
|||
descriptionHtml: descriptionHtmlWithTask,
|
||||
},
|
||||
provide: {
|
||||
glFeatures: { workItems: true },
|
||||
glFeatures: { workItemsCreateFromMarkdown: true },
|
||||
},
|
||||
});
|
||||
return nextTick();
|
||||
|
|
|
@ -2,7 +2,21 @@ import { shallowMount } from '@vue/test-utils';
|
|||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import ReviewerAvatarLink from '~/sidebar/components/reviewers/reviewer_avatar_link.vue';
|
||||
import UncollapsedReviewerList from '~/sidebar/components/reviewers/uncollapsed_reviewer_list.vue';
|
||||
import userDataMock from '../../user_data_mock';
|
||||
|
||||
const userDataMock = () => ({
|
||||
id: 1,
|
||||
name: 'Root',
|
||||
state: 'active',
|
||||
username: 'root',
|
||||
webUrl: `${TEST_HOST}/root`,
|
||||
avatarUrl: `${TEST_HOST}/avatar/root.png`,
|
||||
mergeRequestInteraction: {
|
||||
canMerge: true,
|
||||
canUpdate: true,
|
||||
reviewed: true,
|
||||
approved: false,
|
||||
},
|
||||
});
|
||||
|
||||
describe('UncollapsedReviewerList component', () => {
|
||||
let wrapper;
|
||||
|
@ -69,7 +83,10 @@ describe('UncollapsedReviewerList component', () => {
|
|||
id: 2,
|
||||
name: 'nonrooty-nonrootersen',
|
||||
username: 'hello-world',
|
||||
approved: true,
|
||||
mergeRequestInteraction: {
|
||||
...user.mergeRequestInteraction,
|
||||
approved: true,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -1,9 +1,23 @@
|
|||
import { GlIcon } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { trimText } from 'helpers/text_helper';
|
||||
import UsersMockHelper from 'helpers/user_mock_data_helper';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import Reviewer from '~/sidebar/components/reviewers/reviewers.vue';
|
||||
import UsersMock from './mock_data';
|
||||
|
||||
const usersMock = (id = 1) => ({
|
||||
id,
|
||||
name: 'Root',
|
||||
state: 'active',
|
||||
username: 'root',
|
||||
webUrl: `${TEST_HOST}/root`,
|
||||
avatarUrl: `${TEST_HOST}/avatar/root.png`,
|
||||
mergeRequestInteraction: {
|
||||
canMerge: true,
|
||||
canUpdate: true,
|
||||
reviewed: true,
|
||||
approved: false,
|
||||
},
|
||||
});
|
||||
|
||||
describe('Reviewer component', () => {
|
||||
const getDefaultProps = () => ({
|
||||
|
@ -42,23 +56,23 @@ describe('Reviewer component', () => {
|
|||
it('displays one reviewer icon when collapsed', () => {
|
||||
createWrapper({
|
||||
...getDefaultProps(),
|
||||
users: [UsersMock.user],
|
||||
users: [usersMock()],
|
||||
});
|
||||
|
||||
const collapsedChildren = findCollapsedChildren();
|
||||
const reviewer = collapsedChildren.at(0);
|
||||
|
||||
expect(collapsedChildren.length).toBe(1);
|
||||
expect(reviewer.find('.avatar').attributes('src')).toBe(UsersMock.user.avatar);
|
||||
expect(reviewer.find('.avatar').attributes('alt')).toBe(`${UsersMock.user.name}'s avatar`);
|
||||
expect(reviewer.find('.avatar').attributes('src')).toContain('avatar/root.png');
|
||||
expect(reviewer.find('.avatar').attributes('alt')).toBe(`Root's avatar`);
|
||||
|
||||
expect(trimText(reviewer.find('.author').text())).toBe(UsersMock.user.name);
|
||||
expect(trimText(reviewer.find('.author').text())).toBe('Root');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Two or more reviewers/users', () => {
|
||||
it('displays two reviewer icons when collapsed', () => {
|
||||
const users = UsersMockHelper.createNumberRandomUsers(2);
|
||||
const users = [usersMock(), usersMock(2)];
|
||||
createWrapper({
|
||||
...getDefaultProps(),
|
||||
users,
|
||||
|
@ -70,21 +84,21 @@ describe('Reviewer component', () => {
|
|||
|
||||
const first = collapsedChildren.at(0);
|
||||
|
||||
expect(first.find('.avatar').attributes('src')).toBe(users[0].avatar_url);
|
||||
expect(first.find('.avatar').attributes('src')).toBe(users[0].avatarUrl);
|
||||
expect(first.find('.avatar').attributes('alt')).toBe(`${users[0].name}'s avatar`);
|
||||
|
||||
expect(trimText(first.find('.author').text())).toBe(users[0].name);
|
||||
|
||||
const second = collapsedChildren.at(1);
|
||||
|
||||
expect(second.find('.avatar').attributes('src')).toBe(users[1].avatar_url);
|
||||
expect(second.find('.avatar').attributes('src')).toBe(users[1].avatarUrl);
|
||||
expect(second.find('.avatar').attributes('alt')).toBe(`${users[1].name}'s avatar`);
|
||||
|
||||
expect(trimText(second.find('.author').text())).toBe(users[1].name);
|
||||
});
|
||||
|
||||
it('displays one reviewer icon and counter when collapsed', () => {
|
||||
const users = UsersMockHelper.createNumberRandomUsers(3);
|
||||
const users = [usersMock(), usersMock(2), usersMock(3)];
|
||||
createWrapper({
|
||||
...getDefaultProps(),
|
||||
users,
|
||||
|
@ -96,7 +110,7 @@ describe('Reviewer component', () => {
|
|||
|
||||
const first = collapsedChildren.at(0);
|
||||
|
||||
expect(first.find('.avatar').attributes('src')).toBe(users[0].avatar_url);
|
||||
expect(first.find('.avatar').attributes('src')).toBe(users[0].avatarUrl);
|
||||
expect(first.find('.avatar').attributes('alt')).toBe(`${users[0].name}'s avatar`);
|
||||
|
||||
expect(trimText(first.find('.author').text())).toBe(users[0].name);
|
||||
|
@ -107,7 +121,7 @@ describe('Reviewer component', () => {
|
|||
});
|
||||
|
||||
it('Shows two reviewers', () => {
|
||||
const users = UsersMockHelper.createNumberRandomUsers(2);
|
||||
const users = [usersMock(), usersMock(2)];
|
||||
createWrapper({
|
||||
...getDefaultProps(),
|
||||
users,
|
||||
|
@ -118,10 +132,10 @@ describe('Reviewer component', () => {
|
|||
});
|
||||
|
||||
it('shows sorted reviewer where "can merge" users are sorted first', () => {
|
||||
const users = UsersMockHelper.createNumberRandomUsers(3);
|
||||
users[0].can_merge = false;
|
||||
users[1].can_merge = false;
|
||||
users[2].can_merge = true;
|
||||
const users = [usersMock(), usersMock(2), usersMock(3)];
|
||||
users[0].mergeRequestInteraction.canMerge = false;
|
||||
users[1].mergeRequestInteraction.canMerge = false;
|
||||
users[2].mergeRequestInteraction.canMerge = true;
|
||||
|
||||
createWrapper({
|
||||
...getDefaultProps(),
|
||||
|
@ -129,14 +143,14 @@ describe('Reviewer component', () => {
|
|||
editable: true,
|
||||
});
|
||||
|
||||
expect(wrapper.vm.sortedReviewers[0].can_merge).toBe(true);
|
||||
expect(wrapper.vm.sortedReviewers[0].mergeRequestInteraction.canMerge).toBe(true);
|
||||
});
|
||||
|
||||
it('passes the sorted reviewers to the uncollapsed-reviewer-list', () => {
|
||||
const users = UsersMockHelper.createNumberRandomUsers(3);
|
||||
users[0].can_merge = false;
|
||||
users[1].can_merge = false;
|
||||
users[2].can_merge = true;
|
||||
const users = [usersMock(), usersMock(2), usersMock(3)];
|
||||
users[0].mergeRequestInteraction.canMerge = false;
|
||||
users[1].mergeRequestInteraction.canMerge = false;
|
||||
users[2].mergeRequestInteraction.canMerge = true;
|
||||
|
||||
createWrapper({
|
||||
...getDefaultProps(),
|
||||
|
@ -149,10 +163,10 @@ describe('Reviewer component', () => {
|
|||
});
|
||||
|
||||
it('passes the sorted reviewers to the collapsed-reviewer-list', () => {
|
||||
const users = UsersMockHelper.createNumberRandomUsers(3);
|
||||
users[0].can_merge = false;
|
||||
users[1].can_merge = false;
|
||||
users[2].can_merge = true;
|
||||
const users = [usersMock(), usersMock(2), usersMock(3)];
|
||||
users[0].mergeRequestInteraction.canMerge = false;
|
||||
users[1].mergeRequestInteraction.canMerge = false;
|
||||
users[2].mergeRequestInteraction.canMerge = true;
|
||||
|
||||
createWrapper({
|
||||
...getDefaultProps(),
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
import Vue from 'vue';
|
||||
import { GlForm, GlFormCombobox } from '@gitlab/ui';
|
||||
import { GlForm, GlFormInput, GlFormCombobox } from '@gitlab/ui';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue';
|
||||
import projectWorkItemsQuery from '~/work_items/graphql/project_work_items.query.graphql';
|
||||
import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
|
||||
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
|
||||
import { availableWorkItemsResponse, updateWorkItemMutationResponse } from '../../mock_data';
|
||||
import {
|
||||
availableWorkItemsResponse,
|
||||
createWorkItemMutationResponse,
|
||||
updateWorkItemMutationResponse,
|
||||
} from '../../mock_data';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
|
@ -15,12 +20,14 @@ describe('WorkItemLinksForm', () => {
|
|||
let wrapper;
|
||||
|
||||
const updateMutationResolver = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
|
||||
const createMutationResolver = jest.fn().mockResolvedValue(createWorkItemMutationResponse);
|
||||
|
||||
const createComponent = async ({ listResponse = availableWorkItemsResponse } = {}) => {
|
||||
wrapper = shallowMountExtended(WorkItemLinksForm, {
|
||||
apolloProvider: createMockApollo([
|
||||
[projectWorkItemsQuery, jest.fn().mockResolvedValue(listResponse)],
|
||||
[updateWorkItemMutation, updateMutationResolver],
|
||||
[createWorkItemMutation, createMutationResolver],
|
||||
]),
|
||||
propsData: { issuableGid: 'gid://gitlab/WorkItem/1' },
|
||||
provide: {
|
||||
|
@ -33,6 +40,7 @@ describe('WorkItemLinksForm', () => {
|
|||
|
||||
const findForm = () => wrapper.findComponent(GlForm);
|
||||
const findCombobox = () => wrapper.findComponent(GlFormCombobox);
|
||||
const findInput = () => wrapper.findComponent(GlFormInput);
|
||||
const findAddChildButton = () => wrapper.findByTestId('add-child-button');
|
||||
|
||||
beforeEach(async () => {
|
||||
|
@ -47,19 +55,41 @@ describe('WorkItemLinksForm', () => {
|
|||
expect(findForm().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('passes available work items as prop when typing in combobox', async () => {
|
||||
findCombobox().vm.$emit('input', 'Task');
|
||||
await waitForPromises();
|
||||
it('creates child task', async () => {
|
||||
findInput().vm.$emit('input', 'Create task test');
|
||||
|
||||
expect(findCombobox().exists()).toBe(true);
|
||||
expect(findCombobox().props('tokenList').length).toBe(2);
|
||||
findForm().vm.$emit('submit', {
|
||||
preventDefault: jest.fn(),
|
||||
});
|
||||
await waitForPromises();
|
||||
expect(createMutationResolver).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('selects and add child', async () => {
|
||||
// Follow up issue to turn this functionality back on https://gitlab.com/gitlab-org/gitlab/-/issues/368757
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('selects and add child', async () => {
|
||||
findCombobox().vm.$emit('input', availableWorkItemsResponse.data.workspace.workItems.edges[0]);
|
||||
|
||||
findAddChildButton().vm.$emit('click');
|
||||
await waitForPromises();
|
||||
expect(updateMutationResolver).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
describe.skip('when typing in combobox', () => {
|
||||
beforeEach(async () => {
|
||||
findCombobox().vm.$emit('input', 'Task');
|
||||
await waitForPromises();
|
||||
await jest.runOnlyPendingTimers();
|
||||
});
|
||||
|
||||
it('passes available work items as prop', () => {
|
||||
expect(findCombobox().exists()).toBe(true);
|
||||
expect(findCombobox().props('tokenList').length).toBe(2);
|
||||
});
|
||||
|
||||
it('passes action to create task', () => {
|
||||
expect(findCombobox().props('actionList').length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -219,6 +219,7 @@ export const createWorkItemMutationResponse = {
|
|||
},
|
||||
widgets: [],
|
||||
},
|
||||
errors: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe Gitlab::Git::Branch, :seed_helper do
|
||||
let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
|
||||
RSpec.describe Gitlab::Git::Branch do
|
||||
let(:project) { create(:project, :repository) }
|
||||
let(:repository) { project.repository.raw }
|
||||
|
||||
subject { repository.branches }
|
||||
|
||||
|
@ -54,14 +55,14 @@ RSpec.describe Gitlab::Git::Branch, :seed_helper do
|
|||
describe '#size' do
|
||||
subject { super().size }
|
||||
|
||||
it { is_expected.to eq(SeedRepo::Repo::BRANCHES.size) }
|
||||
it { is_expected.to eq(TestEnv::BRANCH_SHA.size) }
|
||||
end
|
||||
|
||||
describe 'first branch' do
|
||||
let(:branch) { repository.branches.first }
|
||||
|
||||
it { expect(branch.name).to eq(SeedRepo::Repo::BRANCHES.first) }
|
||||
it { expect(branch.dereferenced_target.sha).to eq("0b4bc9a49b562e85de7cc9e834518ea6828729b9") }
|
||||
it { expect(branch.name).to eq(TestEnv::BRANCH_SHA.keys.min) }
|
||||
it { expect(branch.dereferenced_target.sha).to start_with(TestEnv::BRANCH_SHA[TestEnv::BRANCH_SHA.keys.min]) }
|
||||
end
|
||||
|
||||
describe 'master branch' do
|
||||
|
@ -69,14 +70,10 @@ RSpec.describe Gitlab::Git::Branch, :seed_helper do
|
|||
repository.branches.find { |branch| branch.name == 'master' }
|
||||
end
|
||||
|
||||
it { expect(branch.dereferenced_target.sha).to eq(SeedRepo::LastCommit::ID) }
|
||||
it { expect(branch.dereferenced_target.sha).to start_with(TestEnv::BRANCH_SHA['master']) }
|
||||
end
|
||||
|
||||
context 'with active, stale and future branches' do
|
||||
let(:repository) do
|
||||
Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '', 'group/project')
|
||||
end
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:stale_sha) { travel_to(Gitlab::Git::Branch::STALE_BRANCH_THRESHOLD.ago - 5.days) { create_commit } }
|
||||
let(:active_sha) { travel_to(Gitlab::Git::Branch::STALE_BRANCH_THRESHOLD.ago + 5.days) { create_commit } }
|
||||
|
@ -88,10 +85,6 @@ RSpec.describe Gitlab::Git::Branch, :seed_helper do
|
|||
repository.create_branch('future-1', future_sha)
|
||||
end
|
||||
|
||||
after do
|
||||
ensure_seeds
|
||||
end
|
||||
|
||||
describe 'examine if the branch is active or stale' do
|
||||
let(:stale_branch) { repository.find_branch('stale-1') }
|
||||
let(:active_branch) { repository.find_branch('active-1') }
|
||||
|
@ -117,8 +110,6 @@ RSpec.describe Gitlab::Git::Branch, :seed_helper do
|
|||
end
|
||||
end
|
||||
|
||||
it { expect(repository.branches.size).to eq(SeedRepo::Repo::BRANCHES.size) }
|
||||
|
||||
def create_commit
|
||||
repository.multi_action(
|
||||
user,
|
||||
|
|
|
@ -51,10 +51,11 @@ RSpec.describe Gitlab::ImportExport::DecompressedArchiveSizeValidator do
|
|||
shared_examples 'logs raised exception and terminates validator process group' do
|
||||
let(:std) { double(:std, close: nil, value: nil) }
|
||||
let(:wait_thr) { double }
|
||||
let(:wait_threads) { [wait_thr, wait_thr] }
|
||||
|
||||
before do
|
||||
allow(Process).to receive(:getpgid).and_return(2)
|
||||
allow(Open3).to receive(:popen3).and_return([std, std, std, wait_thr])
|
||||
allow(Open3).to receive(:pipeline_r).and_return([std, wait_threads])
|
||||
allow(wait_thr).to receive(:[]).with(:pid).and_return(1)
|
||||
allow(wait_thr).to receive(:value).and_raise(exception)
|
||||
end
|
||||
|
@ -67,7 +68,7 @@ RSpec.describe Gitlab::ImportExport::DecompressedArchiveSizeValidator do
|
|||
import_upload_archive_size: File.size(filepath),
|
||||
message: error_message
|
||||
)
|
||||
expect(Process).to receive(:kill).with(-1, 2)
|
||||
expect(Process).to receive(:kill).with(-1, 2).twice
|
||||
expect(subject.valid?).to eq(false)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3390,6 +3390,13 @@ RSpec.describe Group do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#work_items_create_from_markdown_feature_flag_enabled?' do
|
||||
it_behaves_like 'checks self and root ancestor feature flag' do
|
||||
let(:feature_flag) { :work_items_create_from_markdown }
|
||||
let(:feature_flag_method) { :work_items_create_from_markdown_feature_flag_enabled? }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'group shares' do
|
||||
let!(:sub_group) { create(:group, parent: group) }
|
||||
let!(:sub_sub_group) { create(:group, parent: sub_group) }
|
||||
|
|
|
@ -8281,6 +8281,16 @@ RSpec.describe Project, factory_default: :keep do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#work_items_create_from_markdown_feature_flag_enabled?' do
|
||||
let_it_be(:group_project) { create(:project, :in_subgroup) }
|
||||
|
||||
it_behaves_like 'checks parent group feature flag' do
|
||||
let(:feature_flag_method) { :work_items_create_from_markdown_feature_flag_enabled? }
|
||||
let(:feature_flag) { :work_items_create_from_markdown }
|
||||
let(:subject_project) { group_project }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'serialization' do
|
||||
let(:object) { build(:project) }
|
||||
|
||||
|
|
|
@ -101,6 +101,37 @@ RSpec.describe EnvironmentSerializer do
|
|||
expect(subject.third[:latest][:environment_type]).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when folders and standalone environments share the same name' do
|
||||
before do
|
||||
create(:environment, project: project, name: 'staging/my-review-1')
|
||||
create(:environment, project: project, name: 'staging/my-review-2')
|
||||
create(:environment, project: project, name: 'production/my-review-3')
|
||||
create(:environment, project: project, name: 'staging')
|
||||
create(:environment, project: project, name: 'testing')
|
||||
end
|
||||
|
||||
it 'does not group standalone environments with folders that have the same name' do
|
||||
expect(subject.count).to eq 4
|
||||
|
||||
expect(subject.first[:name]).to eq 'production'
|
||||
expect(subject.first[:size]).to eq 1
|
||||
expect(subject.first[:latest][:name]).to eq 'production/my-review-3'
|
||||
expect(subject.first[:latest][:environment_type]).to eq 'production'
|
||||
expect(subject.second[:name]).to eq 'staging'
|
||||
expect(subject.second[:size]).to eq 1
|
||||
expect(subject.second[:latest][:name]).to eq 'staging'
|
||||
expect(subject.second[:latest][:environment_type]).to be_nil
|
||||
expect(subject.third[:name]).to eq 'staging'
|
||||
expect(subject.third[:size]).to eq 2
|
||||
expect(subject.third[:latest][:name]).to eq 'staging/my-review-2'
|
||||
expect(subject.third[:latest][:environment_type]).to eq 'staging'
|
||||
expect(subject.fourth[:name]).to eq 'testing'
|
||||
expect(subject.fourth[:size]).to eq 1
|
||||
expect(subject.fourth[:latest][:name]).to eq 'testing'
|
||||
expect(subject.fourth[:latest][:environment_type]).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when used with pagination' do
|
||||
|
|
|
@ -66,7 +66,7 @@ Integration.available_integration_names.each do |integration|
|
|||
hash.merge!(k => 'foo@bar.com')
|
||||
elsif (integration == 'slack' || integration == 'mattermost') && k == :labels_to_be_notified_behavior
|
||||
hash.merge!(k => "match_any")
|
||||
elsif integration == 'campfire' && k = :room
|
||||
elsif integration == 'campfire' && k == :room
|
||||
hash.merge!(k => '1234')
|
||||
else
|
||||
hash.merge!(k => "someword")
|
||||
|
|
Loading…
Reference in New Issue