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 @@ + 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); + }); +});