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 @@
-
- {{ token.iid }} {{ token.title }}
-
-
-
{{ dropdownItem.iid }}
-
{{ dropdownItem.title }}
-
-
-
+
+
+ {{ error }}
+
+
+ {{ token.iid }} {{ token.title }}
+
+
+
+
+
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',
},
],