diff --git a/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue index 88639e6f0c9233f8993b9922f11d014e499d6b9f..a5459f98f0ab8e2138f8d743f9f5d2841a482640 100644 --- a/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue +++ b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue @@ -10,8 +10,10 @@ import { GlTooltip, GlTooltipDirective, } from '@gitlab/ui'; +import { escapeRegExp } from 'lodash'; import { __, s__, sprintf } from '~/locale'; import { isScopedLabel } from '~/lib/utils/common_utils'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import WorkItemLinkChildMetadata from 'ee_else_ce/work_items/components/shared/work_item_link_child_metadata.vue'; import RichTimestampTooltip from '../rich_timestamp_tooltip.vue'; import WorkItemTypeIcon from '../work_item_type_icon.vue'; @@ -22,6 +24,7 @@ import { WIDGET_TYPE_ASSIGNEES, WIDGET_TYPE_LABELS, LINKED_CATEGORIES_MAP, + INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION, } from '../../constants'; import WorkItemRelationshipIcons from './work_item_relationship_icons.vue'; @@ -50,6 +53,14 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, + mixins: [glFeatureFlagMixin()], + inject: { + preventRouterNav: { + from: INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION, + default: false, + }, + isGroup: {}, + }, props: { childItem: { type: Object, @@ -137,11 +148,40 @@ export default { return item.linkType !== LINKED_CATEGORIES_MAP.RELATES_TO; }); }, + issueAsWorkItem() { + return ( + !this.isGroup && + this.glFeatures.workItemsViewPreference && + gon.current_user_use_work_items_view + ); + }, }, methods: { showScopedLabel(label) { return isScopedLabel(label) && this.allowsScopedLabels; }, + handleTitleClick(e) { + const workItem = this.childItem; + if (e.metaKey || e.ctrlKey) { + return; + } + const escapedFullPath = escapeRegExp(this.workItemFullPath); + // eslint-disable-next-line no-useless-escape + const regex = new RegExp(`groups\/${escapedFullPath}\/-\/(work_items|epics)\/\\d+`); + const isWorkItemPath = regex.test(workItem.webUrl); + + if (!(isWorkItemPath || this.issueAsWorkItem) || this.preventRouterNav) { + this.$emit('click', e); + } else { + e.preventDefault(); + this.$router.push({ + name: 'workItem', + params: { + iid: workItem.iid, + }, + }); + } + }, }, }; </script> @@ -171,10 +211,10 @@ export default { /> </span> <gl-link - :href="childItem.webUrl" + :href="childItemWebUrl" :class="{ '!gl-text-secondary': !isChildItemOpen }" class="gl-hyphens-auto gl-break-words gl-font-semibold" - @click.exact="$emit('click', $event)" + @click.exact="handleTitleClick" @mouseover="$emit('mouseover')" @mouseout="$emit('mouseout')" > diff --git a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue index 4cf34a5c6af18888b5d836be3528345efa9209c9..1ad6c053a518f138c5114aa97e977e0dccbcb5f5 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue @@ -3,6 +3,7 @@ import { GlAlert, GlModal } from '@gitlab/ui'; import { s__ } from '~/locale'; import { removeHierarchyChild } from '../graphql/cache_utils'; import deleteWorkItemMutation from '../graphql/delete_work_item.mutation.graphql'; +import { INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION } from '../constants'; export default { WORK_ITEM_DETAIL_MODAL_ID: 'work-item-detail-modal', @@ -15,6 +16,9 @@ export default { GlModal, WorkItemDetail: () => import('./work_item_detail.vue'), }, + provide: { + [INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION]: true, + }, props: { parentId: { type: String, diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue index fe3e798cb1a6a6a1cdebb3ce75b7151674012a75..3a08895259da457cda1855aa1529a00a177cb0b0 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue @@ -16,6 +16,7 @@ import { WORKITEM_TREE_SHOWLABELS_LOCALSTORAGEKEY, WORK_ITEM_TYPE_VALUE_EPIC, WIDGET_TYPE_HIERARCHY, + INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION, } from '../../constants'; import { findHierarchyWidgets, @@ -48,6 +49,9 @@ export default { WorkItemRolledUpData, }, inject: ['hasSubepicsFeature'], + provide: { + [INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION]: true, + }, props: { fullPath: { type: String, diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js index 698b383b47b216cdb0466b4aceccf7a01e434bae..d337fb10a5effb5cdd354688d9d232908a383e72 100644 --- a/app/assets/javascripts/work_items/constants.js +++ b/app/assets/javascripts/work_items/constants.js @@ -372,3 +372,5 @@ export const WORK_ITEM_BASE_ROUTE_MAP = { export const WORKITEM_LINKS_SHOWLABELS_LOCALSTORAGEKEY = 'workItemLinks.showLabels'; export const WORKITEM_TREE_SHOWLABELS_LOCALSTORAGEKEY = 'workItemTree.showLabels'; export const WORKITEM_RELATIONSHIPS_SHOWLABELS_LOCALSTORAGEKEY = 'workItemRelationships.showLabels'; + +export const INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION = 'injection:prevent-router-navigation'; diff --git a/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js b/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js index d262db3c3384f88ffffee19235904816ebcdea53..d63d446b03873c823e92d41ad765605331d17174 100644 --- a/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js +++ b/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js @@ -14,6 +14,7 @@ import WorkItemRelationshipIcons from '~/work_items/components/shared/work_item_ import { workItemTask, + workItemEpic, workItemObjectiveWithChild, confidentialWorkItemTask, closedWorkItemTask, @@ -32,6 +33,8 @@ describe('WorkItemLinkChildContents', () => { const mockAssignees = ASSIGNEES.assignees.nodes; const mockLabels = LABELS.labels.nodes; + const mockRouterPush = jest.fn(); + const findStatusBadgeComponent = () => wrapper.findByTestId('item-status-icon').findComponent(WorkItemStateBadge); const findConfidentialIconComponent = () => wrapper.findByTestId('confidential-icon'); @@ -48,13 +51,23 @@ describe('WorkItemLinkChildContents', () => { canUpdate = true, childItem = workItemTask, showLabels = true, + workItemFullPath = 'test-project-path', + isGroup = false, } = {}) => { wrapper = shallowMountExtended(WorkItemLinkChildContents, { propsData: { canUpdate, childItem, showLabels, - workItemFullPath: 'test-project-path', + workItemFullPath, + }, + provide: { + isGroup, + }, + mocks: { + $router: { + push: mockRouterPush, + }, }, }); }; @@ -129,6 +142,29 @@ describe('WorkItemLinkChildContents', () => { expect(wrapper.emitted('click')).toEqual([[eventObj]]); }); + + describe('when the linked item can be navigated to via Vue Router', () => { + const preventDefault = jest.fn(); + beforeEach(() => { + createComponent({ + childItem: workItemEpic, + isGroup: true, + workItemFullPath: 'gitlab-org/gitlab-test', + }); + + findTitleEl().vm.$emit('click', { preventDefault }); + }); + + it('pushes a new router state', () => { + expect(mockRouterPush).toHaveBeenCalled(); + }); + it('prevents the default event behaviour', () => { + expect(preventDefault).toHaveBeenCalled(); + }); + it('does not emit a click event', () => { + expect(wrapper.emitted('click')).not.toBeDefined(); + }); + }); }); describe('item metadata', () => { diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index 3b639ddb443d95bcd506f374af946e52a502889d..2479cbfcd60d59d43963ae4ee99ec677f6185f75 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -1898,14 +1898,15 @@ export const workItemEpic = { namespace: { __typename: 'Project', id: '1', - fullPath: 'test-project-path', + fullPath: 'gitlab-org/gitlab-test', name: 'Project name', }, createdAt: '2022-08-03T12:41:54Z', closedAt: null, - webUrl: '/gitlab-org/gitlab-test/-/work_items/4', + webUrl: 'http://127.0.0.1:3000/groups/gitlab-org/gitlab-test/-/work_items/4', widgets: [ workItemObjectiveMetadataWidgets.ASSIGNEES, + workItemObjectiveMetadataWidgets.LINKED_ITEMS, { type: 'HIERARCHY', hasChildren: false,