Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-10-12 00:14:00 +00:00
parent f53a938b96
commit 3f61501dc5
15 changed files with 222 additions and 28 deletions

View File

@ -40,9 +40,9 @@ export default {
todos: [],
currentTab: 0,
todosCount: {
pending: 0,
done: 0,
all: 0,
pending: '-',
done: '-',
all: '-',
},
queryFilterValues: {
groupId: [],

View File

@ -8,7 +8,7 @@ import {
GlFormSelect,
} from '@gitlab/ui';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { __, getPreferredLocales, s__ } from '~/locale';
import { __, getPreferredLocales, s__, sprintf } from '~/locale';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { fetchPolicies } from '~/lib/graphql';
import { addHierarchyChild, setNewWorkItemCache } from '~/work_items/graphql/cache_utils';
@ -31,6 +31,7 @@ import {
WIDGET_TYPE_LABELS,
WIDGET_TYPE_ROLLEDUP_DATES,
WIDGET_TYPE_CRM_CONTACTS,
WIDGET_TYPE_LINKED_ITEMS,
} from '../constants';
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
import namespaceWorkItemTypesQuery from '../graphql/namespace_work_item_types.query.graphql';
@ -103,11 +104,18 @@ export default {
required: false,
default: null,
},
relatedItem: {
type: Object,
required: false,
validator: (i) => i.id && i.type && i.reference,
default: null,
},
},
data() {
return {
isTitleValid: true,
isConfidential: false,
isRelatedToItem: true,
error: null,
workItemTypes: [],
selectedProjectFullPath: null,
@ -296,6 +304,19 @@ export default {
workItemIid() {
return this.workItem?.iid;
},
relatedItemText() {
return sprintf(s__('WorkItem|Relates to %{workItemType} %{workItemReference}'), {
workItemType: this.relatedItem.type,
workItemReference: this.relatedItem.reference,
});
},
shouldIncludeRelatedItem() {
return (
this.isWidgetSupported(WIDGET_TYPE_LINKED_ITEMS) &&
this.isRelatedToItem &&
this.relatedItem?.id
);
},
},
methods: {
isWidgetSupported(widgetType) {
@ -393,6 +414,12 @@ export default {
};
}
if (this.shouldIncludeRelatedItem) {
workItemCreateInput.linkedItemsWidget = {
workItemsIds: [this.relatedItem.id],
};
}
if (this.parentId) {
workItemCreateInput.hierarchyWidget = {
parentId: this.parentId,
@ -502,16 +529,23 @@ export default {
@error="updateError = $event"
@updateDraft="updateDraftData('description', $event)"
/>
<gl-form-group :label="__('Confidentiality')" label-for="work-item-confidential">
<gl-form-checkbox
id="work-item-confidential"
v-model="isConfidential"
data-testid="confidential-checkbox"
@change="updateDraftData('confidential', $event)"
>
{{ makeConfidentialText }}
</gl-form-checkbox>
</gl-form-group>
<gl-form-checkbox
id="work-item-confidential"
v-model="isConfidential"
data-testid="confidential-checkbox"
@change="updateDraftData('confidential', $event)"
>
{{ makeConfidentialText }}
</gl-form-checkbox>
<gl-form-checkbox
v-if="relatedItem"
id="work-item-relates-to"
v-model="isRelatedToItem"
class="gl-mt-3"
data-testid="relates-to-checkbox"
>
{{ relatedItemText }}
</gl-form-checkbox>
</section>
<aside
v-if="hasWidgets"

View File

@ -67,6 +67,12 @@ export default {
required: false,
default: false,
},
relatedItem: {
type: Object,
required: false,
validator: (i) => i.id && i.type && i.reference,
default: null,
},
},
data() {
return {
@ -201,6 +207,7 @@ export default {
:show-project-selector="showProjectSelector"
:title="title"
:work-item-type-name="workItemTypeName"
:related-item="relatedItem"
@cancel="hideModal"
@workItemCreated="handleCreated"
/>

View File

@ -38,12 +38,16 @@ import {
I18N_WORK_ITEM_ERROR_COPY_EMAIL,
TEST_ID_LOCK_ACTION,
TEST_ID_REPORT_ABUSE,
TEST_ID_NEW_RELATED_WORK_ITEM,
WORK_ITEM_TYPE_VALUE_EPIC,
WORK_ITEM_TYPE_ENUM_EPIC,
} from '../constants';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import updateWorkItemNotificationsMutation from '../graphql/update_work_item_notifications.mutation.graphql';
import convertWorkItemMutation from '../graphql/work_item_convert.mutation.graphql';
import namespaceWorkItemTypesQuery from '../graphql/namespace_work_item_types.query.graphql';
import WorkItemStateToggle from './work_item_state_toggle.vue';
import CreateWorkItemModal from './create_work_item_modal.vue';
export default {
i18n: {
@ -58,6 +62,7 @@ export default {
moreActions: __('More actions'),
reportAbuse: __('Report abuse'),
},
WORK_ITEM_TYPE_ENUM_EPIC,
components: {
GlDisclosureDropdown,
GlDisclosureDropdownItem,
@ -66,6 +71,7 @@ export default {
GlModal,
GlToggle,
WorkItemStateToggle,
CreateWorkItemModal,
},
directives: {
GlModal: GlModalDirective,
@ -82,6 +88,7 @@ export default {
lockDiscussionTestId: TEST_ID_LOCK_ACTION,
stateToggleTestId: TEST_ID_TOGGLE_ACTION,
reportAbuseActionTestId: TEST_ID_REPORT_ABUSE,
newRelatedItemTestId: TEST_ID_NEW_RELATED_WORK_ITEM,
props: {
fullPath: {
type: String,
@ -172,11 +179,17 @@ export default {
required: false,
default: 0,
},
canCreateRelatedItem: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
isLockDiscussionUpdating: false,
isDropdownVisible: false,
isCreateWorkItemModalVisible: false,
};
},
apollo: {
@ -240,6 +253,16 @@ export default {
isAuthor() {
return this.workItemAuthorId === window.gon.current_user_id;
},
relatedItemData() {
return {
id: this.workItemId,
type: this.workItemType,
reference: this.workItemReference,
};
},
isEpic() {
return this.workItemType === WORK_ITEM_TYPE_VALUE_EPIC;
},
},
methods: {
copyToClipboard(text, message) {
@ -423,6 +446,14 @@ export default {
@workItemStateUpdated="$emit('workItemStateUpdated')"
/>
<gl-disclosure-dropdown-item
v-if="canCreateRelatedItem && canUpdate && isEpic"
:data-testid="$options.newRelatedItemTestId"
@action="isCreateWorkItemModalVisible = true"
>
<template #list-item>{{ __('New related Epic') }}</template>
</gl-disclosure-dropdown-item>
<gl-disclosure-dropdown-item
v-if="canPromoteToObjective"
:data-testid="$options.promoteActionTestId"
@ -501,5 +532,13 @@ export default {
>
{{ areYouSureDeleteMessage }}
</gl-modal>
<create-work-item-modal
:visible="isCreateWorkItemModalVisible"
:related-item="relatedItemData"
:work-item-type-name="$options.WORK_ITEM_TYPE_ENUM_EPIC"
hide-button
@hideModal="isCreateWorkItemModalVisible = false"
/>
</div>
</template>

View File

@ -210,7 +210,7 @@ export default {
},
formGroupClass() {
return {
'gl-mb-5 common-note-form': true,
'common-note-form': true,
};
},
showEditedAt() {
@ -313,7 +313,6 @@ export default {
label-for="work-item-description"
>
<markdown-editor
class="gl-mb-5"
:value="descriptionText"
:render-markdown-path="markdownPreviewPath"
:markdown-docs-path="$options.markdownDocsPath"
@ -328,7 +327,12 @@ export default {
@keydown.ctrl.enter="updateWorkItem"
/>
<div class="gl-flex">
<gl-alert v-if="hasConflicts" :dismissible="false" variant="danger" class="gl-w-full">
<gl-alert
v-if="hasConflicts"
:dismissible="false"
variant="danger"
class="gl-mt-5 gl-w-full"
>
<p>
{{
s__(
@ -364,7 +368,7 @@ export default {
</gl-button>
</template>
</gl-alert>
<template v-else-if="showButtonsBelowField">
<div v-else-if="showButtonsBelowField" class="gl-mt-5">
<gl-button
category="primary"
variant="confirm"
@ -376,7 +380,7 @@ export default {
<gl-button category="secondary" class="gl-ml-3" data-testid="cancel" type="reset"
>{{ __('Cancel') }}
</gl-button>
</template>
</div>
</div>
</gl-form-group>
</gl-form>

View File

@ -754,6 +754,7 @@ export default {
:work-item-state="workItem.state"
:has-children="hasChildren"
:work-item-author-id="workItemAuthorId"
:can-create-related-item="workItemLinkedItems !== undefined"
@deleteWorkItem="$emit('deleteWorkItem', { workItemType, workItemId: workItem.id })"
@toggleWorkItemConfidentiality="toggleConfidentiality"
@error="updateError = $event"

View File

@ -270,6 +270,7 @@ export const TEST_ID_COPY_REFERENCE_ACTION = 'copy-reference-action';
export const TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION = 'copy-create-note-email-action';
export const TEST_ID_TOGGLE_ACTION = 'state-toggle-action';
export const TEST_ID_REPORT_ABUSE = 'report-abuse-action';
export const TEST_ID_NEW_RELATED_WORK_ITEM = 'new-related-work-item';
export const TODO_ADD_ICON = 'todo-add';
export const TODO_DONE_ICON = 'todo-done';

View File

@ -34,6 +34,7 @@ import {
WIDGET_TYPE_CRM_CONTACTS,
NEW_WORK_ITEM_IID,
WIDGET_TYPE_CURRENT_USER_TODOS,
WIDGET_TYPE_LINKED_ITEMS,
} from '../constants';
import workItemByIidQuery from './work_item_by_iid.query.graphql';
import getWorkItemTreeQuery from './work_item_tree.query.graphql';
@ -308,6 +309,7 @@ export const setNewWorkItemCache = async (
WIDGET_TYPE_START_AND_DUE_DATE,
WIDGET_TYPE_PROGRESS,
WIDGET_TYPE_HEALTH_STATUS,
WIDGET_TYPE_LINKED_ITEMS,
WIDGET_TYPE_COLOR,
WIDGET_TYPE_HIERARCHY,
WIDGET_TYPE_TIME_TRACKING,
@ -354,6 +356,16 @@ export const setNewWorkItemCache = async (
});
}
if (widgetName === WIDGET_TYPE_LINKED_ITEMS) {
widgets.push({
type: WIDGET_TYPE_LINKED_ITEMS,
linkedItems: {
nodes: [],
},
__typename: 'WorkItemWidgetLinkedItems',
});
}
if (widgetName === WIDGET_TYPE_CRM_CONTACTS) {
widgets.push({
type: 'CRM_CONTACTS',

View File

@ -35910,6 +35910,9 @@ msgstr ""
msgid "New related %{issueType}"
msgstr ""
msgid "New related Epic"
msgstr ""
msgid "New release"
msgstr ""
@ -62407,6 +62410,9 @@ msgstr ""
msgid "WorkItem|Related to"
msgstr ""
msgid "WorkItem|Relates to %{workItemType} %{workItemReference}"
msgstr ""
msgid "WorkItem|Remove"
msgstr ""

View File

@ -531,12 +531,15 @@ RSpec.describe Projects::PipelineSchedulesController, feature_category: :continu
expect(response).to have_gitlab_http_status(:found)
end
it 'prevents users from scheduling the same pipeline repeatedly', :freeze_time do
2.times { go }
context 'when rate limited' do
it 'prevents users from scheduling the same pipeline repeatedly' do
allow(Gitlab::ApplicationRateLimiter).to receive(:throttled_request?).and_return(true)
expect(flash.to_a.size).to eq(2)
expect(flash[:alert]).to eq _('You cannot play this scheduled pipeline at the moment. Please wait a minute.')
expect(response).to have_gitlab_http_status(:found)
go
expect(flash[:alert]).to eq _('You cannot play this scheduled pipeline at the moment. Please wait a minute.')
expect(response).to have_gitlab_http_status(:found)
end
end
end

View File

@ -47,7 +47,6 @@ describe('Create work item component', () => {
namespaceWorkItemTypesQueryResponse.data.workspace.workItemTypes.nodes.find(
({ name }) => name === 'Epic',
).id;
const createWorkItemSuccessHandler = jest.fn().mockResolvedValue(createWorkItemMutationResponse);
const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
@ -65,6 +64,7 @@ describe('Create work item component', () => {
const findProjectsSelector = () => wrapper.findComponent(WorkItemProjectsListbox);
const findSelect = () => wrapper.findComponent(GlFormSelect);
const findConfidentialCheckbox = () => wrapper.find('[data-testid="confidential-checkbox"]');
const findRelatesToCheckbox = () => wrapper.find('[data-testid="relates-to-checkbox"]');
const findCreateWorkItemView = () => wrapper.find('[data-testid="create-work-item-view"]');
const findCreateButton = () => wrapper.find('[data-testid="create-button"]');
@ -355,7 +355,7 @@ describe('Create work item component', () => {
describe('Create work item widgets for epic work item type', () => {
describe('default', () => {
beforeEach(async () => {
await createComponent({ singleWorkItemType: true });
createComponent({ singleWorkItemType: true });
await waitForPromises();
});
@ -382,7 +382,7 @@ describe('Create work item component', () => {
it('uses the description prop as the initial description value when defined', async () => {
const description = 'i am a description';
await createComponent({
createComponent({
singleWorkItemType: true,
props: { description },
});
@ -393,10 +393,60 @@ describe('Create work item component', () => {
it('uses the title prop as the initial title value when defined', async () => {
const title = 'i am a title';
await createComponent({ singleWorkItemType: true, props: { title } });
createComponent({ singleWorkItemType: true, props: { title } });
await waitForPromises();
expect(findTitleInput().props('title')).toBe(title);
});
});
describe('With related item', () => {
const id = 'gid://gitlab/WorkItem/1';
const type = 'Epic';
const reference = 'gitlab-org#1';
beforeEach(async () => {
createComponent({
singleWorkItemType: true,
props: {
relatedItem: {
id,
type,
reference,
},
},
});
await waitForPromises();
});
it('renders a checkbox', () => {
expect(findRelatesToCheckbox().exists()).toBe(true);
});
it('renders the correct text for the checkbox', () => {
expect(findRelatesToCheckbox().text()).toContain(`Relates to ${type} ${reference}`);
});
it('includes the related item in the create work item request', async () => {
await updateWorkItemTitle();
await submitCreateForm();
expect(createWorkItemSuccessHandler).toHaveBeenCalledWith({
input: expect.objectContaining({
linkedItemsWidget: {
workItemsIds: [id],
},
}),
});
});
it('does not include the related item in the create work item request if the checkbox is unchecked', async () => {
await updateWorkItemTitle();
findRelatesToCheckbox().vm.$emit('input', false);
await submitCreateForm();
expect(createWorkItemSuccessHandler).not.toHaveBeenCalledWith({
input: expect.objectContaining({
linkedItemsWidget: {
workItemsIds: [id],
},
}),
});
});
});
});

View File

@ -54,6 +54,7 @@ describe('WorkItemRelationshipPopover', () => {
target,
workItemFullPath: 'gitlab-org/gitlab-test',
workItemWebUrl,
workItemType: 'Task',
loading,
},
});

View File

@ -15,6 +15,7 @@ import toast from '~/vue_shared/plugins/global_toast';
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
import WorkItemAbuseModal from '~/work_items/components/work_item_abuse_modal.vue';
import WorkItemStateToggle from '~/work_items/components/work_item_state_toggle.vue';
import CreateWorkItemModal from '~/work_items/components/create_work_item_modal.vue';
import {
STATE_OPEN,
TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION,
@ -26,6 +27,7 @@ import {
TEST_ID_PROMOTE_ACTION,
TEST_ID_TOGGLE_ACTION,
TEST_ID_REPORT_ABUSE,
TEST_ID_NEW_RELATED_WORK_ITEM,
} from '~/work_items/constants';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import updateWorkItemNotificationsMutation from '~/work_items/graphql/update_work_item_notifications.mutation.graphql';
@ -61,7 +63,9 @@ describe('WorkItemActions component', () => {
const findCopyCreateNoteEmailButton = () =>
wrapper.findByTestId(TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION);
const findReportAbuseButton = () => wrapper.findByTestId(TEST_ID_REPORT_ABUSE);
const findNewRelatedItemButton = () => wrapper.findByTestId(TEST_ID_NEW_RELATED_WORK_ITEM);
const findReportAbuseModal = () => wrapper.findComponent(WorkItemAbuseModal);
const findCreateWorkItemModal = () => wrapper.findComponent(CreateWorkItemModal);
const findMoreDropdown = () => wrapper.findByTestId('work-item-actions-dropdown');
const findMoreDropdownTooltip = () => getBinding(findMoreDropdown().element, 'gl-tooltip');
const findDropdownItems = () => wrapper.findAll('[data-testid="work-item-actions-dropdown"] > *');
@ -119,6 +123,7 @@ describe('WorkItemActions component', () => {
workItemCreateNoteEmail = mockWorkItemCreateNoteEmail,
hideSubscribe = undefined,
hasChildren = false,
canCreateRelatedItem = true,
} = {}) => {
wrapper = shallowMountExtended(WorkItemActions, {
isLoggedIn: isLoggedIn(),
@ -147,10 +152,14 @@ describe('WorkItemActions component', () => {
workItemCreateNoteEmail,
hideSubscribe,
hasChildren,
canCreateRelatedItem,
},
mocks: {
$toast,
},
provide: {
fullPath: 'gitlab-org/gitlab-test',
},
stubs: {
GlModal: stubComponent(GlModal, {
methods: {
@ -222,6 +231,19 @@ describe('WorkItemActions component', () => {
]);
});
it('includes a new related item option when the work item is the correct type', () => {
createComponent({ workItemType: 'Epic' });
expect(findDropdownItemsActual()).toEqual(
expect.arrayContaining([
{
testId: TEST_ID_NEW_RELATED_WORK_ITEM,
text: 'New related Epic',
},
]),
);
});
describe('lock discussion action', () => {
it.each`
isDiscussionLocked | buttonText
@ -518,4 +540,15 @@ describe('WorkItemActions component', () => {
expect(wrapper.emitted('toggleReportAbuseModal')).toEqual([[true]]);
});
});
describe('new related item', () => {
it('opens the create work item modal', async () => {
createComponent({ workItemType: 'Epic' });
findNewRelatedItemButton().vm.$emit('action');
await nextTick();
expect(findCreateWorkItemModal().props('visible')).toBe(true);
});
});
});

View File

@ -250,6 +250,7 @@ and even more`,
hideButton: true,
isGroup: false,
parentId: 'gid://gitlab/WorkItem/818',
relatedItem: null,
showProjectSelector: false,
title:
'item 2 with a really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really rea',

View File

@ -2080,6 +2080,7 @@ export const workItemObjectiveWithChild = {
iconName: 'issue-type-objective',
__typename: 'WorkItemType',
},
webUrl: 'http://gdk.test/gitlab-org/gitlab/-/issues/1',
namespace: {
__typename: 'Project',
id: '1',
@ -2135,6 +2136,7 @@ export const workItemObjectiveWithoutChild = {
iconName: 'issue-type-objective',
__typename: 'WorkItemType',
},
webUrl: 'http://gdk.test/gitlab-org/gitlab/-/issues/1',
namespace: {
__typename: 'Project',
id: '1',