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
new file mode 100644
index 0000000000000000000000000000000000000000..7b38e83803325f99648fdb81a63b9c27ffad926f
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue
@@ -0,0 +1,145 @@
+
+
+
+
+ {{ token.title }}
+
+
+
+
{{ getIdFromGraphQLId(dropdownItem.id) }}
+
{{ dropdownItem.title }}
+
+
+
+
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 4960189fb488a748b859935b8b36568e12c3f9a9..a58752a861a4124c54ab92ff8b3bff19914aba15 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
@@ -3,19 +3,15 @@ import {
GlAlert,
GlFormGroup,
GlForm,
- GlTokenSelector,
GlButton,
GlFormInput,
GlFormCheckbox,
GlTooltip,
} from '@gitlab/ui';
-import { debounce } from 'lodash';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { __, s__, sprintf } from '~/locale';
+import WorkItemTokenInput from '../shared/work_item_token_input.vue';
import { addHierarchyChild } from '../../graphql/cache_utils';
import projectWorkItemTypesQuery from '../../graphql/project_work_item_types.query.graphql';
-import projectWorkItemsQuery from '../../graphql/project_work_items.query.graphql';
import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql';
import createWorkItemMutation from '../../graphql/create_work_item.mutation.graphql';
import {
@@ -23,7 +19,6 @@ import {
WORK_ITEMS_TYPE_MAP,
WORK_ITEM_TYPE_ENUM_TASK,
I18N_WORK_ITEM_CREATE_BUTTON_LABEL,
- I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER,
I18N_WORK_ITEM_ADD_BUTTON_LABEL,
I18N_WORK_ITEM_ADD_MULTIPLE_BUTTON_LABEL,
I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_LABEL,
@@ -35,12 +30,12 @@ export default {
components: {
GlAlert,
GlForm,
- GlTokenSelector,
GlButton,
GlFormGroup,
GlFormInput,
GlFormCheckbox,
GlTooltip,
+ WorkItemTokenInput,
},
inject: ['fullPath', 'hasIterationsFeature'],
props: {
@@ -101,35 +96,14 @@ export default {
return data.workspace?.workItemTypes?.nodes;
},
},
- availableWorkItems: {
- query: projectWorkItemsQuery,
- variables() {
- return {
- fullPath: this.fullPath,
- searchTerm: this.search?.title || this.search,
- types: [this.childrenType],
- in: this.search ? 'TITLE' : undefined,
- };
- },
- skip() {
- return !this.searchStarted;
- },
- update(data) {
- return data.workspace.workItems.nodes.filter(
- (wi) => !this.childrenIds.includes(wi.id) && this.issuableGid !== wi.id,
- );
- },
- },
},
data() {
return {
workItemTypes: [],
- availableWorkItems: [],
- search: '',
- searchStarted: false,
+ workItemsToAdd: [],
error: null,
+ search: '',
childToCreateTitle: null,
- workItemsToAdd: [],
confidential: this.parentConfidential,
};
},
@@ -216,15 +190,6 @@ export default {
}
return this.workItemsToAdd.length === 0 || !this.areWorkItemsToAddValid;
},
- isLoading() {
- return this.$apollo.queries.availableWorkItems.loading;
- },
- addInputPlaceholder() {
- return sprintfWorkItem(I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER, this.childrenTypeName);
- },
- tokenSelectorContainerClass() {
- return !this.areWorkItemsToAddValid ? 'gl-inset-border-1-red-500!' : '';
- },
invalidWorkItemsToAdd() {
return this.parentConfidential
? this.workItemsToAdd.filter((workItem) => !workItem.confidential)
@@ -249,11 +214,7 @@ export default {
);
},
},
- created() {
- this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
- },
methods: {
- getIdFromGraphQLId,
getConfidentialityTooltipTarget() {
// We want tooltip to be anchored to `input` within checkbox component
// but `$el.querySelector('input')` doesn't work. 🤷♂️
@@ -317,20 +278,6 @@ export default {
this.childToCreateTitle = null;
});
},
- setSearchKey(value) {
- this.search = value;
- },
- handleFocus() {
- this.searchStarted = true;
- },
- handleMouseOver() {
- this.timeout = setTimeout(() => {
- this.searchStarted = true;
- }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
- },
- handleMouseOut() {
- clearTimeout(this.timeout);
- },
},
i18n: {
inputLabel: __('Title'),
@@ -385,30 +332,16 @@ export default {
>{{ confidentialityCheckboxTooltip }}
-
-
- {{ token.title }}
-
-
-
-
{{ getIdFromGraphQLId(dropdownItem.id) }}
-
{{ dropdownItem.title }}
-
-
-
+ :is-create-form="isCreateForm"
+ :parent-work-item-id="issuableGid"
+ :children-type="childrenType"
+ :children-ids="childrenIds"
+ :are-work-items-to-add-valid="areWorkItemsToAddValid"
+ :full-path="fullPath"
+ />
{
+ let wrapper;
+
+ const availableWorkItemsResolver = jest.fn().mockResolvedValue(availableWorkItemsResponse);
+ const searchedWorkItemResolver = jest.fn().mockResolvedValue(searchedWorkItemsResponse);
+
+ const createComponent = async ({
+ workItemsToAdd = [],
+ parentConfidential = false,
+ childrenType = WORK_ITEM_TYPE_ENUM_TASK,
+ areWorkItemsToAddValid = true,
+ workItemsResolver = searchedWorkItemResolver,
+ } = {}) => {
+ wrapper = shallowMountExtended(WorkItemTokenInput, {
+ apolloProvider: createMockApollo([[projectWorkItemsQuery, workItemsResolver]]),
+ propsData: {
+ value: workItemsToAdd,
+ childrenType,
+ childrenIds: [],
+ fullPath: 'test-project-path',
+ parentWorkItemId: 'gid://gitlab/WorkItem/1',
+ parentConfidential,
+ areWorkItemsToAddValid,
+ },
+ });
+
+ await waitForPromises();
+ };
+
+ const findTokenSelector = () => wrapper.findComponent(GlTokenSelector);
+
+ it('searches for available work items on focus', async () => {
+ createComponent({ workItemsResolver: availableWorkItemsResolver });
+ findTokenSelector().vm.$emit('focus');
+ await waitForPromises();
+
+ expect(availableWorkItemsResolver).toHaveBeenCalledWith({
+ fullPath: 'test-project-path',
+ searchTerm: '',
+ types: [WORK_ITEM_TYPE_ENUM_TASK],
+ in: undefined,
+ });
+ 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('renders red border around token selector input when work item is not valid', () => {
+ createComponent({
+ areWorkItemsToAddValid: false,
+ });
+
+ expect(findTokenSelector().props('containerClass')).toBe('gl-inset-border-1-red-500!');
+ });
+});
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 8caacc2dc972c043c01204f0dd556abd0182ed32..aaab22fd18dbf007f8ec0ddeaf63065aadb4268a 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
@@ -1,11 +1,12 @@
import Vue from 'vue';
-import { GlForm, GlFormInput, GlFormCheckbox, GlTooltip, GlTokenSelector } from '@gitlab/ui';
+import { GlForm, GlFormInput, GlFormCheckbox, GlTooltip } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import { sprintf, s__ } from '~/locale';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue';
+import WorkItemTokenInput from '~/work_items/components/shared/work_item_token_input.vue';
import {
FORM_TYPES,
WORK_ITEM_TYPE_ENUM_TASK,
@@ -70,10 +71,12 @@ describe('WorkItemLinksForm', () => {
};
const findForm = () => wrapper.findComponent(GlForm);
- const findTokenSelector = () => wrapper.findComponent(GlTokenSelector);
+ const findWorkItemTokenInput = () => wrapper.findComponent(WorkItemTokenInput);
const findInput = () => wrapper.findComponent(GlFormInput);
const findConfidentialCheckbox = () => wrapper.findComponent(GlFormCheckbox);
+ const findTooltip = () => wrapper.findComponent(GlTooltip);
const findAddChildButton = () => wrapper.findByTestId('add-child-button');
+ const findValidationElement = () => wrapper.findByTestId('work-items-invalid');
describe('creating a new work item', () => {
beforeEach(async () => {
@@ -84,7 +87,7 @@ describe('WorkItemLinksForm', () => {
expect(findForm().exists()).toBe(true);
expect(findInput().exists()).toBe(true);
expect(findAddChildButton().text()).toBe('Create task');
- expect(findTokenSelector().exists()).toBe(false);
+ expect(findWorkItemTokenInput().exists()).toBe(false);
});
it('creates child task in non confidential parent', async () => {
@@ -137,7 +140,7 @@ describe('WorkItemLinksForm', () => {
const confidentialCheckbox = findConfidentialCheckbox();
expect(confidentialCheckbox.exists()).toBe(true);
- expect(wrapper.findComponent(GlTooltip).exists()).toBe(false);
+ expect(findTooltip().exists()).toBe(false);
expect(confidentialCheckbox.text()).toBe(
sprintf(I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_LABEL, {
workItemType: WORK_ITEM_TYPE_ENUM_TASK.toLocaleLowerCase(),
@@ -149,12 +152,11 @@ describe('WorkItemLinksForm', () => {
createComponent({ parentConfidential: true });
const confidentialCheckbox = findConfidentialCheckbox();
- const confidentialTooltip = wrapper.findComponent(GlTooltip);
expect(confidentialCheckbox.attributes('disabled')).toBeDefined();
expect(confidentialCheckbox.attributes('checked')).toBe('true');
- expect(confidentialTooltip.exists()).toBe(true);
- expect(confidentialTooltip.text()).toBe(
+ expect(findTooltip().exists()).toBe(true);
+ expect(findTooltip().text()).toBe(
sprintf(I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_TOOLTIP, {
workItemType: WORK_ITEM_TYPE_ENUM_TASK.toLocaleLowerCase(),
parentWorkItemType: WORK_ITEM_TYPE_VALUE_ISSUE.toLocaleLowerCase(),
@@ -165,14 +167,11 @@ describe('WorkItemLinksForm', () => {
});
describe('adding an existing work item', () => {
- const selectAvailableWorkItemTokens = async () => {
- findTokenSelector().vm.$emit(
+ const selectAvailableWorkItemTokens = () => {
+ findWorkItemTokenInput().vm.$emit(
'input',
availableWorkItemsResponse.data.workspace.workItems.nodes,
);
- findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null }));
-
- await waitForPromises();
};
beforeEach(async () => {
@@ -181,24 +180,31 @@ describe('WorkItemLinksForm', () => {
it('renders add form', () => {
expect(findForm().exists()).toBe(true);
- expect(findTokenSelector().exists()).toBe(true);
+ expect(findWorkItemTokenInput().exists()).toBe(true);
expect(findAddChildButton().text()).toBe('Add task');
expect(findInput().exists()).toBe(false);
expect(findConfidentialCheckbox().exists()).toBe(false);
});
- it('searches for available work items as prop when typing in input', async () => {
- findTokenSelector().vm.$emit('focus');
- findTokenSelector().vm.$emit('text-input', 'Task');
- await waitForPromises();
-
- expect(availableWorkItemsResolver).toHaveBeenCalled();
+ it('renders work item token input with default props', () => {
+ expect(findWorkItemTokenInput().props()).toMatchObject({
+ value: [],
+ fullPath: 'project/path',
+ childrenType: WORK_ITEM_TYPE_ENUM_TASK,
+ childrenIds: [],
+ parentWorkItemId: 'gid://gitlab/WorkItem/1',
+ areWorkItemsToAddValid: true,
+ });
});
it('selects and adds children', async () => {
await selectAvailableWorkItemTokens();
expect(findAddChildButton().text()).toBe('Add tasks');
+ expect(findWorkItemTokenInput().props('areWorkItemsToAddValid')).toBe(true);
+ expect(findWorkItemTokenInput().props('value')).toBe(
+ availableWorkItemsResponse.data.workspace.workItems.nodes,
+ );
findForm().vm.$emit('submit', {
preventDefault: jest.fn(),
});
@@ -211,9 +217,9 @@ describe('WorkItemLinksForm', () => {
await selectAvailableWorkItemTokens();
- const validationEl = wrapper.findByTestId('work-items-invalid');
- expect(validationEl.exists()).toBe(true);
- expect(validationEl.text().trim()).toBe(
+ expect(findWorkItemTokenInput().props('areWorkItemsToAddValid')).toBe(false);
+ expect(findValidationElement().exists()).toBe(true);
+ expect(findValidationElement().text().trim()).toBe(
sprintf(
s__(
'WorkItem|%{invalidWorkItemsList} cannot be added: Cannot assign a non-confidential %{childWorkItemType} to a confidential parent %{parentWorkItemType}. Make the selected %{childWorkItemType} confidential and try again.',
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index 05e83c0df3da3e4bc4bb0c4fa4dacddf460c86a6..ea4115f178f5a1e99f775573c5b43e8ca6b66338 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -1471,6 +1471,27 @@ export const availableWorkItemsResponse = {
},
};
+export const searchedWorkItemsResponse = {
+ data: {
+ workspace: {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/2',
+ workItems: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/459',
+ title: 'Task 2',
+ state: 'OPEN',
+ createdAt: '2022-08-03T12:41:54Z',
+ confidential: false,
+ __typename: 'WorkItem',
+ },
+ ],
+ },
+ },
+ },
+};
+
export const projectMembersResponseWithCurrentUser = {
data: {
workspace: {