diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue index 93f552bfa4c2a86d9d04a3cc10008475914094e7..485d85b887262ab5b0bb78410a11488949c5e76c 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -19,6 +19,7 @@ import { WIDGET_TYPE_AWARD_EMOJI, WIDGET_TYPE_HIERARCHY, WORK_ITEM_TYPE_VALUE_OBJECTIVE, + WORK_ITEM_TYPE_VALUE_EPIC, WIDGET_TYPE_NOTES, WIDGET_TYPE_LINKED_ITEMS, } from '../constants'; @@ -232,6 +233,11 @@ export default { workItemLinkedItems() { return this.isWidgetPresent(WIDGET_TYPE_LINKED_ITEMS); }, + showWorkItemTree() { + return [WORK_ITEM_TYPE_VALUE_OBJECTIVE, WORK_ITEM_TYPE_VALUE_EPIC].includes( + this.workItemType, + ); + }, showWorkItemLinkedItems() { return this.hasLinkedWorkItems && this.workItemLinkedItems; }, @@ -586,7 +592,7 @@ export default { @emoji-updated="$emit('work-item-emoji-updated', $event)" /> <work-item-tree - v-if="workItemType === $options.WORK_ITEM_TYPE_VALUE_OBJECTIVE" + v-if="showWorkItemTree" :full-path="fullPath" :work-item-type="workItemType" :parent-work-item-type="workItem.workItemType.name" diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue index f24b56cac361c2e1544ecebf57e8704044536da4..cc46932539d83fbfa58091b1b2c1bbf5f7f1c69a 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue @@ -11,6 +11,7 @@ import { import { __, s__, sprintf } from '~/locale'; import WorkItemTokenInput from '../shared/work_item_token_input.vue'; import { addHierarchyChild } from '../../graphql/cache_utils'; +import groupWorkItemTypesQuery from '../../graphql/group_work_item_types.query.graphql'; import projectWorkItemTypesQuery from '../../graphql/project_work_item_types.query.graphql'; import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql'; import createWorkItemMutation from '../../graphql/create_work_item.mutation.graphql'; @@ -90,7 +91,9 @@ export default { }, apollo: { workItemTypes: { - query: projectWorkItemTypesQuery, + query() { + return this.isGroup ? groupWorkItemTypesQuery : projectWorkItemTypesQuery; + }, variables() { return { fullPath: this.fullPath, diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js index 51f63b147ac2a7fa23dff20d1129cc54c04b8988..62fdc8a21c29dc042525bab95326988c8c1f5bed 100644 --- a/app/assets/javascripts/work_items/constants.js +++ b/app/assets/javascripts/work_items/constants.js @@ -201,6 +201,8 @@ export const WORK_ITEMS_TYPE_MAP = { export const WORK_ITEM_TYPE_VALUE_MAP = { [WORK_ITEM_TYPE_VALUE_OBJECTIVE]: WORK_ITEM_TYPE_ENUM_OBJECTIVE, [WORK_ITEM_TYPE_VALUE_KEY_RESULT]: WORK_ITEM_TYPE_ENUM_KEY_RESULT, + [WORK_ITEM_TYPE_VALUE_ISSUE]: WORK_ITEM_TYPE_ENUM_ISSUE, + [WORK_ITEM_TYPE_VALUE_EPIC]: WORK_ITEM_TYPE_ENUM_EPIC, }; export const WORK_ITEMS_TREE_TEXT_MAP = { @@ -214,9 +216,14 @@ export const WORK_ITEMS_TREE_TEXT_MAP = { 'WorkItem|No tasks are currently assigned. Use tasks to break down this issue into smaller parts.', ), }, + [WORK_ITEM_TYPE_VALUE_EPIC]: { + title: s__('WorkItem|Child items'), + empty: s__('WorkItem|No epics or issues are currently assigned.'), + }, }; export const WORK_ITEM_NAME_TO_ICON_MAP = { + Epic: 'epic', Issue: 'issue-type-issue', Task: 'issue-type-task', Objective: 'issue-type-objective', diff --git a/ee/spec/frontend/work_items/mock_data.js b/ee/spec/frontend/work_items/mock_data.js index b7ef398ddfd283ca33969fd03c8d73378bfeb2d0..a7b2a995ea8068a8e86706179ae9f2771cff5967 100644 --- a/ee/spec/frontend/work_items/mock_data.js +++ b/ee/spec/frontend/work_items/mock_data.js @@ -27,6 +27,7 @@ export const createWorkItemMutationResponse = { confidential: false, createdAt: '2022-08-03T12:41:54Z', closedAt: null, + webUrl: 'http://127.0.0.1:3000/groups/gitlab-org/-/work_items/1', project: { __typename: 'Project', id: '1', @@ -54,7 +55,7 @@ export const createWorkItemMutationErrorResponse = { data: { workItemCreate: { __typename: 'WorkItemCreatePayload', - workItem: {}, + workItem: null, errors: ['Title is too long (maximum is 255 characters)'], }, }, diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 915acda10de728e8554861a0c5fad7576c671ece..a5c7bf71cfb1a9ca0db8dfdd102747f974dd65bf 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -55303,6 +55303,9 @@ msgstr "" msgid "WorkItem|No child items are currently assigned. Use child items to break down this issue into smaller parts." msgstr "" +msgid "WorkItem|No epics or issues are currently assigned." +msgstr "" + msgid "WorkItem|No iteration" msgstr "" diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js index e43c4d3c74d93a30b8d598933fba0c7b33c405d1..45c8c66cebfd61e52b1564418ea5c594c7616175 100644 --- a/spec/frontend/work_items/components/work_item_detail_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_spec.js @@ -33,6 +33,7 @@ import { mockParent, workItemByIidResponseFactory, objectiveType, + epicType, mockWorkItemCommentNote, mockBlockingLinkedItem, } from '../mock_data'; @@ -429,9 +430,18 @@ describe('WorkItemDetail component', () => { workItemType: objectiveType, confidential: true, }); - const handler = jest.fn().mockResolvedValue(objectiveWorkItem); + const objectiveHandler = jest.fn().mockResolvedValue(objectiveWorkItem); - it('renders children tree when work item is an Objective', async () => { + const epicWorkItem = workItemByIidResponseFactory({ + workItemType: epicType, + }); + const epicHandler = jest.fn().mockResolvedValue(epicWorkItem); + + it.each` + type | handler + ${'Objective'} | ${objectiveHandler} + ${'Epic'} | ${epicHandler} + `('renders children tree when work item type is $type', async ({ handler }) => { createComponent({ handler }); await waitForPromises(); @@ -439,14 +449,14 @@ describe('WorkItemDetail component', () => { }); it('renders a modal', async () => { - createComponent({ handler }); + createComponent({ handler: objectiveHandler }); await waitForPromises(); expect(findModal().exists()).toBe(true); }); it('opens the modal with the child when `show-modal` is emitted', async () => { - createComponent({ handler, workItemsMvc2Enabled: true }); + createComponent({ handler: objectiveHandler, workItemsMvc2Enabled: true }); await waitForPromises(); const event = { @@ -469,7 +479,7 @@ describe('WorkItemDetail component', () => { beforeEach(async () => { createComponent({ isModal: true, - handler, + handler: objectiveHandler, workItemsMvc2Enabled: true, }); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js index 0a9da17d2841acc22e66821ccea92819e7ed5326..ba09c7e9ce21ef99358d1fbfa35be99c4dc22775 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js @@ -15,12 +15,14 @@ import { I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_TOOLTIP, } from '~/work_items/constants'; import projectWorkItemsQuery from '~/work_items/graphql/project_work_items.query.graphql'; +import groupWorkItemTypesQuery from '~/work_items/graphql/group_work_item_types.query.graphql'; import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql'; import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import { availableWorkItemsResponse, projectWorkItemTypesQueryResponse, + groupWorkItemTypesQueryResponse, createWorkItemMutationResponse, updateWorkItemMutationResponse, mockIterationWidgetResponse, @@ -34,22 +36,27 @@ describe('WorkItemLinksForm', () => { const updateMutationResolver = jest.fn().mockResolvedValue(updateWorkItemMutationResponse); const createMutationResolver = jest.fn().mockResolvedValue(createWorkItemMutationResponse); const availableWorkItemsResolver = jest.fn().mockResolvedValue(availableWorkItemsResponse); + const projectWorkItemTypesResolver = jest + .fn() + .mockResolvedValue(projectWorkItemTypesQueryResponse); + const groupWorkItemTypesResolver = jest.fn().mockResolvedValue(groupWorkItemTypesQueryResponse); const mockParentIteration = mockIterationWidgetResponse; const createComponent = async ({ - typesResponse = projectWorkItemTypesQueryResponse, parentConfidential = false, hasIterationsFeature = false, parentIteration = null, formType = FORM_TYPES.create, parentWorkItemType = WORK_ITEM_TYPE_VALUE_ISSUE, childrenType = WORK_ITEM_TYPE_ENUM_TASK, + isGroup = false, } = {}) => { wrapper = shallowMountExtended(WorkItemLinksForm, { apolloProvider: createMockApollo([ [projectWorkItemsQuery, availableWorkItemsResolver], - [projectWorkItemTypesQuery, jest.fn().mockResolvedValue(typesResponse)], + [projectWorkItemTypesQuery, projectWorkItemTypesResolver], + [groupWorkItemTypesQuery, groupWorkItemTypesResolver], [updateWorkItemMutation, updateMutationResolver], [createWorkItemMutation, createMutationResolver], ]), @@ -64,7 +71,7 @@ describe('WorkItemLinksForm', () => { }, provide: { hasIterationsFeature, - isGroup: false, + isGroup, }, }); @@ -79,6 +86,19 @@ describe('WorkItemLinksForm', () => { const findAddChildButton = () => wrapper.findByTestId('add-child-button'); const findValidationElement = () => wrapper.findByTestId('work-items-invalid'); + it.each` + workspace | isGroup | queryResolver + ${'project'} | ${false} | ${projectWorkItemTypesResolver} + ${'group'} | ${true} | ${groupWorkItemTypesResolver} + `( + 'fetches $workspace work item types when isGroup is $isGroup', + async ({ isGroup, queryResolver }) => { + await createComponent({ isGroup }); + + expect(queryResolver).toHaveBeenCalled(); + }, + ); + describe('creating a new work item', () => { beforeEach(async () => { await createComponent(); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index ce1919fb6d0ed99992aca0d059369034dfe30ff2..25b476f78f795561d7298c54314d4c4279c8a482 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -475,6 +475,13 @@ export const issueType = { iconName: 'issue-type-issue', }; +export const epicType = { + __typename: 'WorkItemType', + id: 'gid://gitlab/WorkItems::Type/2411', + name: 'Epic', + iconName: 'issue-type-epic', +}; + export const mockEmptyLinkedItems = { type: WIDGET_TYPE_LINKED_ITEMS, blocked: false, @@ -910,6 +917,18 @@ export const projectWorkItemTypesQueryResponse = { }, }; +export const groupWorkItemTypesQueryResponse = { + data: { + workspace: { + __typename: 'Group', + id: 'gid://gitlab/Group/2', + workItemTypes: { + nodes: [{ id: 'gid://gitlab/WorkItems::Type/6', name: 'Epic' }], + }, + }, + }, +}; + export const createWorkItemMutationResponse = { data: { workItemCreate: {