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 c122db6c9024ee94f6d6e1b09089df2ecbe59453..f7f9ca4315a71f03e7acb60b47e1a131fee064ce 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,25 @@
@@ -188,10 +216,11 @@ export default {
v-model="workItemsToAdd"
:dropdown-items="availableWorkItems"
:loading="isLoading"
- :placeholder="addInputPlaceholder"
+ :placeholder="$options.i18n.addInputPlaceholder"
menu-class="gl-dropdown-menu-wide dropdown-reduced-height gl-min-h-7!"
:container-class="tokenSelectorContainerClass"
data-testid="work-item-token-select-input"
+ :text-input-attrs="textInputAttrs"
@text-input="debouncedSearchKeyUpdate"
@focus="handleFocus"
@mouseover.native="handleMouseOver"
@@ -210,6 +239,11 @@ export default {
+
+ {{
+ $options.i18n.noMatchesFoundMessage
+ }}
+
diff --git a/app/assets/javascripts/work_items/components/work_item_parent_inline.vue b/app/assets/javascripts/work_items/components/work_item_parent_inline.vue
index 0c0842a3e051e85801f66febe6c840e0f74356be..bb75de677c3ec7e684020f6c58e8bd765fba927c 100644
--- a/app/assets/javascripts/work_items/components/work_item_parent_inline.vue
+++ b/app/assets/javascripts/work_items/components/work_item_parent_inline.vue
@@ -108,7 +108,7 @@ export default {
types: this.parentType,
in: this.search ? 'TITLE' : undefined,
iid: null,
- isNumber: false,
+ searchByIid: false,
};
},
skip() {
diff --git a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue
index c98bd6ce1e9878d6caffa703cb9c4affa5036254..10c59d677f748f3f417a700554fee37908010fd8 100644
--- a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue
+++ b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue
@@ -172,7 +172,7 @@ export default {
relatedToLabel: s__('WorkItem|relates to'),
blockingLabel: s__('WorkItem|blocks'),
blockedByLabel: s__('WorkItem|is blocked by'),
- linkItemInputLabel: s__('WorkItem|the following item(s)'),
+ linkItemInputLabel: s__('WorkItem|the following items'),
addLinkedItemErrorMessage: s__(
'WorkItem|Something went wrong when trying to link a item. Please try again.',
),
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index 41cf5d8932d5072770dd1e23127eeadbecf754c9..51f63b147ac2a7fa23dff20d1129cc54c04b8988 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -46,6 +46,9 @@ export const WORK_ITEM_TYPE_VALUE_REQUIREMENTS = 'Requirements';
export const WORK_ITEM_TYPE_VALUE_KEY_RESULT = 'Key Result';
export const WORK_ITEM_TYPE_VALUE_OBJECTIVE = 'Objective';
+export const NAMESPACE_GROUP = 'group';
+export const NAMESPACE_PROJECT = 'project';
+
export const WORK_ITEM_TITLE_MAX_LENGTH = 255;
export const i18n = {
@@ -91,10 +94,13 @@ 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 items');
+export const I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER = s__(
+ 'WorkItem|Search existing items, paste URL, or enter reference ID',
+);
export const I18N_WORK_ITEM_SEARCH_ERROR = s__(
'WorkItem|Something went wrong while fetching the %{workItemType}. Please try again.',
);
+export const I18N_WORK_ITEM_NO_MATCHES_FOUND = s__('WorkItem|No matches found');
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/project_work_items.query.graphql b/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql
index 7efd67467e5644c33a158de94af26929bd94a621..17b338f7a8d98cb673583d89bbfbdd5431837926 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
@@ -4,11 +4,12 @@ query projectWorkItems(
$types: [IssueType!]
$in: [IssuableSearchableField!]
$iid: String = null
- $isNumber: Boolean!
+ $searchByIid: Boolean = false
+ $searchByText: Boolean = true
) {
workspace: project(fullPath: $fullPath) {
id
- workItems(search: $searchTerm, types: $types, in: $in) {
+ workItems(search: $searchTerm, types: $types, in: $in) @include(if: $searchByText) {
nodes {
id
iid
@@ -16,7 +17,7 @@ query projectWorkItems(
confidential
}
}
- workItemsByIid: workItems(iid: $iid, types: $types) @include(if: $isNumber) {
+ workItemsByIid: workItems(iid: $iid, types: $types) @include(if: $searchByIid) {
nodes {
id
iid
diff --git a/app/assets/javascripts/work_items/graphql/work_items_by_references.query.graphql b/app/assets/javascripts/work_items/graphql/work_items_by_references.query.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..1e8d62596b747e9088de1273fe1b31d00d902590
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/work_items_by_references.query.graphql
@@ -0,0 +1,10 @@
+query getWorkItemsByReferences($contextNamespacePath: ID!, $refs: [String!]!) {
+ workItemsByReference(contextNamespacePath: $contextNamespacePath, refs: $refs) {
+ nodes {
+ id
+ iid
+ title
+ confidential
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/utils.js b/app/assets/javascripts/work_items/utils.js
index c3c292c3dd94e8c1265804e62f33a1b7d9b7207f..6d304e7ebf01abae5dc3d9def4d9a11bfff085a2 100644
--- a/app/assets/javascripts/work_items/utils.js
+++ b/app/assets/javascripts/work_items/utils.js
@@ -55,3 +55,14 @@ export const markdownPreviewPath = (fullPath, iid) =>
`${
gon.relative_url_root || ''
}/${fullPath}/preview_markdown?target_type=WorkItem&target_id=${iid}`;
+
+export const isReference = (input) => {
+ /**
+ * The regular expression checks if the `value` is
+ * a project work item or group work item.
+ * e.g., gitlab-org/project-path#101 or gitlab-org&101
+ * or #1234
+ */
+
+ return /^([\w-]+(?:\/[\w-]+)*)?[#&](\d+)$/.test(input);
+};
diff --git a/doc/user/okrs.md b/doc/user/okrs.md
index 2d24bf0a193e46c042925b84e6bfc58a55da22ef..980eb6f84bb920c3898af9b1b5df49c59380bc6f 100644
--- a/doc/user/okrs.md
+++ b/doc/user/okrs.md
@@ -498,6 +498,7 @@ or assignees, on the right.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/416558) in GitLab 16.5 [with a flag](../administration/feature_flags.md) named `linked_work_items`. Enabled by default.
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/139394) in GitLab 16.7.
+> - Adding related items by entering their URLs and IDs [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/427594) in GitLab 16.8.
FLAG:
On self-managed GitLab, by default this feature is available. To hide the feature, an administrator can [disable the feature flag](../administration/feature_flags.md) named `linked_work_items`.
@@ -522,7 +523,7 @@ To link an item to an objective or key result:
- **Relates to**
- **Blocks**
- **Is blocked by**
-1. Enter the search text of the item.
+1. Enter the search text of the item, URL, or its reference ID.
1. When you have added all the items to be linked, select **Add** below the search box.
When you have finished adding all linked items, you can see
diff --git a/doc/user/tasks.md b/doc/user/tasks.md
index 1ec211dcab3ae8647bb2f6afc64b59820d7a250f..12b12ceccc654bd950cea429de7019bed473fb8e 100644
--- a/doc/user/tasks.md
+++ b/doc/user/tasks.md
@@ -497,6 +497,7 @@ or assignees, on the right.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/416558) in GitLab 16.5 [with a flag](../administration/feature_flags.md) named `linked_work_items`. Disabled by default.
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/139394) in GitLab 16.7.
+> - Adding related items by entering their URLs and IDs [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/427594) in GitLab 16.8.
FLAG:
On self-managed GitLab, by default this feature is available. To hide the feature, an administrator can [disable the feature flag](../administration/feature_flags.md) named `linked_work_items`.
@@ -524,7 +525,7 @@ To link an item to a task:
- **relates to**
- **blocks**
- **is blocked by**
-1. Enter the search text of the item.
+1. Enter the search text of the item, URL, or its reference ID.
1. When you have added all the items to be linked, select **Add** below the search box.
When you have finished adding all linked items, you can see
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f5478c84e575dcef689b64e8a37552d30e2814ed..db7d999cf1dd74b4b742bfc5a0f2a6135c337550 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -55156,6 +55156,9 @@ msgstr ""
msgid "WorkItem|No iteration"
msgstr ""
+msgid "WorkItem|No matches found"
+msgstr ""
+
msgid "WorkItem|No matching results"
msgstr ""
@@ -55207,7 +55210,7 @@ msgstr ""
msgid "WorkItem|Save and overwrite"
msgstr ""
-msgid "WorkItem|Search existing items"
+msgid "WorkItem|Search existing items, paste URL, or enter reference ID"
msgstr ""
msgid "WorkItem|Select type"
@@ -55372,7 +55375,7 @@ msgstr ""
msgid "WorkItem|relates to"
msgstr ""
-msgid "WorkItem|the following item(s)"
+msgid "WorkItem|the following items"
msgstr ""
msgid "Workspaces"
diff --git a/spec/features/projects/work_items/linked_work_items_spec.rb b/spec/features/projects/work_items/linked_work_items_spec.rb
index 49f723c3055408bea18196c902297ecb3287c166..963be23e5a8d4b218cdadfc61042f130925995f3 100644
--- a/spec/features/projects/work_items/linked_work_items_spec.rb
+++ b/spec/features/projects/work_items/linked_work_items_spec.rb
@@ -11,6 +11,8 @@
let_it_be(:task) { create(:work_item, :task, project: project, title: 'Task 1') }
context 'for signed in user' do
+ let(:token_input_selector) { '[data-testid="work-item-token-select-input"] .gl-token-selector-input' }
+
before_all do
project.add_developer(user)
end
@@ -62,25 +64,24 @@
end
end
- it 'links a new item', :aggregate_failures do
- page.within('.work-item-relationships') do
- click_button 'Add'
-
- within_testid('link-work-item-form') do
- expect(page).to have_button('Add', disabled: true)
- find_by_testid('work-item-token-select-input').set(task.title)
- wait_for_all_requests
- click_button task.title
+ it 'links a new item with work item text', :aggregate_failures do
+ verify_linked_item_added(task.title)
+ end
- expect(page).to have_button('Add', disabled: false)
+ it 'links a new item with work item iid', :aggregate_failures do
+ verify_linked_item_added(task.iid)
+ end
- click_button 'Add'
+ it 'links a new item with work item wildcard iid', :aggregate_failures do
+ verify_linked_item_added("##{task.iid}")
+ end
- wait_for_all_requests
- end
+ it 'links a new item with work item reference', :aggregate_failures do
+ verify_linked_item_added(task.to_reference(full: true))
+ end
- expect(find('.work-items-list')).to have_content('Task 1')
- end
+ it 'links a new item with work item url', :aggregate_failures do
+ verify_linked_item_added("#{task.project.web_url}/-/work_items/#{task.iid}")
end
it 'removes a linked item', :aggregate_failures do
@@ -111,4 +112,27 @@
end
end
end
+
+ def verify_linked_item_added(input)
+ page.within('.work-item-relationships') do
+ click_button 'Add'
+
+ within_testid('link-work-item-form') do
+ expect(page).to have_button('Add', disabled: true)
+
+ find(token_input_selector).set(input)
+ wait_for_all_requests
+
+ click_button task.title
+
+ expect(page).to have_button('Add', disabled: false)
+
+ click_button 'Add'
+
+ wait_for_all_requests
+ end
+
+ expect(find('.work-items-list')).to have_content('Task 1')
+ end
+ end
end
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 5726aaaa2d0419430fc2b966b62f797e1fa4015c..8ad2a34055e38cc4c785a73eca7ece763de01192 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
@@ -8,23 +8,78 @@ 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,
- searchWorkItemsTextResponse,
- searchWorkItemsIidResponse,
- searchWorkItemsTextIidResponse,
-} from '../../mock_data';
+import workItemsByReferencesQuery from '~/work_items/graphql/work_items_by_references.query.graphql';
+import { searchWorkItemsResponse } from '../../mock_data';
Vue.use(VueApollo);
describe('WorkItemTokenInput', () => {
let wrapper;
- const availableWorkItemsResolver = jest.fn().mockResolvedValue(availableWorkItemsResponse);
- 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 availableWorkItemsResolver = jest.fn().mockResolvedValue(
+ searchWorkItemsResponse({
+ workItems: [
+ {
+ id: 'gid://gitlab/WorkItem/458',
+ iid: '2',
+ title: 'Task 1',
+ confidential: false,
+ __typename: 'WorkItem',
+ },
+ {
+ id: 'gid://gitlab/WorkItem/459',
+ iid: '3',
+ title: 'Task 2',
+ confidential: false,
+ __typename: 'WorkItem',
+ },
+ {
+ id: 'gid://gitlab/WorkItem/460',
+ iid: '4',
+ title: 'Task 3',
+ confidential: false,
+ __typename: 'WorkItem',
+ },
+ ],
+ }),
+ );
+
+ const mockWorkItem = {
+ id: 'gid://gitlab/WorkItem/459',
+ iid: '3',
+ title: 'Task 2',
+ confidential: false,
+ __typename: 'WorkItem',
+ };
+ const groupSearchedWorkItemResolver = jest.fn().mockResolvedValue(
+ searchWorkItemsResponse({
+ workItems: [mockWorkItem],
+ }),
+ );
+ const searchWorkItemTextResolver = jest.fn().mockResolvedValue(
+ searchWorkItemsResponse({
+ workItems: [mockWorkItem],
+ }),
+ );
+ const mockworkItemReferenceQueryResponse = {
+ data: {
+ workItemsByReference: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/705',
+ iid: '111',
+ title: 'Objective linked items 104',
+ confidential: false,
+ __typename: 'WorkItem',
+ },
+ ],
+ __typename: 'WorkItemConnection',
+ },
+ },
+ };
+ const workItemReferencesQueryResolver = jest
+ .fn()
+ .mockResolvedValue(mockworkItemReferenceQueryResponse);
const createComponent = async ({
workItemsToAdd = [],
@@ -38,6 +93,7 @@ describe('WorkItemTokenInput', () => {
apolloProvider: createMockApollo([
[projectWorkItemsQuery, workItemsResolver],
[groupWorkItemsQuery, groupSearchedWorkItemResolver],
+ [workItemsByReferencesQuery, workItemReferencesQueryResolver],
]),
provide: {
isGroup,
@@ -58,6 +114,7 @@ describe('WorkItemTokenInput', () => {
const findTokenSelector = () => wrapper.findComponent(GlTokenSelector);
const findGlAlert = () => wrapper.findComponent(GlAlert);
+ const findNoMatchFoundMessage = () => wrapper.findByTestId('no-match-found-namespace-message');
it('searches for available work items on focus', async () => {
createComponent({ workItemsResolver: availableWorkItemsResolver });
@@ -68,42 +125,151 @@ describe('WorkItemTokenInput', () => {
fullPath: 'test-project-path',
searchTerm: '',
types: [WORK_ITEM_TYPE_ENUM_TASK],
- in: undefined,
iid: null,
- isNumber: false,
+ searchByIid: false,
+ searchByText: true,
});
expect(findTokenSelector().props('dropdownItems')).toHaveLength(3);
});
- 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 });
+ 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!');
+ });
+
+ describe('when input data is provided', () => {
+ const fillWorkItemInput = (input) => {
findTokenSelector().vm.$emit('focus');
findTokenSelector().vm.$emit('text-input', input);
+ };
+
+ const mockWorkItemResponseItem1 = {
+ id: 'gid://gitlab/WorkItem/460',
+ iid: '101',
+ title: 'Task 3',
+ confidential: false,
+ __typename: 'WorkItem',
+ };
+ const mockWorkItemResponseItem2 = {
+ id: 'gid://gitlab/WorkItem/461',
+ iid: '3',
+ title: 'Task 123',
+ confidential: false,
+ __typename: 'WorkItem',
+ };
+ const mockWorkItemResponseItem3 = {
+ id: 'gid://gitlab/WorkItem/462',
+ iid: '123',
+ title: 'Task 2',
+ confidential: false,
+ __typename: 'WorkItem',
+ };
+
+ const searchWorkItemIidResolver = jest.fn().mockResolvedValue(
+ searchWorkItemsResponse({
+ workItemsByIid: [mockWorkItemResponseItem1],
+ }),
+ );
+ const searchWorkItemTextIidResolver = jest.fn().mockResolvedValue(
+ searchWorkItemsResponse({
+ workItems: [mockWorkItemResponseItem2],
+ workItemsByIid: [mockWorkItemResponseItem3],
+ }),
+ );
+
+ const emptyWorkItemResolver = jest.fn().mockResolvedValue(searchWorkItemsResponse());
+
+ const validIid = mockWorkItemResponseItem1.iid;
+ const validWildCardIid = `#${mockWorkItemResponseItem1.iid}`;
+ const searchedText = mockWorkItem.title;
+ const searchedIidText = mockWorkItemResponseItem3.iid;
+ const nonExistentIid = '111';
+ const nonExistentWorkItem = 'Key result';
+ const validWorkItemUrl = 'http://localhost/gitlab-org/test-project-path/-/work_items/111';
+ const validWorkItemReference = 'gitlab-org/test-project-path#111';
+ const invalidWorkItemUrl = 'invalid-url/gitlab-org/test-project-path/-/work_items/101';
+
+ it.each`
+ inputType | input | resolver | searchTerm | iid | searchByText | searchByIid | length
+ ${'iid'} | ${validIid} | ${searchWorkItemIidResolver} | ${validIid} | ${validIid} | ${true} | ${true} | ${1}
+ ${'text'} | ${searchedText} | ${searchWorkItemTextResolver} | ${searchedText} | ${null} | ${true} | ${false} | ${1}
+ ${'iid and text'} | ${searchedIidText} | ${searchWorkItemTextIidResolver} | ${searchedIidText} | ${searchedIidText} | ${true} | ${true} | ${2}
+ `(
+ 'lists work items when $inputType is valid',
+ async ({ input, resolver, searchTerm, iid, searchByIid, searchByText, length }) => {
+ createComponent({ workItemsResolver: resolver });
+
+ fillWorkItemInput(input);
+
+ await waitForPromises();
+
+ expect(resolver).toHaveBeenCalledWith({
+ searchTerm,
+ in: 'TITLE',
+ iid,
+ searchByIid,
+ searchByText,
+ });
+ expect(findTokenSelector().props('dropdownItems')).toHaveLength(length);
+ },
+ );
+
+ it.each`
+ inputType | input | searchTerm | iid | searchByText | searchByIid
+ ${'iid'} | ${nonExistentIid} | ${nonExistentIid} | ${nonExistentIid} | ${true} | ${true}
+ ${'text'} | ${nonExistentWorkItem} | ${nonExistentWorkItem} | ${null} | ${true} | ${false}
+ ${'url'} | ${invalidWorkItemUrl} | ${invalidWorkItemUrl} | ${null} | ${true} | ${false}
+ `(
+ 'list is empty when $inputType is invalid',
+ async ({ input, searchTerm, iid, searchByIid, searchByText }) => {
+ createComponent({ workItemsResolver: emptyWorkItemResolver });
+
+ fillWorkItemInput(input);
+
+ await waitForPromises();
+
+ expect(emptyWorkItemResolver).toHaveBeenCalledWith({
+ searchTerm,
+ in: 'TITLE',
+ iid,
+ searchByIid,
+ searchByText,
+ });
+ expect(findTokenSelector().props('dropdownItems')).toHaveLength(0);
+ },
+ );
+
+ it.each`
+ inputType | input | refs | length
+ ${'iid with wildcard'} | ${validWildCardIid} | ${[validWildCardIid]} | ${1}
+ ${'url'} | ${validWorkItemUrl} | ${[validWorkItemUrl]} | ${1}
+ ${'reference'} | ${validWorkItemReference} | ${[validWorkItemReference]} | ${1}
+ `('lists work items when valid $inputType is pasted', async ({ input, refs, length }) => {
+ createComponent({ workItemsResolver: workItemReferencesQueryResolver });
+
+ fillWorkItemInput(input);
+
await waitForPromises();
- expect(resolver).toHaveBeenCalledWith({
- searchTerm,
- in: 'TITLE',
- iid,
- isNumber,
+ expect(workItemReferencesQueryResolver).toHaveBeenCalledWith({
+ contextNamespacePath: 'test-project-path',
+ refs,
});
expect(findTokenSelector().props('dropdownItems')).toHaveLength(length);
- },
- );
-
- 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!');
+ it('shows proper message if provided with cross project URL', async () => {
+ createComponent({ workItemsResolver: emptyWorkItemResolver });
+
+ fillWorkItemInput('http://localhost/gitlab-org/cross-project-path/-/work_items/101');
+
+ await waitForPromises();
+
+ expect(findNoMatchFoundMessage().text()).toBe('No matches found');
+ });
});
describe('when project context', () => {
diff --git a/spec/frontend/work_items/components/work_item_parent_inline_spec.js b/spec/frontend/work_items/components/work_item_parent_inline_spec.js
index 3e4f99d593569f697aa2d18ae4876f8b067d4868..0cd314c377af33dcecc07c94d672996b06a57728 100644
--- a/spec/frontend/work_items/components/work_item_parent_inline_spec.js
+++ b/spec/frontend/work_items/components/work_item_parent_inline_spec.js
@@ -157,7 +157,8 @@ describe('WorkItemParentInline component', () => {
types: [WORK_ITEM_TYPE_ENUM_OBJECTIVE],
in: undefined,
iid: null,
- isNumber: false,
+ searchByIid: false,
+ searchByText: true,
});
await findCollapsibleListbox().vm.$emit('search', 'Objective 101');
@@ -168,7 +169,8 @@ describe('WorkItemParentInline component', () => {
types: [WORK_ITEM_TYPE_ENUM_OBJECTIVE],
in: 'TITLE',
iid: null,
- isNumber: false,
+ searchByIid: false,
+ searchByText: true,
});
await nextTick();
diff --git a/spec/frontend/work_items/components/work_item_parent_with_edit_spec.js b/spec/frontend/work_items/components/work_item_parent_with_edit_spec.js
index 61e43456479293e964b1b813b65949f1d6a9d2ff..d5fab9353ac233e4577c1d31a010c73e1bbcf4b2 100644
--- a/spec/frontend/work_items/components/work_item_parent_with_edit_spec.js
+++ b/spec/frontend/work_items/components/work_item_parent_with_edit_spec.js
@@ -290,6 +290,8 @@ describe('WorkItemParent component', () => {
in: undefined,
iid: null,
isNumber: false,
+ searchByIid: false,
+ searchByText: true,
});
await findCollapsibleListbox().vm.$emit('search', 'Objective 101');
@@ -301,6 +303,8 @@ describe('WorkItemParent component', () => {
in: 'TITLE',
iid: null,
isNumber: false,
+ searchByIid: false,
+ searchByText: true,
});
await nextTick();
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index 9d4606eb95ae29b6097e9d5ec66341ae1649fafa..9e39380750fb325b361cf17901e94a88a9c53a8a 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -1734,6 +1734,28 @@ export const searchWorkItemsIidResponse = {
},
};
+export const searchWorkItemsURLRefResponse = {
+ 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: {
@@ -1765,6 +1787,23 @@ export const searchWorkItemsTextIidResponse = {
},
};
+export const searchWorkItemsResponse = ({ workItems = [], workItemsByIid = [] } = {}) => {
+ return {
+ data: {
+ workspace: {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/2',
+ workItems: {
+ nodes: workItems,
+ },
+ workItemsByIid: {
+ nodes: workItemsByIid,
+ },
+ },
+ },
+ };
+};
+
export const projectMembersResponseWithCurrentUser = {
data: {
workspace: {
diff --git a/spec/frontend/work_items/utils_spec.js b/spec/frontend/work_items/utils_spec.js
index aa24b80cf08037828ec944a25d331955edcdf472..166712de20b8c32a402bc72e6f04e5b2a4ea1889 100644
--- a/spec/frontend/work_items/utils_spec.js
+++ b/spec/frontend/work_items/utils_spec.js
@@ -1,4 +1,4 @@
-import { autocompleteDataSources, markdownPreviewPath } from '~/work_items/utils';
+import { autocompleteDataSources, markdownPreviewPath, isReference } from '~/work_items/utils';
describe('autocompleteDataSources', () => {
beforeEach(() => {
@@ -25,3 +25,25 @@ describe('markdownPreviewPath', () => {
);
});
});
+
+describe('isReference', () => {
+ it.each`
+ referenceId | result
+ ${'#101'} | ${true}
+ ${'&101'} | ${true}
+ ${'101'} | ${false}
+ ${'#'} | ${false}
+ ${'&'} | ${false}
+ ${' &101'} | ${false}
+ ${'gitlab-org&101'} | ${true}
+ ${'gitlab-org/project-path#101'} | ${true}
+ ${'gitlab-org/sub-group/project-path#101'} | ${true}
+ ${'gitlab-org'} | ${false}
+ ${'gitlab-org101#'} | ${false}
+ ${'gitlab-org101&'} | ${false}
+ ${'#gitlab-org101'} | ${false}
+ ${'&gitlab-org101'} | ${false}
+ `('returns $result for $referenceId', ({ referenceId, result }) => {
+ expect(isReference(referenceId)).toEqual(result);
+ });
+});