Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
b86ad5f488
commit
a7d30d92f8
|
@ -15,6 +15,7 @@ import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue
|
|||
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
|
||||
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
|
||||
import SidebarLabelsWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
|
||||
import { LabelType } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
|
||||
export default {
|
||||
|
@ -63,7 +64,7 @@ export default {
|
|||
'groupPathForActiveIssue',
|
||||
'projectPathForActiveIssue',
|
||||
]),
|
||||
...mapState(['sidebarType', 'issuableType', 'isSettingLabels']),
|
||||
...mapState(['sidebarType', 'issuableType']),
|
||||
isIssuableSidebar() {
|
||||
return this.sidebarType === ISSUABLE;
|
||||
},
|
||||
|
@ -84,7 +85,10 @@ export default {
|
|||
});
|
||||
},
|
||||
attrWorkspacePath() {
|
||||
return this.isGroupBoard ? this.groupPathForActiveIssue : undefined;
|
||||
return this.isGroupBoard ? this.groupPathForActiveIssue : this.projectPathForActiveIssue;
|
||||
},
|
||||
labelType() {
|
||||
return this.isGroupBoard ? LabelType.group : LabelType.project;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
@ -98,21 +102,19 @@ export default {
|
|||
handleClose() {
|
||||
this.toggleBoardItem({ boardItem: this.activeBoardItem, sidebarType: this.sidebarType });
|
||||
},
|
||||
handleUpdateSelectedLabels(input) {
|
||||
handleUpdateSelectedLabels({ labels, id }) {
|
||||
this.setActiveBoardItemLabels({
|
||||
iid: this.activeBoardItem.iid,
|
||||
id,
|
||||
projectPath: this.projectPathForActiveIssue,
|
||||
addLabelIds: input.map((label) => getIdFromGraphQLId(label.id)),
|
||||
removeLabelIds: this.activeBoardItem.labels
|
||||
.filter((label) => !input.find((selected) => selected.id === label.id))
|
||||
.map((label) => label.id),
|
||||
labelIds: labels.map((label) => getIdFromGraphQLId(label.id)),
|
||||
labels,
|
||||
});
|
||||
},
|
||||
handleLabelRemove(input) {
|
||||
handleLabelRemove(removeLabelId) {
|
||||
this.setActiveBoardItemLabels({
|
||||
iid: this.activeBoardItem.iid,
|
||||
projectPath: this.projectPathForActiveIssue,
|
||||
removeLabelIds: [input],
|
||||
removeLabelIds: [removeLabelId],
|
||||
});
|
||||
},
|
||||
},
|
||||
|
@ -207,14 +209,13 @@ export default {
|
|||
:full-path="projectPathForActiveIssue"
|
||||
:allow-label-remove="allowLabelEdit"
|
||||
:allow-multiselect="true"
|
||||
:selected-labels="activeBoardItem.labels"
|
||||
:labels-select-in-progress="isSettingLabels"
|
||||
:footer-create-label-title="createLabelTitle"
|
||||
:footer-manage-label-title="manageLabelTitle"
|
||||
:labels-create-title="createLabelTitle"
|
||||
:labels-filter-base-path="projectPathForActiveIssue"
|
||||
:attr-workspace-path="attrWorkspacePath"
|
||||
:issuable-type="issuableType"
|
||||
:label-type="labelType"
|
||||
@onLabelRemove="handleLabelRemove"
|
||||
@updateSelectedLabels="handleUpdateSelectedLabels"
|
||||
>
|
||||
|
|
|
@ -91,9 +91,7 @@ export default {
|
|||
|
||||
try {
|
||||
const addLabelIds = payload.filter((label) => label.set).map((label) => label.id);
|
||||
const removeLabelIds = this.selectedLabels
|
||||
.filter((label) => !payload.find((selected) => selected.id === label.id))
|
||||
.map((label) => label.id);
|
||||
const removeLabelIds = payload.filter((label) => !label.set).map((label) => label.id);
|
||||
|
||||
const input = {
|
||||
addLabelIds,
|
||||
|
@ -164,7 +162,7 @@ export default {
|
|||
:labels-list-title="__('Select label')"
|
||||
:dropdown-button-text="__('Choose labels')"
|
||||
:is-editing="edit"
|
||||
variant="embedded"
|
||||
variant="sidebar"
|
||||
class="gl-display-block labels gl-w-full"
|
||||
@updateSelectedLabels="setLabels"
|
||||
>
|
||||
|
|
|
@ -656,30 +656,45 @@ export default {
|
|||
},
|
||||
|
||||
setActiveIssueLabels: async ({ commit, getters }, input) => {
|
||||
commit(types.SET_LABELS_LOADING, true);
|
||||
const { activeBoardItem } = getters;
|
||||
const { data } = await gqlClient.mutate({
|
||||
mutation: issueSetLabelsMutation,
|
||||
variables: {
|
||||
input: {
|
||||
iid: input.iid || String(activeBoardItem.iid),
|
||||
addLabelIds: input.addLabelIds ?? [],
|
||||
removeLabelIds: input.removeLabelIds ?? [],
|
||||
projectPath: input.projectPath,
|
||||
|
||||
if (!gon.features?.labelsWidget) {
|
||||
const { data } = await gqlClient.mutate({
|
||||
mutation: issueSetLabelsMutation,
|
||||
variables: {
|
||||
input: {
|
||||
iid: input.iid || String(activeBoardItem.iid),
|
||||
labelIds: input.labelsId ?? undefined,
|
||||
addLabelIds: input.addLabelIds ?? [],
|
||||
removeLabelIds: input.removeLabelIds ?? [],
|
||||
projectPath: input.projectPath,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
commit(types.SET_LABELS_LOADING, false);
|
||||
if (data.updateIssue?.errors?.length > 0) {
|
||||
throw new Error(data.updateIssue.errors);
|
||||
}
|
||||
|
||||
if (data.updateIssue?.errors?.length > 0) {
|
||||
throw new Error(data.updateIssue.errors);
|
||||
commit(types.UPDATE_BOARD_ITEM_BY_ID, {
|
||||
itemId: data.updateIssue?.issue?.id || activeBoardItem.id,
|
||||
prop: 'labels',
|
||||
value: data.updateIssue?.issue?.labels.nodes,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let labels = input?.labels || [];
|
||||
if (input.removeLabelIds) {
|
||||
labels = activeBoardItem.labels.filter(
|
||||
(label) => input.removeLabelIds[0] !== getIdFromGraphQLId(label.id),
|
||||
);
|
||||
}
|
||||
commit(types.UPDATE_BOARD_ITEM_BY_ID, {
|
||||
itemId: data.updateIssue?.issue?.id || activeBoardItem.id,
|
||||
itemId: input.id || activeBoardItem.id,
|
||||
prop: 'labels',
|
||||
value: data.updateIssue.issue.labels.nodes,
|
||||
value: labels,
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -28,7 +28,6 @@ export const ADD_BOARD_ITEM_TO_LIST = 'ADD_BOARD_ITEM_TO_LIST';
|
|||
export const REMOVE_BOARD_ITEM_FROM_LIST = 'REMOVE_BOARD_ITEM_FROM_LIST';
|
||||
export const SET_ACTIVE_ID = 'SET_ACTIVE_ID';
|
||||
export const UPDATE_BOARD_ITEM_BY_ID = 'UPDATE_BOARD_ITEM_BY_ID';
|
||||
export const SET_LABELS_LOADING = 'SET_LABELS_LOADING';
|
||||
export const SET_ASSIGNEE_LOADING = 'SET_ASSIGNEE_LOADING';
|
||||
export const RESET_ISSUES = 'RESET_ISSUES';
|
||||
export const REQUEST_GROUP_PROJECTS = 'REQUEST_GROUP_PROJECTS';
|
||||
|
|
|
@ -195,10 +195,6 @@ export default {
|
|||
Vue.set(state.boardItems[itemId], prop, value);
|
||||
},
|
||||
|
||||
[mutationTypes.SET_LABELS_LOADING](state, isLoading) {
|
||||
state.isSettingLabels = isLoading;
|
||||
},
|
||||
|
||||
[mutationTypes.SET_ASSIGNEE_LOADING](state, isLoading) {
|
||||
state.isSettingAssignees = isLoading;
|
||||
},
|
||||
|
|
|
@ -12,7 +12,6 @@ export default () => ({
|
|||
listsFlags: {},
|
||||
boardItemsByListId: {},
|
||||
backupItemsList: [],
|
||||
isSettingLabels: false,
|
||||
isSettingAssignees: false,
|
||||
pageInfoByListId: {},
|
||||
boardItems: {},
|
||||
|
|
|
@ -11,6 +11,7 @@ import { toLabelGid } from '~/sidebar/utils';
|
|||
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
|
||||
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
|
||||
import LabelsSelectWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
|
||||
import { LabelType } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
|
||||
const mutationMap = {
|
||||
|
@ -48,6 +49,7 @@ export default {
|
|||
return {
|
||||
isLabelsSelectInProgress: false,
|
||||
selectedLabels: this.initiallySelectedLabels,
|
||||
LabelType,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
@ -154,13 +156,11 @@ export default {
|
|||
:footer-manage-label-title="__('Manage project labels')"
|
||||
:labels-create-title="__('Create project label')"
|
||||
:labels-filter-base-path="projectIssuesPath"
|
||||
:labels-select-in-progress="isLabelsSelectInProgress"
|
||||
:selected-labels="selectedLabels"
|
||||
:variant="$options.variant"
|
||||
:issuable-type="issuableType"
|
||||
:attr-workspace-path="fullPath"
|
||||
:label-type="LabelType.project"
|
||||
data-qa-selector="labels_block"
|
||||
@onLabelRemove="handleLabelRemove"
|
||||
@updateSelectedLabels="handleUpdateSelectedLabels"
|
||||
>
|
||||
{{ __('None') }}
|
||||
</labels-select-widget>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
|
||||
import { IssuableType } from '~/issue_show/constants';
|
||||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||
import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql';
|
||||
|
@ -29,6 +30,7 @@ import updateIssueConfidentialMutation from '~/sidebar/queries/update_issue_conf
|
|||
import updateIssueDueDateMutation from '~/sidebar/queries/update_issue_due_date.mutation.graphql';
|
||||
import updateIssueSubscriptionMutation from '~/sidebar/queries/update_issue_subscription.mutation.graphql';
|
||||
import mergeRequestMilestoneMutation from '~/sidebar/queries/update_merge_request_milestone.mutation.graphql';
|
||||
import updateMergeRequestLabelsMutation from '~/sidebar/queries/update_merge_request_labels.mutation.graphql';
|
||||
import updateMergeRequestSubscriptionMutation from '~/sidebar/queries/update_merge_request_subscription.mutation.graphql';
|
||||
import updateAlertAssigneesMutation from '~/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql';
|
||||
import epicLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql';
|
||||
|
@ -120,6 +122,17 @@ export const labelsQueries = {
|
|||
},
|
||||
};
|
||||
|
||||
export const labelsMutations = {
|
||||
[IssuableType.Issue]: {
|
||||
mutation: updateIssueLabelsMutation,
|
||||
mutationName: 'updateIssue',
|
||||
},
|
||||
[IssuableType.MergeRequest]: {
|
||||
mutation: updateMergeRequestLabelsMutation,
|
||||
mutationName: 'mergeRequestSetLabels',
|
||||
},
|
||||
};
|
||||
|
||||
export const dateTypes = {
|
||||
start: 'startDate',
|
||||
due: 'dueDate',
|
||||
|
|
|
@ -5,3 +5,8 @@ export const DropdownVariant = {
|
|||
Standalone: 'standalone',
|
||||
Embedded: 'embedded',
|
||||
};
|
||||
|
||||
export const LabelType = {
|
||||
group: 'GroupLabel',
|
||||
project: 'ProjectLabel',
|
||||
};
|
||||
|
|
|
@ -1,20 +1,25 @@
|
|||
<script>
|
||||
import { GlButton, GlDropdown, GlDropdownItem, GlLink } from '@gitlab/ui';
|
||||
import { debounce } from 'lodash';
|
||||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
import DropdownContentsCreateView from './dropdown_contents_create_view.vue';
|
||||
import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue';
|
||||
import DropdownFooter from './dropdown_footer.vue';
|
||||
import DropdownHeader from './dropdown_header.vue';
|
||||
import { isDropdownVariantStandalone, isDropdownVariantSidebar } from './utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DropdownContentsLabelsView,
|
||||
DropdownContentsCreateView,
|
||||
DropdownHeader,
|
||||
DropdownFooter,
|
||||
GlButton,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlLink,
|
||||
},
|
||||
inject: ['allowLabelCreate', 'labelsManagePath'],
|
||||
props: {
|
||||
labelsCreateTitle: {
|
||||
type: String,
|
||||
|
@ -63,8 +68,11 @@ export default {
|
|||
},
|
||||
attrWorkspacePath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: undefined,
|
||||
required: true,
|
||||
},
|
||||
labelType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
|
@ -72,6 +80,7 @@ export default {
|
|||
showDropdownContentsCreateView: false,
|
||||
localSelectedLabels: [...this.selectedLabels],
|
||||
isDirty: false,
|
||||
searchKey: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -113,15 +122,24 @@ export default {
|
|||
if (newVal) {
|
||||
this.$refs.dropdown.show();
|
||||
this.isDirty = false;
|
||||
this.localSelectedLabels = this.selectedLabels;
|
||||
} else {
|
||||
this.$refs.dropdown.hide();
|
||||
this.setLabels();
|
||||
}
|
||||
},
|
||||
selectedLabels(newVal) {
|
||||
this.localSelectedLabels = newVal;
|
||||
if (!this.isDirty) {
|
||||
this.localSelectedLabels = newVal;
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.debouncedSearchKeyUpdate.cancel();
|
||||
},
|
||||
methods: {
|
||||
toggleDropdownContentsCreateView() {
|
||||
this.showDropdownContentsCreateView = !this.showDropdownContentsCreateView;
|
||||
|
@ -144,6 +162,12 @@ export default {
|
|||
this.setLabels();
|
||||
}
|
||||
},
|
||||
setSearchKey(value) {
|
||||
this.searchKey = value;
|
||||
},
|
||||
setFocus() {
|
||||
this.$refs.header.focusInput();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -155,60 +179,41 @@ export default {
|
|||
class="gl-w-full gl-mt-2"
|
||||
data-qa-selector="labels_dropdown_content"
|
||||
@hide="handleDropdownHide"
|
||||
@shown="setFocus"
|
||||
>
|
||||
<template #header>
|
||||
<div
|
||||
<dropdown-header
|
||||
v-if="!isStandalone"
|
||||
class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
|
||||
data-testid="dropdown-header"
|
||||
>
|
||||
<gl-button
|
||||
v-if="showDropdownContentsCreateView"
|
||||
:aria-label="__('Go back')"
|
||||
variant="link"
|
||||
size="small"
|
||||
class="js-btn-back dropdown-header-button gl-p-0"
|
||||
icon="arrow-left"
|
||||
data-testid="go-back-button"
|
||||
@click.stop="toggleDropdownContent"
|
||||
/>
|
||||
<span class="gl-flex-grow-1">{{ dropdownTitle }}</span>
|
||||
<gl-button
|
||||
:aria-label="__('Close')"
|
||||
variant="link"
|
||||
size="small"
|
||||
class="dropdown-header-button gl-p-0!"
|
||||
icon="close"
|
||||
data-testid="close-button"
|
||||
@click="$emit('closeDropdown')"
|
||||
/>
|
||||
</div>
|
||||
ref="header"
|
||||
v-model="searchKey"
|
||||
:labels-create-title="labelsCreateTitle"
|
||||
:labels-list-title="labelsListTitle"
|
||||
:show-dropdown-contents-create-view="showDropdownContentsCreateView"
|
||||
@toggleDropdownContentsCreateView="toggleDropdownContent"
|
||||
@closeDropdown="$emit('closeDropdown')"
|
||||
@input="debouncedSearchKeyUpdate"
|
||||
/>
|
||||
</template>
|
||||
<template #default>
|
||||
<component
|
||||
:is="dropdownContentsView"
|
||||
v-model="localSelectedLabels"
|
||||
:selected-labels="selectedLabels"
|
||||
:search-key="searchKey"
|
||||
:allow-multiselect="allowMultiselect"
|
||||
:issuable-type="issuableType"
|
||||
:full-path="fullPath"
|
||||
:attr-workspace-path="attrWorkspacePath"
|
||||
:label-type="labelType"
|
||||
@hideCreateView="toggleDropdownContentsCreateView"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div v-if="showDropdownFooter" data-testid="dropdown-footer">
|
||||
<gl-dropdown-item
|
||||
v-if="allowLabelCreate"
|
||||
data-testid="create-label-button"
|
||||
@click.capture.native.stop="toggleDropdownContent"
|
||||
>
|
||||
{{ footerCreateLabelTitle }}
|
||||
</gl-dropdown-item>
|
||||
<gl-dropdown-item :href="labelsManagePath" @click.capture.native.stop>
|
||||
{{ footerManageLabelTitle }}
|
||||
</gl-dropdown-item>
|
||||
</div>
|
||||
<dropdown-footer
|
||||
v-if="showDropdownFooter"
|
||||
:footer-create-label-title="footerCreateLabelTitle"
|
||||
:footer-manage-label-title="footerManageLabelTitle"
|
||||
@toggleDropdownContentsCreateView="toggleDropdownContent"
|
||||
/>
|
||||
</template>
|
||||
</gl-dropdown>
|
||||
</template>
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
import { GlTooltipDirective, GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
|
||||
import produce from 'immer';
|
||||
import createFlash from '~/flash';
|
||||
import { IssuableType } from '~/issue_show/constants';
|
||||
import { __ } from '~/locale';
|
||||
import { labelsQueries } from '~/sidebar/constants';
|
||||
import createLabelMutation from './graphql/create_label.mutation.graphql';
|
||||
import { LabelType } from './constants';
|
||||
|
||||
const errorMessage = __('Error creating label.');
|
||||
|
||||
|
@ -30,8 +30,11 @@ export default {
|
|||
},
|
||||
attrWorkspacePath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: undefined,
|
||||
required: true,
|
||||
},
|
||||
labelType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
|
@ -50,25 +53,13 @@ export default {
|
|||
return Object.keys(colorsMap).map((color) => ({ [color]: colorsMap[color] }));
|
||||
},
|
||||
mutationVariables() {
|
||||
if (this.issuableType === IssuableType.Epic) {
|
||||
return {
|
||||
title: this.labelTitle,
|
||||
color: this.selectedColor,
|
||||
groupPath: this.fullPath,
|
||||
};
|
||||
}
|
||||
const attributePath = this.labelType === LabelType.group ? 'groupPath' : 'projectPath';
|
||||
|
||||
return this.attrWorkspacePath !== undefined
|
||||
? {
|
||||
title: this.labelTitle,
|
||||
color: this.selectedColor,
|
||||
groupPath: this.attrWorkspacePath,
|
||||
}
|
||||
: {
|
||||
title: this.labelTitle,
|
||||
color: this.selectedColor,
|
||||
projectPath: this.fullPath,
|
||||
};
|
||||
return {
|
||||
title: this.labelTitle,
|
||||
color: this.selectedColor,
|
||||
[attributePath]: this.attrWorkspacePath,
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -1,16 +1,8 @@
|
|||
<script>
|
||||
import {
|
||||
GlDropdownForm,
|
||||
GlDropdownItem,
|
||||
GlLoadingIcon,
|
||||
GlSearchBoxByType,
|
||||
GlIntersectionObserver,
|
||||
} from '@gitlab/ui';
|
||||
import { GlDropdownForm, GlDropdownItem, GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
|
||||
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
||||
import { debounce } from 'lodash';
|
||||
import createFlash from '~/flash';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||
import { __ } from '~/locale';
|
||||
import { labelsQueries } from '~/sidebar/constants';
|
||||
import LabelItem from './label_item.vue';
|
||||
|
@ -20,7 +12,6 @@ export default {
|
|||
GlDropdownForm,
|
||||
GlDropdownItem,
|
||||
GlLoadingIcon,
|
||||
GlSearchBoxByType,
|
||||
GlIntersectionObserver,
|
||||
LabelItem,
|
||||
},
|
||||
|
@ -28,10 +19,6 @@ export default {
|
|||
prop: 'localSelectedLabels',
|
||||
},
|
||||
props: {
|
||||
selectedLabels: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
allowMultiselect: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
|
@ -48,10 +35,13 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
searchKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchKey: '',
|
||||
labels: [],
|
||||
isVisible: false,
|
||||
};
|
||||
|
@ -71,12 +61,6 @@ export default {
|
|||
return this.searchKey.length === 1 || !this.isVisible;
|
||||
},
|
||||
update: (data) => data.workspace?.labels?.nodes || [],
|
||||
async result() {
|
||||
if (this.$refs.searchInput) {
|
||||
await this.$nextTick;
|
||||
this.$refs.searchInput.focusInput();
|
||||
}
|
||||
},
|
||||
error() {
|
||||
createFlash({ message: __('Error fetching labels.') });
|
||||
},
|
||||
|
@ -101,12 +85,6 @@ export default {
|
|||
return Boolean(this.searchKey) && this.visibleLabels.length === 0;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.debouncedSearchKeyUpdate.cancel();
|
||||
},
|
||||
methods: {
|
||||
isLabelSelected(label) {
|
||||
return this.localSelectedLabelsIds.includes(getIdFromGraphQLId(label.id));
|
||||
|
@ -153,12 +131,8 @@ export default {
|
|||
this.$emit('closeDropdown', this.localSelectedLabels);
|
||||
}
|
||||
},
|
||||
setSearchKey(value) {
|
||||
this.searchKey = value;
|
||||
},
|
||||
onDropdownAppear() {
|
||||
this.isVisible = true;
|
||||
this.$refs.searchInput.focusInput();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -167,14 +141,6 @@ export default {
|
|||
<template>
|
||||
<gl-intersection-observer @appear="onDropdownAppear">
|
||||
<gl-dropdown-form class="labels-select-contents-list js-labels-list">
|
||||
<gl-search-box-by-type
|
||||
ref="searchInput"
|
||||
:value="searchKey"
|
||||
:disabled="labelsFetchInProgress"
|
||||
data-qa-selector="dropdown_input_field"
|
||||
data-testid="dropdown-input-field"
|
||||
@input="debouncedSearchKeyUpdate"
|
||||
/>
|
||||
<div ref="labelsListContainer" data-testid="dropdown-content">
|
||||
<gl-loading-icon
|
||||
v-if="labelsFetchInProgress"
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
<script>
|
||||
import { GlDropdownItem } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlDropdownItem,
|
||||
},
|
||||
inject: ['allowLabelCreate', 'labelsManagePath'],
|
||||
props: {
|
||||
footerCreateLabelTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
footerManageLabelTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div data-testid="dropdown-footer">
|
||||
<gl-dropdown-item
|
||||
v-if="allowLabelCreate"
|
||||
data-testid="create-label-button"
|
||||
@click.capture.native.stop="$emit('toggleDropdownContentsCreateView')"
|
||||
>
|
||||
{{ footerCreateLabelTitle }}
|
||||
</gl-dropdown-item>
|
||||
<gl-dropdown-item :href="labelsManagePath" @click.capture.native.stop>
|
||||
{{ footerManageLabelTitle }}
|
||||
</gl-dropdown-item>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,82 @@
|
|||
<script>
|
||||
import { GlButton, GlSearchBoxByType } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
GlSearchBoxByType,
|
||||
},
|
||||
model: {
|
||||
prop: 'searchKey',
|
||||
},
|
||||
props: {
|
||||
labelsCreateTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
labelsListTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
showDropdownContentsCreateView: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
labelsFetchInProgress: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
searchKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
dropdownTitle() {
|
||||
return this.showDropdownContentsCreateView ? this.labelsCreateTitle : this.labelsListTitle;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
focusInput() {
|
||||
this.$refs.searchInput.focusInput();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div data-testid="dropdown-header">
|
||||
<div class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!">
|
||||
<gl-button
|
||||
v-if="showDropdownContentsCreateView"
|
||||
:aria-label="__('Go back')"
|
||||
variant="link"
|
||||
size="small"
|
||||
class="js-btn-back dropdown-header-button gl-p-0"
|
||||
icon="arrow-left"
|
||||
data-testid="go-back-button"
|
||||
@click.stop="$emit('toggleDropdownContentsCreateView')"
|
||||
/>
|
||||
<span class="gl-flex-grow-1">{{ dropdownTitle }}</span>
|
||||
<gl-button
|
||||
:aria-label="__('Close')"
|
||||
variant="link"
|
||||
size="small"
|
||||
class="dropdown-header-button gl-p-0!"
|
||||
icon="close"
|
||||
data-testid="close-button"
|
||||
@click="$emit('closeDropdown')"
|
||||
/>
|
||||
</div>
|
||||
<gl-search-box-by-type
|
||||
v-if="!showDropdownContentsCreateView"
|
||||
ref="searchInput"
|
||||
:value="searchKey"
|
||||
:disabled="labelsFetchInProgress"
|
||||
data-qa-selector="dropdown_input_field"
|
||||
data-testid="dropdown-input-field"
|
||||
@input="$emit('input', $event)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -1,8 +1,10 @@
|
|||
<script>
|
||||
import { MutationOperationMode } from '~/graphql_shared/utils';
|
||||
import createFlash from '~/flash';
|
||||
import { IssuableType } from '~/issue_show/constants';
|
||||
import { __ } from '~/locale';
|
||||
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
|
||||
import { labelsQueries } from '~/sidebar/constants';
|
||||
import { labelsQueries, labelsMutations } from '~/sidebar/constants';
|
||||
import { DropdownVariant } from './constants';
|
||||
import DropdownContents from './dropdown_contents.vue';
|
||||
import DropdownValue from './dropdown_value.vue';
|
||||
|
@ -50,16 +52,6 @@ export default {
|
|||
required: false,
|
||||
default: DropdownVariant.Sidebar,
|
||||
},
|
||||
selectedLabels: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
labelsSelectInProgress: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
labelsFilterBasePath: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
@ -95,25 +87,25 @@ export default {
|
|||
required: false,
|
||||
default: __('Manage group labels'),
|
||||
},
|
||||
isEditing: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
issuableType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
attrWorkspacePath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: undefined,
|
||||
required: true,
|
||||
},
|
||||
labelType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
contentIsOnViewport: true,
|
||||
issuableLabels: [],
|
||||
labelsSelectInProgress: false,
|
||||
oldIid: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -143,9 +135,19 @@ export default {
|
|||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
iid(_, oldVal) {
|
||||
this.oldIid = oldVal;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleDropdownClose(labels) {
|
||||
this.$emit('updateSelectedLabels', labels);
|
||||
if (this.iid !== '') {
|
||||
this.updateSelectedLabels(this.getUpdateVariables(labels));
|
||||
} else {
|
||||
this.$emit('updateSelectedLabels', { labels });
|
||||
}
|
||||
|
||||
this.collapseEditableItem();
|
||||
},
|
||||
collapseEditableItem() {
|
||||
|
@ -154,6 +156,85 @@ export default {
|
|||
handleCollapsedValueClick() {
|
||||
this.$emit('toggleCollapse');
|
||||
},
|
||||
getUpdateVariables(labels) {
|
||||
let labelIds = [];
|
||||
|
||||
labelIds = labels.map(({ id }) => id);
|
||||
const currentIid = this.oldIid || this.iid;
|
||||
|
||||
const updateVariables = {
|
||||
iid: currentIid,
|
||||
projectPath: this.fullPath,
|
||||
labelIds,
|
||||
};
|
||||
|
||||
switch (this.issuableType) {
|
||||
case IssuableType.Issue:
|
||||
return updateVariables;
|
||||
case IssuableType.MergeRequest:
|
||||
updateVariables.operationMode = MutationOperationMode.Replace;
|
||||
return updateVariables;
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
},
|
||||
updateSelectedLabels(inputVariables) {
|
||||
this.labelsSelectInProgress = true;
|
||||
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: labelsMutations[this.issuableType].mutation,
|
||||
variables: { input: inputVariables },
|
||||
})
|
||||
.then(({ data }) => {
|
||||
const { mutationName } = labelsMutations[this.issuableType];
|
||||
|
||||
if (data[mutationName]?.errors?.length) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
this.$emit('updateSelectedLabels', {
|
||||
id: data[mutationName]?.[this.issuableType].id,
|
||||
labels: data[mutationName]?.[this.issuableType].labels?.nodes,
|
||||
});
|
||||
})
|
||||
.catch((error) =>
|
||||
createFlash({
|
||||
message: __('An error occurred while updating labels.'),
|
||||
captureError: true,
|
||||
error,
|
||||
}),
|
||||
)
|
||||
.finally(() => {
|
||||
this.labelsSelectInProgress = false;
|
||||
});
|
||||
},
|
||||
getRemoveVariables(labelId) {
|
||||
const removeVariables = {
|
||||
iid: this.iid,
|
||||
projectPath: this.fullPath,
|
||||
};
|
||||
|
||||
switch (this.issuableType) {
|
||||
case IssuableType.Issue:
|
||||
return {
|
||||
...removeVariables,
|
||||
removeLabelIds: [labelId],
|
||||
};
|
||||
case IssuableType.MergeRequest:
|
||||
return {
|
||||
...removeVariables,
|
||||
labelIds: [labelId],
|
||||
operationMode: MutationOperationMode.Remove,
|
||||
};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
},
|
||||
handleLabelRemove(labelId) {
|
||||
this.updateSelectedLabels(this.getRemoveVariables(labelId));
|
||||
this.$emit('onLabelRemove', labelId);
|
||||
},
|
||||
isDropdownVariantSidebar,
|
||||
isDropdownVariantStandalone,
|
||||
isDropdownVariantEmbedded,
|
||||
|
@ -180,6 +261,7 @@ export default {
|
|||
:title="__('Labels')"
|
||||
:loading="isLoading"
|
||||
:can-edit="allowLabelEdit"
|
||||
@open="oldIid = null"
|
||||
>
|
||||
<template #collapsed>
|
||||
<dropdown-value
|
||||
|
@ -188,7 +270,7 @@ export default {
|
|||
:allow-label-remove="allowLabelRemove"
|
||||
:labels-filter-base-path="labelsFilterBasePath"
|
||||
:labels-filter-param="labelsFilterParam"
|
||||
@onLabelRemove="$emit('onLabelRemove', $event)"
|
||||
@onLabelRemove="handleLabelRemove"
|
||||
>
|
||||
<slot></slot>
|
||||
</dropdown-value>
|
||||
|
@ -201,7 +283,7 @@ export default {
|
|||
:labels-filter-base-path="labelsFilterBasePath"
|
||||
:labels-filter-param="labelsFilterParam"
|
||||
class="gl-mb-2"
|
||||
@onLabelRemove="$emit('onLabelRemove', $event)"
|
||||
@onLabelRemove="handleLabelRemove"
|
||||
>
|
||||
<slot></slot>
|
||||
</dropdown-value>
|
||||
|
@ -212,12 +294,13 @@ export default {
|
|||
:footer-create-label-title="footerCreateLabelTitle"
|
||||
:footer-manage-label-title="footerManageLabelTitle"
|
||||
:labels-create-title="labelsCreateTitle"
|
||||
:selected-labels="selectedLabels"
|
||||
:selected-labels="issuableLabels"
|
||||
:variant="variant"
|
||||
:issuable-type="issuableType"
|
||||
:is-visible="edit"
|
||||
:full-path="fullPath"
|
||||
:attr-workspace-path="attrWorkspacePath"
|
||||
:label-type="labelType"
|
||||
@setLabels="handleDropdownClose"
|
||||
@closeDropdown="collapseEditableItem"
|
||||
/>
|
||||
|
@ -233,10 +316,12 @@ export default {
|
|||
:footer-create-label-title="footerCreateLabelTitle"
|
||||
:footer-manage-label-title="footerManageLabelTitle"
|
||||
:labels-create-title="labelsCreateTitle"
|
||||
:selected-labels="selectedLabels"
|
||||
:selected-labels="issuableLabels"
|
||||
:variant="variant"
|
||||
:issuable-type="issuableType"
|
||||
:full-path="fullPath"
|
||||
:attr-workspace-path="attrWorkspacePath"
|
||||
:label-type="labelType"
|
||||
@setLabels="handleDropdownClose"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -6,6 +6,8 @@ class Projects::CommitsController < Projects::ApplicationController
|
|||
include ExtractsPath
|
||||
include RendersCommits
|
||||
|
||||
COMMITS_DEFAULT_LIMIT = 40
|
||||
|
||||
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
|
||||
around_action :allow_gitaly_ref_name_caching
|
||||
before_action :require_non_empty_project
|
||||
|
@ -63,7 +65,9 @@ class Projects::CommitsController < Projects::ApplicationController
|
|||
|
||||
def set_commits
|
||||
render_404 unless @path.empty? || request.format == :atom || @repository.blob_at(@commit.id, @path) || @repository.tree(@commit.id, @path).entries.present?
|
||||
@limit = (params[:limit] || 40).to_i
|
||||
|
||||
limit = params[:limit].to_i
|
||||
@limit = limit > 0 ? limit : COMMITS_DEFAULT_LIMIT # limit can only ever be a positive number
|
||||
@offset = (params[:offset] || 0).to_i
|
||||
search = params[:search]
|
||||
author = params[:author]
|
||||
|
|
|
@ -284,9 +284,7 @@ module IssuablesHelper
|
|||
end
|
||||
|
||||
def issuables_count_for_state(issuable_type, state)
|
||||
store_in_cache = parent.is_a?(Group) ? parent.cached_issues_state_count_enabled? : false
|
||||
|
||||
Gitlab::IssuablesCountForState.new(finder, store_in_redis_cache: store_in_cache)[state]
|
||||
Gitlab::IssuablesCountForState.new(finder, store_in_redis_cache: true)[state]
|
||||
end
|
||||
|
||||
def close_issuable_path(issuable)
|
||||
|
@ -442,7 +440,7 @@ module IssuablesHelper
|
|||
end
|
||||
|
||||
def format_count(issuable_type, count, threshold)
|
||||
if issuable_type == :issues && parent.is_a?(Group) && parent.cached_issues_state_count_enabled?
|
||||
if issuable_type == :issues && parent.is_a?(Group)
|
||||
format_cached_count(threshold, count)
|
||||
else
|
||||
number_with_delimiter(count)
|
||||
|
|
|
@ -61,9 +61,9 @@ class ErrorTracking::ErrorEvent < ApplicationRecord
|
|||
pre_context = entry['pre_context']
|
||||
post_context = entry['post_context']
|
||||
|
||||
context += lines_with_position(pre_context, error_line_no - pre_context.size)
|
||||
context += lines_with_position(pre_context, error_line_no - pre_context.size) if pre_context
|
||||
context += lines_with_position([error_line], error_line_no)
|
||||
context += lines_with_position(post_context, error_line_no + 1)
|
||||
context += lines_with_position(post_context, error_line_no + 1) if post_context
|
||||
|
||||
context.reject(&:blank?)
|
||||
end
|
||||
|
|
|
@ -760,10 +760,6 @@ class Group < Namespace
|
|||
Timelog.in_group(self)
|
||||
end
|
||||
|
||||
def cached_issues_state_count_enabled?
|
||||
Feature.enabled?(:cached_issues_state_count, self, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
def organizations
|
||||
::CustomerRelations::Organization.where(group_id: self.id)
|
||||
end
|
||||
|
|
|
@ -2582,18 +2582,21 @@ class Project < ApplicationRecord
|
|||
config = Gitlab.config.incoming_email
|
||||
wildcard = Gitlab::IncomingEmail::WILDCARD_PLACEHOLDER
|
||||
|
||||
config.address&.gsub(wildcard, "#{full_path_slug}-#{id}-issue-")
|
||||
config.address&.gsub(wildcard, "#{full_path_slug}-#{default_service_desk_suffix}")
|
||||
end
|
||||
|
||||
def service_desk_custom_address
|
||||
return unless Gitlab::ServiceDeskEmail.enabled?
|
||||
|
||||
key = service_desk_setting&.project_key
|
||||
return unless key.present?
|
||||
key = service_desk_setting&.project_key || default_service_desk_suffix
|
||||
|
||||
Gitlab::ServiceDeskEmail.address_for_key("#{full_path_slug}-#{key}")
|
||||
end
|
||||
|
||||
def default_service_desk_suffix
|
||||
"#{id}-issue-"
|
||||
end
|
||||
|
||||
def root_namespace
|
||||
if namespace.has_parent?
|
||||
namespace.root_ancestor
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
%li= s_("GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility.")
|
||||
|
||||
- if group.paid?
|
||||
.gl-alert.gl-alert-info.gl-mb-5{ data: { testid: 'group-to-transfer-has-linked-subscription-alert' } }
|
||||
.gl-alert.gl-alert-info.gl-mb-5
|
||||
= sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
|
||||
.gl-alert-body
|
||||
= html_escape(_("This group can't be transfered because it is linked to a subscription. To transfer this group, %{linkStart}link the subscription%{linkEnd} with a different group.")) % { linkStart: "<a href=\"#{help_page_path('subscriptions/index', anchor: 'change-the-linked-namespace')}\">".html_safe, linkEnd: '</a>'.html_safe }
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: cached_issues_state_count
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67418
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/333089
|
||||
milestone: '14.3'
|
||||
type: development
|
||||
group: group::product planning
|
||||
default_enabled: false
|
|
@ -102,14 +102,12 @@ track_action: "click_button" })
|
|||
### Implement Vue component tracking
|
||||
|
||||
For custom event tracking, use a Vue `mixin` in components. Vue `mixin` exposes the `Tracking.event`
|
||||
static method and the `track` method called from components or templates. You can specify tracking
|
||||
options in `data` or `computed`. These options override any defaults and allow the values to be dynamic
|
||||
from props or based on state.
|
||||
static method and the `track` method. You can specify tracking options in `data` or `computed`.
|
||||
These options override any defaults and allow the values to be dynamic from props or based on state.
|
||||
|
||||
Default options are passed when an event is tracked from the component. If you don't specify an option,
|
||||
the default `document.body.dataset.page` is used. The default options are:
|
||||
Several default options are passed when an event is tracked from the component:
|
||||
|
||||
- `category`
|
||||
- `category`: If you don't specify, by default `document.body.dataset.page` is used.
|
||||
- `label`
|
||||
- `property`
|
||||
- `value`
|
||||
|
|
|
@ -166,13 +166,13 @@ To edit the custom email display name:
|
|||
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/284656) in GitLab 13.8.
|
||||
|
||||
It is possible to customize the email address used by Service Desk. To do this, you must configure
|
||||
both a [custom mailbox](#configuring-a-custom-mailbox) and a
|
||||
a [custom mailbox](#configuring-a-custom-mailbox). If you want you can also configure a
|
||||
[custom suffix](#configuring-a-custom-email-address-suffix).
|
||||
|
||||
#### Configuring a custom mailbox
|
||||
|
||||
NOTE:
|
||||
On GitLab.com a custom mailbox is already configured with `contact-project+%{key}@incoming.gitlab.com` as the email address, so you only have to configure the
|
||||
On GitLab.com a custom mailbox is already configured with `contact-project+%{key}@incoming.gitlab.com` as the email address, you can still configure the
|
||||
[custom suffix](#configuring-a-custom-email-address-suffix) in project settings.
|
||||
|
||||
Using the `service_desk_email` configuration, you can customize the mailbox
|
||||
|
@ -271,6 +271,8 @@ For example, suppose the `mygroup/myproject` project Service Desk settings has t
|
|||
The Service Desk email address for this project is: `contact+mygroup-myproject-support@example.com`.
|
||||
The [incoming email](../../administration/incoming_email.md) address still works.
|
||||
|
||||
If you don't configure the custom suffix, the default project identification will be used for identifying the project. You can see that email address in the project settings.
|
||||
|
||||
## Using Service Desk
|
||||
|
||||
You can use Service Desk to [create an issue](#as-an-end-user-issue-creator) or [respond to one](#as-a-responder-to-the-issue).
|
||||
|
|
|
@ -15,16 +15,14 @@ module Gitlab
|
|||
PROJECT_KEY_PATTERN = /\A(?<slug>.+)-(?<key>[a-z0-9_]+)\z/.freeze
|
||||
|
||||
def initialize(mail, mail_key, service_desk_key: nil)
|
||||
if service_desk_key
|
||||
mail_key ||= service_desk_key
|
||||
@service_desk_key = service_desk_key
|
||||
end
|
||||
|
||||
super(mail, mail_key)
|
||||
|
||||
if service_desk_key.present?
|
||||
@service_desk_key = service_desk_key
|
||||
elsif !mail_key&.include?('/') && (matched = HANDLER_REGEX.match(mail_key.to_s))
|
||||
@project_slug = matched[:project_slug]
|
||||
@project_id = matched[:project_id]&.to_i
|
||||
elsif matched = HANDLER_REGEX_LEGACY.match(mail_key.to_s)
|
||||
@project_path = matched[:project_path]
|
||||
end
|
||||
match_project_slug || match_legacy_project_slug
|
||||
end
|
||||
|
||||
def can_handle?
|
||||
|
@ -42,15 +40,29 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def match_project_slug
|
||||
return if mail_key&.include?('/')
|
||||
return unless matched = HANDLER_REGEX.match(mail_key.to_s)
|
||||
|
||||
@project_slug = matched[:project_slug]
|
||||
@project_id = matched[:project_id]&.to_i
|
||||
end
|
||||
|
||||
def match_legacy_project_slug
|
||||
return unless matched = HANDLER_REGEX_LEGACY.match(mail_key.to_s)
|
||||
|
||||
@project_path = matched[:project_path]
|
||||
end
|
||||
|
||||
def metrics_event
|
||||
:receive_email_service_desk
|
||||
end
|
||||
|
||||
def project
|
||||
strong_memoize(:project) do
|
||||
@project = service_desk_key ? project_from_key : super
|
||||
@project = nil unless @project&.service_desk_enabled?
|
||||
@project
|
||||
project_record = super
|
||||
project_record ||= project_from_key if service_desk_key
|
||||
project_record&.service_desk_enabled? ? project_record : nil
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -59,6 +59,9 @@ module QA
|
|||
}
|
||||
end
|
||||
|
||||
# Get import status
|
||||
#
|
||||
# @return [String]
|
||||
def import_status
|
||||
response = get(Runtime::API::Request.new(api_client, "/bulk_imports/#{import_id}").url)
|
||||
|
||||
|
@ -69,6 +72,15 @@ module QA
|
|||
parse_body(response)[:status]
|
||||
end
|
||||
|
||||
# Get import details
|
||||
#
|
||||
# @return [Array]
|
||||
def import_details
|
||||
response = get(Runtime::API::Request.new(api_client, "/bulk_imports/#{import_id}/entities").url)
|
||||
|
||||
parse_body(response)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def transform_api_resource(api_resource)
|
||||
|
|
|
@ -48,6 +48,10 @@ module QA
|
|||
imported_group.reload!.projects
|
||||
end
|
||||
|
||||
let(:import_details) do
|
||||
imported_group.import_details.find { |entity| entity[:destination_name] == source_project.name }
|
||||
end
|
||||
|
||||
before do
|
||||
Runtime::Feature.enable(:bulk_import_projects)
|
||||
Runtime::Feature.enable(:top_level_group_creation_enabled) if staging?
|
||||
|
@ -70,6 +74,7 @@ module QA
|
|||
testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/2297'
|
||||
) do
|
||||
expect { imported_group.import_status }.to eventually_eq('finished').within(import_wait_duration)
|
||||
expect(import_details[:failures]).to be_empty, "Expected to not have import errors, was: #{import_details}"
|
||||
|
||||
aggregate_failures do
|
||||
expect(imported_projects.count).to eq(1)
|
||||
|
@ -109,6 +114,7 @@ module QA
|
|||
testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/2325'
|
||||
) do
|
||||
expect { imported_group.import_status }.to eventually_eq('finished').within(import_wait_duration)
|
||||
expect(import_details[:failures]).to be_empty, "Expected to not have import errors, was: #{import_details}"
|
||||
|
||||
aggregate_failures do
|
||||
expect(imported_issues.count).to eq(1)
|
||||
|
|
|
@ -67,6 +67,29 @@ RSpec.describe Projects::CommitsController do
|
|||
end
|
||||
end
|
||||
|
||||
context "with an invalid limit" do
|
||||
let(:id) { "master/README.md" }
|
||||
|
||||
it "uses the default limit" do
|
||||
expect_any_instance_of(Repository).to receive(:commits).with(
|
||||
"master",
|
||||
path: "README.md",
|
||||
limit: described_class::COMMITS_DEFAULT_LIMIT,
|
||||
offset: 0
|
||||
).and_call_original
|
||||
|
||||
get(:show,
|
||||
params: {
|
||||
namespace_id: project.namespace,
|
||||
project_id: project,
|
||||
id: id,
|
||||
limit: "foo"
|
||||
})
|
||||
|
||||
expect(response).to be_successful
|
||||
end
|
||||
end
|
||||
|
||||
context "when the ref name ends in .atom" do
|
||||
context "when the ref does not exist with the suffix" do
|
||||
before do
|
||||
|
|
|
@ -63,5 +63,9 @@ FactoryBot.define do
|
|||
level { 'error' }
|
||||
occurred_at { Time.now.iso8601 }
|
||||
payload { Gitlab::Json.parse(File.read(Rails.root.join('spec/fixtures/', 'error_tracking/parsed_event.json'))) }
|
||||
|
||||
trait :browser do
|
||||
payload { Gitlab::Json.parse(File.read(Rails.root.join('spec/fixtures/', 'error_tracking/browser_event.json'))) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -94,7 +94,7 @@ RSpec.describe 'Admin Appearance' do
|
|||
sign_in(admin)
|
||||
gitlab_enable_admin_mode_sign_in(admin)
|
||||
visit new_project_path
|
||||
find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
|
||||
click_link 'Create blank project'
|
||||
|
||||
expect_custom_new_project_appearance(appearance)
|
||||
end
|
||||
|
|
|
@ -83,6 +83,18 @@ RSpec.describe 'Group issues page' do
|
|||
end
|
||||
end
|
||||
|
||||
it 'truncates issue counts if over the threshold', :clean_gitlab_redis_cache do
|
||||
allow(Rails.cache).to receive(:read).and_call_original
|
||||
allow(Rails.cache).to receive(:read).with(
|
||||
['group', group.id, 'issues'],
|
||||
{ expires_in: Gitlab::IssuablesCountForState::CACHE_EXPIRES_IN }
|
||||
).and_return({ opened: 1050, closed: 500, all: 1550 })
|
||||
|
||||
visit issues_group_path(group)
|
||||
|
||||
expect(page).to have_text('Open 1.1k Closed 500 All 1.6k')
|
||||
end
|
||||
|
||||
context 'when project is archived' do
|
||||
before do
|
||||
::Projects::UpdateService.new(project, user_in_group, archived: true).execute
|
||||
|
@ -94,41 +106,6 @@ RSpec.describe 'Group issues page' do
|
|||
expect(page).not_to have_content issue.title[0..80]
|
||||
end
|
||||
end
|
||||
|
||||
context 'when cached issues state count is enabled', :clean_gitlab_redis_cache do
|
||||
before do
|
||||
stub_feature_flags(cached_issues_state_count: true)
|
||||
end
|
||||
|
||||
it 'truncates issue counts if over the threshold' do
|
||||
allow(Rails.cache).to receive(:read).and_call_original
|
||||
allow(Rails.cache).to receive(:read).with(
|
||||
['group', group.id, 'issues'],
|
||||
{ expires_in: Gitlab::IssuablesCountForState::CACHE_EXPIRES_IN }
|
||||
).and_return({ opened: 1050, closed: 500, all: 1550 })
|
||||
|
||||
visit issues_group_path(group)
|
||||
|
||||
expect(page).to have_text('Open 1.1k Closed 500 All 1.6k')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when cached issues state count is disabled', :clean_gitlab_redis_cache do
|
||||
before do
|
||||
stub_feature_flags(cached_issues_state_count: false)
|
||||
end
|
||||
|
||||
it 'does not truncate counts if they are over the threshold' do
|
||||
allow_next_instance_of(IssuesFinder) do |finder|
|
||||
allow(finder).to receive(:count_by_state).and_return(true)
|
||||
.and_return({ opened: 1050, closed: 500, all: 1550 })
|
||||
end
|
||||
|
||||
visit issues_group_path(group)
|
||||
|
||||
expect(page).to have_text('Open 1,050 Closed 500 All 1,550')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'projects with issues disabled' do
|
||||
|
|
|
@ -21,7 +21,7 @@ RSpec.describe 'Project variables', :js do
|
|||
click_button('Add variable')
|
||||
|
||||
page.within('#add-ci-variable') do
|
||||
find('[data-qa-selector="ci_variable_key_field"] input').set('akey') # rubocop:disable QA/SelectorUsage
|
||||
fill_in 'Key', with: 'akey'
|
||||
find('#ci-variable-value').set('akey_value')
|
||||
find('[data-testid="environment-scope"]').click
|
||||
find('[data-testid="ci-environment-search"]').set('review/*')
|
||||
|
|
|
@ -31,7 +31,7 @@ RSpec.describe 'Import/Export - project import integration test', :js do
|
|||
|
||||
it 'user imports an exported project successfully', :sidekiq_might_not_need_inline do
|
||||
visit new_project_path
|
||||
click_import_project
|
||||
click_link 'Import project'
|
||||
click_link 'GitLab export'
|
||||
|
||||
fill_in :name, with: 'Test Project Name', visible: true
|
||||
|
@ -50,7 +50,7 @@ RSpec.describe 'Import/Export - project import integration test', :js do
|
|||
|
||||
visit new_project_path
|
||||
|
||||
click_import_project
|
||||
click_link 'Import project'
|
||||
click_link 'GitLab export'
|
||||
fill_in :name, with: project.name, visible: true
|
||||
attach_file('file', file)
|
||||
|
@ -61,8 +61,4 @@ RSpec.describe 'Import/Export - project import integration test', :js do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def click_import_project
|
||||
find('[data-qa-panel-name="import_project"]').click # rubocop:disable QA/SelectorUsage
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,7 +23,7 @@ RSpec.describe 'New project', :js do
|
|||
)
|
||||
|
||||
visit new_project_path
|
||||
find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
|
||||
click_link 'Create blank project'
|
||||
|
||||
expect(page).to have_content 'Other visibility settings have been disabled by the administrator.'
|
||||
end
|
||||
|
@ -34,7 +34,7 @@ RSpec.describe 'New project', :js do
|
|||
)
|
||||
|
||||
visit new_project_path
|
||||
find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
|
||||
click_link 'Create blank project'
|
||||
|
||||
expect(page).to have_content 'Visibility settings have been disabled by the administrator.'
|
||||
end
|
||||
|
@ -49,14 +49,14 @@ RSpec.describe 'New project', :js do
|
|||
|
||||
it 'shows "New project" page', :js do
|
||||
visit new_project_path
|
||||
find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
|
||||
click_link 'Create blank project'
|
||||
|
||||
expect(page).to have_content('Project name')
|
||||
expect(page).to have_content('Project URL')
|
||||
expect(page).to have_content('Project slug')
|
||||
|
||||
click_link('New project')
|
||||
find('[data-qa-panel-name="import_project"]').click # rubocop:disable QA/SelectorUsage
|
||||
click_link 'Import project'
|
||||
|
||||
expect(page).to have_link('GitHub')
|
||||
expect(page).to have_link('Bitbucket')
|
||||
|
@ -69,7 +69,7 @@ RSpec.describe 'New project', :js do
|
|||
before do
|
||||
visit new_project_path
|
||||
|
||||
find('[data-qa-panel-name="import_project"]').click # rubocop:disable QA/SelectorUsage
|
||||
click_link 'Import project'
|
||||
end
|
||||
|
||||
it 'has Manifest file' do
|
||||
|
@ -83,7 +83,7 @@ RSpec.describe 'New project', :js do
|
|||
stub_application_setting(default_project_visibility: level)
|
||||
|
||||
visit new_project_path
|
||||
find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
|
||||
click_link 'Create blank project'
|
||||
page.within('#blank-project-pane') do
|
||||
expect(find_field("project_visibility_level_#{level}")).to be_checked
|
||||
end
|
||||
|
@ -91,7 +91,7 @@ RSpec.describe 'New project', :js do
|
|||
|
||||
it "saves visibility level #{level} on validation error" do
|
||||
visit new_project_path
|
||||
find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
|
||||
click_link 'Create blank project'
|
||||
|
||||
choose(key)
|
||||
click_button('Create project')
|
||||
|
@ -111,7 +111,7 @@ RSpec.describe 'New project', :js do
|
|||
context 'when admin mode is enabled', :enable_admin_mode do
|
||||
it 'has private selected' do
|
||||
visit new_project_path(namespace_id: group.id)
|
||||
find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
|
||||
click_link 'Create blank project'
|
||||
|
||||
page.within('#blank-project-pane') do
|
||||
expect(find_field("project_visibility_level_#{Gitlab::VisibilityLevel::PRIVATE}")).to be_checked
|
||||
|
@ -138,7 +138,7 @@ RSpec.describe 'New project', :js do
|
|||
context 'when admin mode is enabled', :enable_admin_mode do
|
||||
it 'has private selected' do
|
||||
visit new_project_path(namespace_id: group.id, project: { visibility_level: Gitlab::VisibilityLevel::PRIVATE })
|
||||
find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
|
||||
click_link 'Create blank project'
|
||||
|
||||
page.within('#blank-project-pane') do
|
||||
expect(find_field("project_visibility_level_#{Gitlab::VisibilityLevel::PRIVATE}")).to be_checked
|
||||
|
@ -159,7 +159,7 @@ RSpec.describe 'New project', :js do
|
|||
context 'Readme selector' do
|
||||
it 'shows the initialize with Readme checkbox on "Blank project" tab' do
|
||||
visit new_project_path
|
||||
find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
|
||||
click_link 'Create blank project'
|
||||
|
||||
expect(page).to have_css('input#project_initialize_with_readme')
|
||||
expect(page).to have_content('Initialize repository with a README')
|
||||
|
@ -167,7 +167,7 @@ RSpec.describe 'New project', :js do
|
|||
|
||||
it 'does not show the initialize with Readme checkbox on "Create from template" tab' do
|
||||
visit new_project_path
|
||||
find('[data-qa-panel-name="create_from_template"]').click # rubocop:disable QA/SelectorUsage
|
||||
click_link 'Create from template'
|
||||
first('.choose-template').click
|
||||
|
||||
page.within '.project-fields-form' do
|
||||
|
@ -178,7 +178,7 @@ RSpec.describe 'New project', :js do
|
|||
|
||||
it 'does not show the initialize with Readme checkbox on "Import project" tab' do
|
||||
visit new_project_path
|
||||
find('[data-qa-panel-name="import_project"]').click # rubocop:disable QA/SelectorUsage
|
||||
click_link 'Import project'
|
||||
first('.js-import-git-toggle-button').click
|
||||
|
||||
page.within '#import-project-pane' do
|
||||
|
@ -192,7 +192,7 @@ RSpec.describe 'New project', :js do
|
|||
context 'with user namespace' do
|
||||
before do
|
||||
visit new_project_path
|
||||
find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
|
||||
click_link 'Create blank project'
|
||||
end
|
||||
|
||||
it 'selects the user namespace' do
|
||||
|
@ -208,7 +208,7 @@ RSpec.describe 'New project', :js do
|
|||
before do
|
||||
group.add_owner(user)
|
||||
visit new_project_path(namespace_id: group.id)
|
||||
find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
|
||||
click_link 'Create blank project'
|
||||
end
|
||||
|
||||
it 'selects the group namespace' do
|
||||
|
@ -225,7 +225,7 @@ RSpec.describe 'New project', :js do
|
|||
before do
|
||||
group.add_maintainer(user)
|
||||
visit new_project_path(namespace_id: subgroup.id)
|
||||
find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
|
||||
click_link 'Create blank project'
|
||||
end
|
||||
|
||||
it 'selects the group namespace' do
|
||||
|
@ -245,7 +245,7 @@ RSpec.describe 'New project', :js do
|
|||
internal_group.add_owner(user)
|
||||
private_group.add_owner(user)
|
||||
visit new_project_path(namespace_id: public_group.id)
|
||||
find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
|
||||
click_link 'Create blank project'
|
||||
end
|
||||
|
||||
it 'enables the correct visibility options' do
|
||||
|
@ -275,7 +275,7 @@ RSpec.describe 'New project', :js do
|
|||
context 'Import project options', :js do
|
||||
before do
|
||||
visit new_project_path
|
||||
find('[data-qa-panel-name="import_project"]').click # rubocop:disable QA/SelectorUsage
|
||||
click_link 'Import project'
|
||||
end
|
||||
|
||||
context 'from git repository url, "Repo by URL"' do
|
||||
|
@ -351,7 +351,7 @@ RSpec.describe 'New project', :js do
|
|||
before do
|
||||
group.add_developer(user)
|
||||
visit new_project_path(namespace_id: group.id)
|
||||
find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
|
||||
click_link 'Create blank project'
|
||||
end
|
||||
|
||||
it 'selects the group namespace' do
|
||||
|
|
|
@ -54,7 +54,7 @@ RSpec.describe 'Service Desk Setting', :js, :clean_gitlab_redis_cache do
|
|||
wait_for_requests
|
||||
|
||||
project.reload
|
||||
expect(find('[data-testid="incoming-email"]').value).to eq(project.service_desk_incoming_address)
|
||||
expect(find('[data-testid="incoming-email"]').value).to eq(project.service_desk_custom_address)
|
||||
|
||||
page.within '#js-service-desk' do
|
||||
fill_in('service-desk-project-suffix', with: 'foo')
|
||||
|
|
|
@ -14,7 +14,7 @@ RSpec.describe 'User creates a project', :js do
|
|||
it 'creates a new project' do
|
||||
visit(new_project_path)
|
||||
|
||||
find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
|
||||
click_link 'Create blank project'
|
||||
fill_in(:project_name, with: 'Empty')
|
||||
|
||||
expect(page).to have_checked_field 'Initialize repository with a README'
|
||||
|
@ -38,7 +38,7 @@ RSpec.describe 'User creates a project', :js do
|
|||
|
||||
visit(new_project_path)
|
||||
|
||||
find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
|
||||
click_link 'Create blank project'
|
||||
fill_in(:project_name, with: 'With initial commits')
|
||||
|
||||
expect(page).to have_checked_field 'Initialize repository with a README'
|
||||
|
@ -67,7 +67,7 @@ RSpec.describe 'User creates a project', :js do
|
|||
it 'creates a new project' do
|
||||
visit(new_project_path)
|
||||
|
||||
find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
|
||||
click_link 'Create blank project'
|
||||
fill_in :project_name, with: 'A Subgroup Project'
|
||||
fill_in :project_path, with: 'a-subgroup-project'
|
||||
|
||||
|
@ -96,7 +96,7 @@ RSpec.describe 'User creates a project', :js do
|
|||
it 'creates a new project' do
|
||||
visit(new_project_path)
|
||||
|
||||
find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
|
||||
click_link 'Create blank project'
|
||||
fill_in :project_name, with: 'a-new-project'
|
||||
fill_in :project_path, with: 'a-new-project'
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ RSpec.describe 'Project' do
|
|||
|
||||
shared_examples 'creates from template' do |template, sub_template_tab = nil|
|
||||
it "is created from template", :js do
|
||||
find('[data-qa-panel-name="create_from_template"]').click # rubocop:disable QA/SelectorUsage
|
||||
click_link 'Create from template'
|
||||
find(".project-template #{sub_template_tab}").click if sub_template_tab
|
||||
find("label[for=#{template.name}]").click
|
||||
fill_in("project_name", with: template.name)
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
Return-Path: <jake@adventuretime.ooo>
|
||||
Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
|
||||
Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <support+project_slug-project_key@example.com>; Thu, 13 Jun 2013 14:03:48 -0700
|
||||
Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
|
||||
Date: Thu, 13 Jun 2013 17:03:48 -0400
|
||||
From: Jake the Dog <jake@adventuretime.ooo>
|
||||
To: support+email-test-project_id-issue-@example.com
|
||||
Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
|
||||
Subject: The message subject! @all
|
||||
Mime-Version: 1.0
|
||||
Content-Type: text/plain;
|
||||
charset=ISO-8859-1
|
||||
Content-Transfer-Encoding: 7bit
|
||||
X-Sieve: CMU Sieve 2.2
|
||||
X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
|
||||
13 Jun 2013 14:03:48 -0700 (PDT)
|
||||
X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
|
||||
|
||||
Service desk stuff!
|
||||
|
||||
```
|
||||
a = b
|
||||
```
|
||||
|
||||
/label ~label1
|
||||
/assign @user1
|
||||
/close
|
|
@ -0,0 +1 @@
|
|||
{"sdk":{"name":"sentry.javascript.browser","version":"5.7.1","packages":[{"name":"npm:@sentry/browser","version":"5.7.1"}],"integrations":["InboundFilters","FunctionToString","TryCatch","Breadcrumbs","GlobalHandlers","LinkedErrors","UserAgent","Dedupe","ExtraErrorData","ReportingObserver","RewriteFrames","Vue"]},"level":"error","request":{"url":"http://localhost:5444/","headers":{"User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:91.0) Gecko/20100101 Firefox/91.0"}},"event_id":"6a32dc45cd924196930e06aa21b48c8d","platform":"javascript","exception":{"values":[{"type":"TypeError","value":"Cannot read property 'filter' of undefined","mechanism":{"type":"generic","handled":true},"stacktrace":{"frames":[{"colno":34,"in_app":true,"lineno":6395,"filename":"webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js","function":"hydrate"},{"colno":57,"in_app":true,"lineno":6362,"filename":"webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js","function":"hydrate"},{"colno":13,"in_app":true,"lineno":3115,"filename":"webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js","function":"init"},{"colno":10,"in_app":true,"lineno":8399,"filename":"webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js","function":"Vue.prototype.$mount"},{"colno":3,"in_app":true,"lineno":4061,"filename":"webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js","function":"mountComponent"},{"colno":12,"in_app":true,"lineno":4456,"filename":"webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js","function":"Watcher"},{"colno":25,"in_app":true,"lineno":4467,"filename":"webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js","function":"get"},{"colno":10,"in_app":true,"lineno":4048,"filename":"webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js","function":"updateComponent"},{"colno":19,"in_app":true,"lineno":3933,"filename":"webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js","function":"lifecycleMixin/Vue.prototype._update"},{"colno":24,"in_app":true,"lineno":6477,"filename":"webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js","function":"patch"},{"colno":34,"in_app":true,"lineno":6395,"filename":"webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js","function":"hydrate"},{"colno":64,"in_app":true,"lineno":78,"filename":"webpack-internal:///./node_modules/babel-loader/lib/index.js?!./node_modules/vue-loader/lib/index.js?!./pages/index.vue?vue&type=script&lang=js&","function":"data"}]}}]},"environment":"development"}
|
|
@ -105,6 +105,7 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => {
|
|||
describe('when labels are updated over existing labels', () => {
|
||||
const testLabelsPayload = [
|
||||
{ id: 5, set: true },
|
||||
{ id: 6, set: false },
|
||||
{ id: 7, set: true },
|
||||
];
|
||||
const expectedLabels = [{ id: 5 }, { id: 7 }];
|
||||
|
|
|
@ -27,6 +27,7 @@ import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql'
|
|||
import actions from '~/boards/stores/actions';
|
||||
import * as types from '~/boards/stores/mutation_types';
|
||||
import mutations from '~/boards/stores/mutations';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
|
||||
import {
|
||||
mockLists,
|
||||
|
@ -1572,12 +1573,13 @@ describe('setActiveIssueLabels', () => {
|
|||
const getters = { activeBoardItem: mockIssue };
|
||||
const testLabelIds = labels.map((label) => label.id);
|
||||
const input = {
|
||||
addLabelIds: testLabelIds,
|
||||
labelIds: testLabelIds,
|
||||
removeLabelIds: [],
|
||||
projectPath: 'h/b',
|
||||
labels,
|
||||
};
|
||||
|
||||
it('should assign labels on success, and sets loading state for labels', (done) => {
|
||||
it('should assign labels on success', (done) => {
|
||||
jest
|
||||
.spyOn(gqlClient, 'mutate')
|
||||
.mockResolvedValue({ data: { updateIssue: { issue: { labels: { nodes: labels } } } } });
|
||||
|
@ -1593,14 +1595,6 @@ describe('setActiveIssueLabels', () => {
|
|||
input,
|
||||
{ ...state, ...getters },
|
||||
[
|
||||
{
|
||||
type: types.SET_LABELS_LOADING,
|
||||
payload: true,
|
||||
},
|
||||
{
|
||||
type: types.SET_LABELS_LOADING,
|
||||
payload: false,
|
||||
},
|
||||
{
|
||||
type: types.UPDATE_BOARD_ITEM_BY_ID,
|
||||
payload,
|
||||
|
@ -1618,6 +1612,64 @@ describe('setActiveIssueLabels', () => {
|
|||
|
||||
await expect(actions.setActiveIssueLabels({ getters }, input)).rejects.toThrow(Error);
|
||||
});
|
||||
|
||||
describe('labels_widget FF on', () => {
|
||||
beforeEach(() => {
|
||||
window.gon = {
|
||||
features: { labelsWidget: true },
|
||||
};
|
||||
|
||||
getters.activeBoardItem = { ...mockIssue, labels };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.gon = {
|
||||
features: {},
|
||||
};
|
||||
});
|
||||
|
||||
it('should assign labels', () => {
|
||||
const payload = {
|
||||
itemId: getters.activeBoardItem.id,
|
||||
prop: 'labels',
|
||||
value: labels,
|
||||
};
|
||||
|
||||
testAction(
|
||||
actions.setActiveIssueLabels,
|
||||
input,
|
||||
{ ...state, ...getters },
|
||||
[
|
||||
{
|
||||
type: types.UPDATE_BOARD_ITEM_BY_ID,
|
||||
payload,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove label', () => {
|
||||
const payload = {
|
||||
itemId: getters.activeBoardItem.id,
|
||||
prop: 'labels',
|
||||
value: [labels[1]],
|
||||
};
|
||||
|
||||
testAction(
|
||||
actions.setActiveIssueLabels,
|
||||
{ ...input, removeLabelIds: [getIdFromGraphQLId(labels[0].id)] },
|
||||
{ ...state, ...getters },
|
||||
[
|
||||
{
|
||||
type: types.UPDATE_BOARD_ITEM_BY_ID,
|
||||
payload,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setActiveItemSubscribed', () => {
|
||||
|
|
|
@ -51,6 +51,7 @@ describe('DropdownContentsCreateView', () => {
|
|||
const createComponent = ({
|
||||
mutationHandler = createLabelSuccessHandler,
|
||||
issuableType = IssuableType.Issue,
|
||||
labelType = 'ProjectLabel',
|
||||
} = {}) => {
|
||||
const mockApollo = createMockApollo([[createLabelMutation, mutationHandler]]);
|
||||
mockApollo.clients.defaultClient.cache.writeQuery({
|
||||
|
@ -68,6 +69,8 @@ describe('DropdownContentsCreateView', () => {
|
|||
propsData: {
|
||||
issuableType,
|
||||
fullPath: '',
|
||||
attrWorkspacePath: '',
|
||||
labelType,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -174,7 +177,7 @@ describe('DropdownContentsCreateView', () => {
|
|||
});
|
||||
|
||||
it('calls a mutation with `groupPath` variable on the epic', () => {
|
||||
createComponent({ issuableType: IssuableType.Epic });
|
||||
createComponent({ issuableType: IssuableType.Epic, labelType: 'GroupLabel' });
|
||||
fillLabelAttributes();
|
||||
findCreateButton().vm.$emit('click');
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@ describe('DropdownContentsLabelsView', () => {
|
|||
initialState = mockConfig,
|
||||
queryHandler = successfulQueryHandler,
|
||||
injected = {},
|
||||
searchKey = '',
|
||||
} = {}) => {
|
||||
const mockApollo = createMockApollo([[projectLabelsQuery, queryHandler]]);
|
||||
|
||||
|
@ -57,6 +58,7 @@ describe('DropdownContentsLabelsView', () => {
|
|||
...initialState,
|
||||
localSelectedLabels,
|
||||
issuableType: IssuableType.Issue,
|
||||
searchKey,
|
||||
},
|
||||
stubs: {
|
||||
GlSearchBoxByType,
|
||||
|
@ -68,7 +70,6 @@ describe('DropdownContentsLabelsView', () => {
|
|||
wrapper.destroy();
|
||||
});
|
||||
|
||||
const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
|
||||
const findLabels = () => wrapper.findAllComponents(LabelItem);
|
||||
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||
const findObserver = () => wrapper.findComponent(GlIntersectionObserver);
|
||||
|
@ -81,12 +82,6 @@ describe('DropdownContentsLabelsView', () => {
|
|||
}
|
||||
|
||||
describe('when loading labels', () => {
|
||||
it('renders disabled search input field', async () => {
|
||||
createComponent();
|
||||
await makeObserverAppear();
|
||||
expect(findSearchInput().props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('renders loading icon', async () => {
|
||||
createComponent();
|
||||
await makeObserverAppear();
|
||||
|
@ -107,10 +102,6 @@ describe('DropdownContentsLabelsView', () => {
|
|||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('renders enabled search input field', async () => {
|
||||
expect(findSearchInput().props('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not render loading icon', async () => {
|
||||
expect(findLoadingIcon().exists()).toBe(false);
|
||||
});
|
||||
|
@ -132,9 +123,9 @@ describe('DropdownContentsLabelsView', () => {
|
|||
},
|
||||
},
|
||||
}),
|
||||
searchKey: '123',
|
||||
});
|
||||
await makeObserverAppear();
|
||||
findSearchInput().vm.$emit('input', '123');
|
||||
await waitForPromises();
|
||||
await nextTick();
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@ import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_w
|
|||
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue';
|
||||
import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue';
|
||||
import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue';
|
||||
import DropdownHeader from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue';
|
||||
import DropdownFooter from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue';
|
||||
|
||||
import { mockLabels } from './mock_data';
|
||||
|
||||
|
@ -26,7 +28,7 @@ const GlDropdownStub = {
|
|||
describe('DropdownContent', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = ({ props = {}, injected = {}, data = {} } = {}) => {
|
||||
const createComponent = ({ props = {}, data = {} } = {}) => {
|
||||
wrapper = shallowMount(DropdownContents, {
|
||||
propsData: {
|
||||
labelsCreateTitle: 'test',
|
||||
|
@ -39,6 +41,8 @@ describe('DropdownContent', () => {
|
|||
variant: 'sidebar',
|
||||
issuableType: 'issue',
|
||||
fullPath: 'test',
|
||||
labelType: 'ProjectLabel',
|
||||
attrWorkspacePath: 'path',
|
||||
...props,
|
||||
},
|
||||
data() {
|
||||
|
@ -46,11 +50,6 @@ describe('DropdownContent', () => {
|
|||
...data,
|
||||
};
|
||||
},
|
||||
provide: {
|
||||
allowLabelCreate: true,
|
||||
labelsManagePath: 'foo/bar',
|
||||
...injected,
|
||||
},
|
||||
stubs: {
|
||||
GlDropdown: GlDropdownStub,
|
||||
},
|
||||
|
@ -63,13 +62,10 @@ describe('DropdownContent', () => {
|
|||
|
||||
const findCreateView = () => wrapper.findComponent(DropdownContentsCreateView);
|
||||
const findLabelsView = () => wrapper.findComponent(DropdownContentsLabelsView);
|
||||
const findDropdownHeader = () => wrapper.findComponent(DropdownHeader);
|
||||
const findDropdownFooter = () => wrapper.findComponent(DropdownFooter);
|
||||
const findDropdown = () => wrapper.findComponent(GlDropdownStub);
|
||||
|
||||
const findDropdownFooter = () => wrapper.find('[data-testid="dropdown-footer"]');
|
||||
const findDropdownHeader = () => wrapper.find('[data-testid="dropdown-header"]');
|
||||
const findCreateLabelButton = () => wrapper.find('[data-testid="create-label-button"]');
|
||||
const findGoBackButton = () => wrapper.find('[data-testid="go-back-button"]');
|
||||
|
||||
it('calls dropdown `show` method on `isVisible` prop change', async () => {
|
||||
createComponent();
|
||||
await wrapper.setProps({
|
||||
|
@ -136,6 +132,16 @@ describe('DropdownContent', () => {
|
|||
expect(findDropdownHeader().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('sets searchKey for labels view on input event from header', async () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.vm.searchKey).toEqual('');
|
||||
findDropdownHeader().vm.$emit('input', '123');
|
||||
await nextTick();
|
||||
|
||||
expect(findLabelsView().props('searchKey')).toEqual('123');
|
||||
});
|
||||
|
||||
describe('Create view', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ data: { showDropdownContentsCreateView: true } });
|
||||
|
@ -149,16 +155,8 @@ describe('DropdownContent', () => {
|
|||
expect(findDropdownFooter().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not render create label button', () => {
|
||||
expect(findCreateLabelButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('renders go back button', () => {
|
||||
expect(findGoBackButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('changes the view to Labels view on back button click', async () => {
|
||||
findGoBackButton().vm.$emit('click', new MouseEvent('click'));
|
||||
it('changes the view to Labels view on `toggleDropdownContentsCreateView` event', async () => {
|
||||
findDropdownHeader().vm.$emit('toggleDropdownContentsCreateView');
|
||||
await nextTick();
|
||||
|
||||
expect(findCreateView().exists()).toBe(false);
|
||||
|
@ -198,32 +196,5 @@ describe('DropdownContent', () => {
|
|||
|
||||
expect(findDropdownFooter().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not render go back button', () => {
|
||||
expect(findGoBackButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not render create label button if `allowLabelCreate` is false', () => {
|
||||
createComponent({ injected: { allowLabelCreate: false } });
|
||||
|
||||
expect(findCreateLabelButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('when `allowLabelCreate` is true', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders create label button', () => {
|
||||
expect(findCreateLabelButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('changes the view to Create on create label button click', async () => {
|
||||
findCreateLabelButton().trigger('click');
|
||||
|
||||
await nextTick();
|
||||
expect(findLabelsView().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import DropdownFooter from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue';
|
||||
|
||||
describe('DropdownFooter', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = ({ props = {}, injected = {} } = {}) => {
|
||||
wrapper = shallowMount(DropdownFooter, {
|
||||
propsData: {
|
||||
footerCreateLabelTitle: 'create',
|
||||
footerManageLabelTitle: 'manage',
|
||||
...props,
|
||||
},
|
||||
provide: {
|
||||
allowLabelCreate: true,
|
||||
labelsManagePath: 'foo/bar',
|
||||
...injected,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
const findCreateLabelButton = () => wrapper.find('[data-testid="create-label-button"]');
|
||||
|
||||
describe('Labels view', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('does not render create label button if `allowLabelCreate` is false', () => {
|
||||
createComponent({ injected: { allowLabelCreate: false } });
|
||||
|
||||
expect(findCreateLabelButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('when `allowLabelCreate` is true', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders create label button', () => {
|
||||
expect(findCreateLabelButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('emits `toggleDropdownContentsCreateView` event on create label button click', async () => {
|
||||
findCreateLabelButton().trigger('click');
|
||||
|
||||
await nextTick();
|
||||
expect(wrapper.emitted('toggleDropdownContentsCreateView')).toEqual([[]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,75 @@
|
|||
import { GlSearchBoxByType } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import DropdownHeader from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue';
|
||||
|
||||
describe('DropdownHeader', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = ({
|
||||
showDropdownContentsCreateView = false,
|
||||
labelsFetchInProgress = false,
|
||||
} = {}) => {
|
||||
wrapper = extendedWrapper(
|
||||
shallowMount(DropdownHeader, {
|
||||
propsData: {
|
||||
showDropdownContentsCreateView,
|
||||
labelsFetchInProgress,
|
||||
labelsCreateTitle: 'Create label',
|
||||
labelsListTitle: 'Select label',
|
||||
searchKey: '',
|
||||
},
|
||||
stubs: {
|
||||
GlSearchBoxByType,
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
|
||||
const findGoBackButton = () => wrapper.findByTestId('go-back-button');
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
describe('Create view', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ showDropdownContentsCreateView: true });
|
||||
});
|
||||
|
||||
it('renders go back button', () => {
|
||||
expect(findGoBackButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not render search input field', async () => {
|
||||
expect(findSearchInput().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Labels view', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('does not render go back button', () => {
|
||||
expect(findGoBackButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it.each`
|
||||
labelsFetchInProgress | disabled
|
||||
${true} | ${true}
|
||||
${false} | ${false}
|
||||
`(
|
||||
'when labelsFetchInProgress is $labelsFetchInProgress, renders search input with disabled prop to $disabled',
|
||||
({ labelsFetchInProgress, disabled }) => {
|
||||
createComponent({ labelsFetchInProgress });
|
||||
expect(findSearchInput().props('disabled')).toBe(disabled);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
|
@ -41,6 +41,7 @@ describe('LabelsSelectRoot', () => {
|
|||
propsData: {
|
||||
...config,
|
||||
issuableType: IssuableType.Issue,
|
||||
labelType: 'ProjectLabel',
|
||||
},
|
||||
stubs: {
|
||||
SidebarEditableItem,
|
||||
|
@ -121,11 +122,11 @@ describe('LabelsSelectRoot', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('emits `updateSelectedLabels` event on dropdown contents `setLabels` event', async () => {
|
||||
it('emits `updateSelectedLabels` event on dropdown contents `setLabels` event if iid is not set', async () => {
|
||||
const label = { id: 'gid://gitlab/ProjectLabel/1' };
|
||||
createComponent();
|
||||
createComponent({ config: { ...mockConfig, iid: undefined } });
|
||||
|
||||
findDropdownContents().vm.$emit('setLabels', [label]);
|
||||
expect(wrapper.emitted('updateSelectedLabels')).toEqual([[[label]]]);
|
||||
expect(wrapper.emitted('updateSelectedLabels')).toEqual([[{ labels: [label] }]]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -40,12 +40,12 @@ export const mockConfig = {
|
|||
labelsListTitle: 'Assign labels',
|
||||
labelsCreateTitle: 'Create label',
|
||||
variant: 'sidebar',
|
||||
selectedLabels: [mockRegularLabel, mockScopedLabel],
|
||||
labelsSelectInProgress: false,
|
||||
labelsFilterBasePath: '/gitlab-org/my-project/issues',
|
||||
labelsFilterParam: 'label_name',
|
||||
footerCreateLabelTitle: 'create',
|
||||
footerManageLabelTitle: 'manage',
|
||||
attrWorkspacePath: 'test',
|
||||
};
|
||||
|
||||
export const mockSuggestedColors = {
|
||||
|
|
|
@ -169,26 +169,9 @@ RSpec.describe IssuablesHelper do
|
|||
stub_const("Gitlab::IssuablesCountForState::THRESHOLD", 1000)
|
||||
end
|
||||
|
||||
context 'when feature flag cached_issues_state_count is disabled' do
|
||||
before do
|
||||
stub_feature_flags(cached_issues_state_count: false)
|
||||
end
|
||||
|
||||
it 'returns complete count' do
|
||||
expect(helper.issuables_state_counter_text(:issues, :opened, true))
|
||||
.to eq('<span>Open</span> <span class="badge badge-muted badge-pill gl-badge gl-tab-counter-badge sm gl-display-none gl-sm-display-inline-flex">1,100</span>')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when feature flag cached_issues_state_count is enabled' do
|
||||
before do
|
||||
stub_feature_flags(cached_issues_state_count: true)
|
||||
end
|
||||
|
||||
it 'returns truncated count' do
|
||||
expect(helper.issuables_state_counter_text(:issues, :opened, true))
|
||||
.to eq('<span>Open</span> <span class="badge badge-muted badge-pill gl-badge gl-tab-counter-badge sm gl-display-none gl-sm-display-inline-flex">1.1k</span>')
|
||||
end
|
||||
it 'returns truncated count' do
|
||||
expect(helper.issuables_state_counter_text(:issues, :opened, true))
|
||||
.to eq('<span>Open</span> <span class="badge badge-muted badge-pill gl-badge gl-tab-counter-badge sm gl-display-none gl-sm-display-inline-flex">1.1k</span>')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -196,51 +196,64 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when using service desk key' do
|
||||
let_it_be(:service_desk_key) { 'mykey' }
|
||||
|
||||
let(:email_raw) { service_desk_fixture('emails/service_desk_custom_address.eml') }
|
||||
context 'when using custom service desk address' do
|
||||
let(:receiver) { Gitlab::Email::ServiceDeskReceiver.new(email_raw) }
|
||||
|
||||
before do
|
||||
stub_service_desk_email_setting(enabled: true, address: 'support+%{key}@example.com')
|
||||
end
|
||||
|
||||
before_all do
|
||||
create(:service_desk_setting, project: project, project_key: service_desk_key)
|
||||
end
|
||||
context 'when using project key' do
|
||||
let_it_be(:service_desk_key) { 'mykey' }
|
||||
|
||||
it_behaves_like 'a new issue request'
|
||||
let(:email_raw) { service_desk_fixture('emails/service_desk_custom_address.eml') }
|
||||
|
||||
context 'when there is no project with the key' do
|
||||
let(:email_raw) { service_desk_fixture('emails/service_desk_custom_address.eml', key: 'some_key') }
|
||||
before_all do
|
||||
create(:service_desk_setting, project: project, project_key: service_desk_key)
|
||||
end
|
||||
|
||||
it 'bounces the email' do
|
||||
expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound)
|
||||
it_behaves_like 'a new issue request'
|
||||
|
||||
context 'when there is no project with the key' do
|
||||
let(:email_raw) { service_desk_fixture('emails/service_desk_custom_address.eml', key: 'some_key') }
|
||||
|
||||
it 'bounces the email' do
|
||||
expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the project slug does not match' do
|
||||
let(:email_raw) { service_desk_fixture('emails/service_desk_custom_address.eml', slug: 'some-slug') }
|
||||
|
||||
it 'bounces the email' do
|
||||
expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are multiple projects with same key' do
|
||||
let_it_be(:project_with_same_key) { create(:project, group: group, service_desk_enabled: true) }
|
||||
|
||||
let(:email_raw) { service_desk_fixture('emails/service_desk_custom_address.eml', slug: project_with_same_key.full_path_slug.to_s) }
|
||||
|
||||
before do
|
||||
create(:service_desk_setting, project: project_with_same_key, project_key: service_desk_key)
|
||||
end
|
||||
|
||||
it 'process email for project with matching slug' do
|
||||
expect { receiver.execute }.to change { Issue.count }.by(1)
|
||||
expect(Issue.last.project).to eq(project_with_same_key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the project slug does not match' do
|
||||
let(:email_raw) { service_desk_fixture('emails/service_desk_custom_address.eml', slug: 'some-slug') }
|
||||
|
||||
it 'bounces the email' do
|
||||
expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are multiple projects with same key' do
|
||||
let_it_be(:project_with_same_key) { create(:project, group: group, service_desk_enabled: true) }
|
||||
|
||||
let(:email_raw) { service_desk_fixture('emails/service_desk_custom_address.eml', slug: project_with_same_key.full_path_slug.to_s) }
|
||||
context 'when project key is not set' do
|
||||
let(:email_raw) { email_fixture('emails/service_desk_custom_address_no_key.eml') }
|
||||
|
||||
before do
|
||||
create(:service_desk_setting, project: project_with_same_key, project_key: service_desk_key)
|
||||
stub_service_desk_email_setting(enabled: true, address: 'support+%{key}@example.com')
|
||||
end
|
||||
|
||||
it 'process email for project with matching slug' do
|
||||
expect { receiver.execute }.to change { Issue.count }.by(1)
|
||||
expect(Issue.last.project).to eq(project_with_same_key)
|
||||
end
|
||||
it_behaves_like 'a new issue request'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -37,6 +37,23 @@ RSpec.describe ErrorTracking::ErrorEvent, type: :model do
|
|||
expect(event.stacktrace).to be_kind_of(Array)
|
||||
expect(event.stacktrace.first).to eq(expected_entry)
|
||||
end
|
||||
|
||||
context 'error context is missing' do
|
||||
let(:event) { create(:error_tracking_error_event, :browser) }
|
||||
|
||||
it 'generates a stacktrace without context' do
|
||||
expected_entry = {
|
||||
'lineNo' => 6395,
|
||||
'context' => [],
|
||||
'filename' => 'webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js',
|
||||
'function' => 'hydrate',
|
||||
'colNo' => 0
|
||||
}
|
||||
|
||||
expect(event.stacktrace).to be_kind_of(Array)
|
||||
expect(event.stacktrace.first).to eq(expected_entry)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#to_sentry_error_event' do
|
||||
|
|
|
@ -1715,13 +1715,19 @@ RSpec.describe Project, factory_default: :keep do
|
|||
allow(::Gitlab::ServiceDeskEmail).to receive(:config).and_return(config)
|
||||
end
|
||||
|
||||
it 'returns custom address when project_key is set' do
|
||||
create(:service_desk_setting, project: project, project_key: 'key1')
|
||||
context 'when project_key is set' do
|
||||
it 'returns custom address including the project_key' do
|
||||
create(:service_desk_setting, project: project, project_key: 'key1')
|
||||
|
||||
expect(subject).to eq("foo+#{project.full_path_slug}-key1@bar.com")
|
||||
expect(subject).to eq("foo+#{project.full_path_slug}-key1@bar.com")
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'with incoming email address'
|
||||
context 'when project_key is not set' do
|
||||
it 'returns custom address including the project full path' do
|
||||
expect(subject).to eq("foo+#{project.full_path_slug}-#{project.project_id}-issue-@bar.com")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -9,9 +9,9 @@ RSpec.describe 'groups/settings/_transfer.html.haml' do
|
|||
|
||||
render 'groups/settings/transfer', group: group
|
||||
|
||||
expect(rendered).to have_selector '[data-qa-selector="select_group_dropdown"]' # rubocop:disable QA/SelectorUsage
|
||||
expect(rendered).not_to have_selector '[data-qa-selector="select_group_dropdown"][disabled]' # rubocop:disable QA/SelectorUsage
|
||||
expect(rendered).not_to have_selector '[data-testid="group-to-transfer-has-linked-subscription-alert"]'
|
||||
expect(rendered).to have_button 'Select parent group'
|
||||
expect(rendered).not_to have_button 'Select parent group', disabled: true
|
||||
expect(rendered).not_to have_text "This group can't be transfered because it is linked to a subscription."
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -67,6 +67,8 @@ func (p *Injector) Inject(w http.ResponseWriter, r *http.Request, sendData strin
|
|||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Length", dependencyResponse.Header.Get("Content-Length"))
|
||||
|
||||
teeReader := io.TeeReader(dependencyResponse.Body, w)
|
||||
saveFileRequest, err := http.NewRequestWithContext(r.Context(), "POST", r.URL.String()+"/upload", teeReader)
|
||||
if err != nil {
|
||||
|
@ -75,8 +77,6 @@ func (p *Injector) Inject(w http.ResponseWriter, r *http.Request, sendData strin
|
|||
saveFileRequest.Header = helper.HeaderClone(r.Header)
|
||||
saveFileRequest.ContentLength = dependencyResponse.ContentLength
|
||||
|
||||
w.Header().Del("Content-Length")
|
||||
|
||||
nrw := &nullResponseWriter{header: make(http.Header)}
|
||||
p.uploadHandler.ServeHTTP(nrw, saveFileRequest)
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ func (f *fakeUploadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
type errWriter struct{ writes int }
|
||||
|
||||
func (w *errWriter) Header() http.Header { return nil }
|
||||
func (w *errWriter) Header() http.Header { return make(http.Header) }
|
||||
func (w *errWriter) WriteHeader(h int) {}
|
||||
|
||||
// First call of Write function succeeds while all the subsequent ones fail
|
||||
|
@ -112,8 +112,9 @@ func TestInject(t *testing.T) {
|
|||
|
||||
func TestSuccessfullRequest(t *testing.T) {
|
||||
content := []byte("result")
|
||||
contentLength := strconv.Itoa(len(content))
|
||||
originResourceServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(content)))
|
||||
w.Header().Set("Content-Length", contentLength)
|
||||
w.Write(content)
|
||||
}))
|
||||
|
||||
|
@ -135,6 +136,7 @@ func TestSuccessfullRequest(t *testing.T) {
|
|||
|
||||
require.Equal(t, 200, response.Code)
|
||||
require.Equal(t, string(content), response.Body.String())
|
||||
require.Equal(t, contentLength, response.Header().Get("Content-Length"))
|
||||
}
|
||||
|
||||
func TestIncorrectSendData(t *testing.T) {
|
||||
|
|
Loading…
Reference in New Issue