diff --git a/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue b/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue index ce3d4749f17e2642ae8c7464623a6395b8ad4ceb..c122db6c9024ee94f6d6e1b09089df2ecbe59453 100644 --- a/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue +++ b/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue @@ -1,21 +1,28 @@ diff --git a/app/assets/javascripts/work_items/components/work_item_parent.vue b/app/assets/javascripts/work_items/components/work_item_parent.vue index d0e83ba8c21d4a6394696dd0afc13efacb85c8f0..ce30f7985cf5047f0c72d51040a61a1eeb714dd8 100644 --- a/app/assets/javascripts/work_items/components/work_item_parent.vue +++ b/app/assets/javascripts/work_items/components/work_item_parent.vue @@ -107,6 +107,8 @@ export default { searchTerm: this.search, types: this.parentType, in: this.search ? 'TITLE' : undefined, + iid: null, + isNumber: false, }; }, skip() { diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js index e2dbfeb55a5d1fceee776fbb4f875fbfdb1a9fea..c3d3d62351565c46ec7283eb8d80816df7390f63 100644 --- a/app/assets/javascripts/work_items/constants.js +++ b/app/assets/javascripts/work_items/constants.js @@ -94,8 +94,9 @@ export const I18N_WORK_ITEM_FETCH_AWARD_EMOJI_ERROR = s__( export const I18N_WORK_ITEM_CREATE_BUTTON_LABEL = s__('WorkItem|Create %{workItemType}'); export const I18N_WORK_ITEM_ADD_BUTTON_LABEL = s__('WorkItem|Add %{workItemType}'); export const I18N_WORK_ITEM_ADD_MULTIPLE_BUTTON_LABEL = s__('WorkItem|Add %{workItemType}s'); -export const I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER = s__( - 'WorkItem|Search existing %{workItemType}s', +export const I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER = s__('WorkItem|Search existing items'); +export const I18N_WORK_ITEM_SEARCH_ERROR = s__( + 'WorkItem|Something went wrong while fetching the %{workItemType}. Please try again.', ); export const I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_LABEL = s__( 'WorkItem|This %{workItemType} is confidential and should only be visible to team members with at least Reporter access', diff --git a/app/assets/javascripts/work_items/graphql/group_work_items.query.graphql b/app/assets/javascripts/work_items/graphql/group_work_items.query.graphql index 320bb4a2494502473da9f0d95bc6fa6a20c20f60..5332e21a0cbd7d0c94294393ec4f6399127f3684 100644 --- a/app/assets/javascripts/work_items/graphql/group_work_items.query.graphql +++ b/app/assets/javascripts/work_items/graphql/group_work_items.query.graphql @@ -11,8 +11,6 @@ query groupWorkItems( id iid title - state - confidential } } } diff --git a/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql b/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql index 2be436aa8c20d6cb4f125938f56958fb442f5065..3aeaaa1116a541f00cbe9d6972b168c0a2d04968 100644 --- a/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql +++ b/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql @@ -3,6 +3,8 @@ query projectWorkItems( $fullPath: ID! $types: [IssueType!] $in: [IssuableSearchableField!] + $iid: String = null + $isNumber: Boolean! ) { workspace: project(fullPath: $fullPath) { id @@ -11,8 +13,13 @@ query projectWorkItems( id iid title - state - confidential + } + } + workItemsByIid: workItems(iid: $iid, types: $types) @include(if: $isNumber) { + nodes { + id + iid + title } } } diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 9e8e539ff109e53da3c59f96e298fcd25dd66afb..b4fcb747bd211dbf4e88481c07a8b562b904c5bc 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -54519,7 +54519,7 @@ msgstr "" msgid "WorkItem|Save and overwrite" msgstr "" -msgid "WorkItem|Search existing %{workItemType}s" +msgid "WorkItem|Search existing items" msgstr "" msgid "WorkItem|Select type" @@ -54579,6 +54579,9 @@ msgstr "" msgid "WorkItem|Something went wrong while fetching milestones. Please try again." msgstr "" +msgid "WorkItem|Something went wrong while fetching the %{workItemType}. Please try again." +msgstr "" + msgid "WorkItem|Something went wrong while fetching work item award emojis. Please try again." msgstr "" diff --git a/spec/frontend/work_items/components/shared/work_item_token_input_spec.js b/spec/frontend/work_items/components/shared/work_item_token_input_spec.js index c70dbbd909d5170755772c74f60305a59cba70f9..5726aaaa2d0419430fc2b966b62f797e1fa4015c 100644 --- a/spec/frontend/work_items/components/shared/work_item_token_input_spec.js +++ b/spec/frontend/work_items/components/shared/work_item_token_input_spec.js @@ -1,5 +1,5 @@ -import Vue from 'vue'; -import { GlTokenSelector } from '@gitlab/ui'; +import Vue, { nextTick } from 'vue'; +import { GlTokenSelector, GlAlert } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; @@ -8,7 +8,12 @@ import WorkItemTokenInput from '~/work_items/components/shared/work_item_token_i import { WORK_ITEM_TYPE_ENUM_TASK } from '~/work_items/constants'; import groupWorkItemsQuery from '~/work_items/graphql/group_work_items.query.graphql'; import projectWorkItemsQuery from '~/work_items/graphql/project_work_items.query.graphql'; -import { availableWorkItemsResponse, searchedWorkItemsResponse } from '../../mock_data'; +import { + availableWorkItemsResponse, + searchWorkItemsTextResponse, + searchWorkItemsIidResponse, + searchWorkItemsTextIidResponse, +} from '../../mock_data'; Vue.use(VueApollo); @@ -16,15 +21,17 @@ describe('WorkItemTokenInput', () => { let wrapper; const availableWorkItemsResolver = jest.fn().mockResolvedValue(availableWorkItemsResponse); - const groupSearchedWorkItemResolver = jest.fn().mockResolvedValue(searchedWorkItemsResponse); - const searchedWorkItemResolver = jest.fn().mockResolvedValue(searchedWorkItemsResponse); + const groupSearchedWorkItemResolver = jest.fn().mockResolvedValue(searchWorkItemsTextResponse); + const searchWorkItemTextResolver = jest.fn().mockResolvedValue(searchWorkItemsTextResponse); + const searchWorkItemIidResolver = jest.fn().mockResolvedValue(searchWorkItemsIidResponse); + const searchWorkItemTextIidResolver = jest.fn().mockResolvedValue(searchWorkItemsTextIidResponse); const createComponent = async ({ workItemsToAdd = [], parentConfidential = false, childrenType = WORK_ITEM_TYPE_ENUM_TASK, areWorkItemsToAddValid = true, - workItemsResolver = searchedWorkItemResolver, + workItemsResolver = searchWorkItemTextResolver, isGroup = false, } = {}) => { wrapper = shallowMountExtended(WorkItemTokenInput, { @@ -50,6 +57,7 @@ describe('WorkItemTokenInput', () => { }; const findTokenSelector = () => wrapper.findComponent(GlTokenSelector); + const findGlAlert = () => wrapper.findComponent(GlAlert); it('searches for available work items on focus', async () => { createComponent({ workItemsResolver: availableWorkItemsResolver }); @@ -61,24 +69,34 @@ describe('WorkItemTokenInput', () => { searchTerm: '', types: [WORK_ITEM_TYPE_ENUM_TASK], in: undefined, + iid: null, + isNumber: false, }); expect(findTokenSelector().props('dropdownItems')).toHaveLength(3); }); - it('searches for available work items when typing in input', async () => { - createComponent({ workItemsResolver: searchedWorkItemResolver }); - findTokenSelector().vm.$emit('focus'); - findTokenSelector().vm.$emit('text-input', 'Task 2'); - await waitForPromises(); - - expect(searchedWorkItemResolver).toHaveBeenCalledWith({ - fullPath: 'test-project-path', - searchTerm: 'Task 2', - types: [WORK_ITEM_TYPE_ENUM_TASK], - in: 'TITLE', - }); - expect(findTokenSelector().props('dropdownItems')).toHaveLength(1); - }); + it.each` + inputType | input | resolver | searchTerm | iid | isNumber | length + ${'iid'} | ${'101'} | ${searchWorkItemIidResolver} | ${'101'} | ${'101'} | ${true} | ${1} + ${'text'} | ${'Task 2'} | ${searchWorkItemTextResolver} | ${'Task 2'} | ${null} | ${false} | ${1} + ${'iid and text'} | ${'123'} | ${searchWorkItemTextIidResolver} | ${'123'} | ${'123'} | ${true} | ${2} + `( + 'searches by $inputType for available work items when typing in input', + async ({ input, resolver, searchTerm, iid, isNumber, length }) => { + createComponent({ workItemsResolver: resolver }); + findTokenSelector().vm.$emit('focus'); + findTokenSelector().vm.$emit('text-input', input); + await waitForPromises(); + + expect(resolver).toHaveBeenCalledWith({ + searchTerm, + in: 'TITLE', + iid, + isNumber, + }); + expect(findTokenSelector().props('dropdownItems')).toHaveLength(length); + }, + ); it('renders red border around token selector input when work item is not valid', () => { createComponent({ @@ -95,7 +113,7 @@ describe('WorkItemTokenInput', () => { }); it('calls the project work items query', () => { - expect(searchedWorkItemResolver).toHaveBeenCalled(); + expect(searchWorkItemTextResolver).toHaveBeenCalled(); }); it('skips calling the group work items query', () => { @@ -110,11 +128,35 @@ describe('WorkItemTokenInput', () => { }); it('skips calling the project work items query', () => { - expect(searchedWorkItemResolver).not.toHaveBeenCalled(); + expect(searchWorkItemTextResolver).not.toHaveBeenCalled(); }); it('calls the group work items query', () => { expect(groupSearchedWorkItemResolver).toHaveBeenCalled(); }); }); + + describe('when project work items query fails', () => { + beforeEach(() => { + createComponent({ + workItemsResolver: jest + .fn() + .mockRejectedValue('Something went wrong while fetching the results'), + }); + findTokenSelector().vm.$emit('focus'); + }); + + it('shows error and allows error alert to be closed', async () => { + await waitForPromises(); + expect(findGlAlert().exists()).toBe(true); + expect(findGlAlert().text()).toBe( + 'Something went wrong while fetching the task. Please try again.', + ); + + findGlAlert().vm.$emit('dismiss'); + await nextTick(); + + expect(findGlAlert().exists()).toBe(false); + }); + }); }); diff --git a/spec/frontend/work_items/components/work_item_parent_spec.js b/spec/frontend/work_items/components/work_item_parent_spec.js index 0c02f0c63ec45b51ba7501d746378ea214d87463..11fe6dffbfa416af16b20b64539b67c558ce9867 100644 --- a/spec/frontend/work_items/components/work_item_parent_spec.js +++ b/spec/frontend/work_items/components/work_item_parent_spec.js @@ -148,15 +148,27 @@ describe('WorkItemParent component', () => { }); await findCollapsibleListbox().vm.$emit('shown'); - await findCollapsibleListbox().vm.$emit('search', 'Objective 101'); await waitForPromises(); + expect(searchedItemQueryHandler).toHaveBeenCalledWith({ + fullPath: 'full-path', + searchTerm: '', + types: [WORK_ITEM_TYPE_ENUM_OBJECTIVE], + in: undefined, + iid: null, + isNumber: false, + }); + + await findCollapsibleListbox().vm.$emit('search', 'Objective 101'); + expect(searchedItemQueryHandler).toHaveBeenCalledWith({ fullPath: 'full-path', searchTerm: 'Objective 101', types: [WORK_ITEM_TYPE_ENUM_OBJECTIVE], in: 'TITLE', + iid: null, + isNumber: false, }); await nextTick(); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index 06d59a3436798e628ffff2d498aaa0e95d774b6a..41e8a01de362723d4577834e7ac23a19bf9f10cc 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -1600,27 +1600,18 @@ export const availableWorkItemsResponse = { id: 'gid://gitlab/WorkItem/458', iid: '2', title: 'Task 1', - state: 'OPEN', - createdAt: '2022-08-03T12:41:54Z', - confidential: false, __typename: 'WorkItem', }, { id: 'gid://gitlab/WorkItem/459', iid: '3', title: 'Task 2', - state: 'OPEN', - createdAt: '2022-08-03T12:41:54Z', - confidential: false, __typename: 'WorkItem', }, { id: 'gid://gitlab/WorkItem/460', iid: '4', title: 'Task 3', - state: 'OPEN', - createdAt: '2022-08-03T12:41:54Z', - confidential: true, __typename: 'WorkItem', }, ], @@ -1640,24 +1631,18 @@ export const availableObjectivesResponse = { id: 'gid://gitlab/WorkItem/716', iid: '122', title: 'Objective 101', - state: 'OPEN', - confidential: false, __typename: 'WorkItem', }, { id: 'gid://gitlab/WorkItem/712', iid: '118', title: 'Objective 103', - state: 'OPEN', - confidential: false, __typename: 'WorkItem', }, { id: 'gid://gitlab/WorkItem/711', iid: '117', title: 'Objective 102', - state: 'OPEN', - confidential: false, __typename: 'WorkItem', }, ], @@ -1677,8 +1662,6 @@ export const searchedObjectiveResponse = { id: 'gid://gitlab/WorkItem/716', iid: '122', title: 'Objective 101', - state: 'OPEN', - confidential: false, __typename: 'WorkItem', }, ], @@ -1687,7 +1670,7 @@ export const searchedObjectiveResponse = { }, }; -export const searchedWorkItemsResponse = { +export const searchWorkItemsTextResponse = { data: { workspace: { __typename: 'Project', @@ -1698,9 +1681,57 @@ export const searchedWorkItemsResponse = { id: 'gid://gitlab/WorkItem/459', iid: '3', title: 'Task 2', - state: 'OPEN', - createdAt: '2022-08-03T12:41:54Z', - confidential: false, + __typename: 'WorkItem', + }, + ], + }, + }, + }, +}; + +export const searchWorkItemsIidResponse = { + data: { + workspace: { + __typename: 'Project', + id: 'gid://gitlab/Project/2', + workItems: { + nodes: [], + }, + workItemsByIid: { + nodes: [ + { + id: 'gid://gitlab/WorkItem/460', + iid: '101', + title: 'Task 3', + __typename: 'WorkItem', + }, + ], + }, + }, + }, +}; + +export const searchWorkItemsTextIidResponse = { + data: { + workspace: { + __typename: 'Project', + id: 'gid://gitlab/Project/2', + workItems: { + nodes: [ + { + id: 'gid://gitlab/WorkItem/459', + iid: '3', + title: 'Task 123', + __typename: 'WorkItem', + }, + ], + }, + workItemsByIid: { + nodes: [ + { + id: 'gid://gitlab/WorkItem/460', + iid: '123', + title: 'Task 2', __typename: 'WorkItem', }, ],