diff --git a/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue index 0a38dcb77f67d37fa7dd970a9f2c12c304b15e8e..b10a3727e9f638be8e4d743729aa632c8835774a 100644 --- a/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue +++ b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue @@ -43,15 +43,6 @@ export default { type: Boolean, required: true, }, - parentWorkItemId: { - type: String, - required: true, - }, - workItemType: { - type: String, - required: false, - default: '', - }, childPath: { type: String, required: true, diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue index 8146b66dc1fd1bb35f17507c0b77c39475abc461..6beca682f8f441ac13fb10380289faf3466c0003 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -31,6 +31,7 @@ import { WORK_ITEM_TYPE_VALUE_ISSUE, WORK_ITEM_TYPE_VALUE_OBJECTIVE, WIDGET_TYPE_NOTES, + WIDGET_TYPE_LINKED_ITEMS, } from '../constants'; import workItemUpdatedSubscription from '../graphql/work_item_updated.subscription.graphql'; @@ -50,6 +51,7 @@ import WorkItemNotes from './work_item_notes.vue'; import WorkItemDetailModal from './work_item_detail_modal.vue'; import WorkItemAwardEmoji from './work_item_award_emoji.vue'; import WorkItemStateToggleButton from './work_item_state_toggle_button.vue'; +import WorkItemRelationships from './work_item_relationships/work_item_relationships.vue'; export default { i18n, @@ -79,6 +81,7 @@ export default { AbuseCategorySelector, GlIntersectionObserver, ConfidentialityBadge, + WorkItemRelationships, }, mixins: [glFeatureFlagMixin()], inject: ['fullPath', 'reportAbusePath'], @@ -259,6 +262,15 @@ export default { showIntersectionObserver() { return !this.isModal && this.workItemsMvc2Enabled; }, + hasLinkedWorkItems() { + return this.glFeatures.linkedWorkItems; + }, + workItemLinkedItems() { + return this.isWidgetPresent(WIDGET_TYPE_LINKED_ITEMS); + }, + showWorkItemLinkedItems() { + return this.hasLinkedWorkItems && this.workItemLinkedItems; + }, }, mounted() { if (this.modalWorkItemIid) { @@ -591,6 +603,11 @@ export default { @show-modal="openInModal" @addChild="$emit('addChild')" /> + +import { GlButton } from '@gitlab/ui'; + +import { s__ } from '~/locale'; + +import WidgetWrapper from '../widget_wrapper.vue'; + +export default { + components: { + WidgetWrapper, + GlButton, + }, + props: { + workItemIid: { + type: String, + required: true, + }, + workItemFullpath: { + type: String, + required: true, + }, + }, + i18n: { + title: s__('WorkItem|Linked Items'), + emptyStateMessage: s__( + "WorkItem|Link work items together to show that they're related or that one is blocking others.", + ), + addLinkedWorkItemButtonLabel: s__('WorkItem|Add'), + }, +}; + + + + {{ $options.i18n.title }} + + + {{ $options.i18n.addLinkedWorkItemButtonLabel }} + + + + + + + {{ $options.i18n.emptyStateMessage }} + + + + + + diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js index 715c2ef843fad185fc65d4287c1360c6a46e18d6..c68f59abe00d257a0b10c9005078f470010b3081 100644 --- a/app/assets/javascripts/work_items/constants.js +++ b/app/assets/javascripts/work_items/constants.js @@ -26,6 +26,7 @@ export const WIDGET_TYPE_MILESTONE = 'MILESTONE'; export const WIDGET_TYPE_ITERATION = 'ITERATION'; export const WIDGET_TYPE_NOTES = 'NOTES'; export const WIDGET_TYPE_HEALTH_STATUS = 'HEALTH_STATUS'; +export const WIDGET_TYPE_LINKED_ITEMS = 'LINKED_ITEMS'; export const WORK_ITEM_TYPE_ENUM_INCIDENT = 'INCIDENT'; export const WORK_ITEM_TYPE_ENUM_ISSUE = 'ISSUE'; diff --git a/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql index 383d003e78ccfcfcb525965dad642dc88fffcfde..14cb6f8415c423bea0a6c34f12c8c1358524376c 100644 --- a/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql +++ b/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql @@ -100,4 +100,8 @@ fragment WorkItemWidgets on WorkItemWidget { ... on WorkItemWidgetAwardEmoji { type } + + ... on WorkItemWidgetLinkedItems { + type + } } diff --git a/app/assets/javascripts/work_items/utils.js b/app/assets/javascripts/work_items/utils.js index ac5d8b32fad976f240b3703e17d35fe5c35afaa5..1443e4b509dcaafd88fa80dca31ecacfd31459d8 100644 --- a/app/assets/javascripts/work_items/utils.js +++ b/app/assets/javascripts/work_items/utils.js @@ -1,3 +1,4 @@ +import { joinPaths } from '~/lib/utils/url_utility'; import { WIDGET_TYPE_ASSIGNEES, WIDGET_TYPE_HEALTH_STATUS, @@ -42,3 +43,7 @@ export const markdownPreviewPath = (fullPath, iid) => `${ gon.relative_url_root || '' }/${fullPath}/preview_markdown?target_type=WorkItem&target_id=${iid}`; + +export const workItemPath = (fullPath, workItemIid) => { + return joinPaths(gon?.relative_url_root || '/', fullPath, '-', 'work_items', workItemIid); +}; diff --git a/app/controllers/projects/incidents_controller.rb b/app/controllers/projects/incidents_controller.rb index 6109e29b16981b4964c4bf488c0d530ecae14094..69d349b1f1d53e2ebdc649a4cbe125ef50c66984 100644 --- a/app/controllers/projects/incidents_controller.rb +++ b/app/controllers/projects/incidents_controller.rb @@ -12,6 +12,7 @@ class Projects::IncidentsController < Projects::ApplicationController push_force_frontend_feature_flag(:work_items_mvc_2, @project&.work_items_mvc_2_feature_flag_enabled?) push_frontend_feature_flag(:moved_mr_sidebar, project) push_frontend_feature_flag(:move_close_into_dropdown, project) + push_force_frontend_feature_flag(:linked_work_items, @project&.linked_work_items_feature_flag_enabled?) end feature_category :incident_management diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 159e839cfec83ae3a48dda8d9fd1f74ba8473d7b..86e914f3447ca3f9727bbe869325491c5edfb2fd 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -72,6 +72,7 @@ class Projects::IssuesController < Projects::ApplicationController push_frontend_feature_flag(:epic_widget_edit_confirmation, project) push_frontend_feature_flag(:moved_mr_sidebar, project) push_frontend_feature_flag(:move_close_into_dropdown, project) + push_force_frontend_feature_flag(:linked_work_items, project.linked_work_items_feature_flag_enabled?) end around_action :allow_gitaly_ref_name_caching, only: [:discussions] diff --git a/app/controllers/projects/work_items_controller.rb b/app/controllers/projects/work_items_controller.rb index 7da31c199a12b960f1bef622321eddb2e91de9bf..c3986be31b0a6d0b17f6ebeb421d98a8e85b0338 100644 --- a/app/controllers/projects/work_items_controller.rb +++ b/app/controllers/projects/work_items_controller.rb @@ -12,6 +12,7 @@ class Projects::WorkItemsController < Projects::ApplicationController push_force_frontend_feature_flag(:work_items_mvc, project&.work_items_mvc_feature_flag_enabled?) push_force_frontend_feature_flag(:work_items_mvc_2, project&.work_items_mvc_2_feature_flag_enabled?) push_force_frontend_feature_flag(:saved_replies, current_user) + push_force_frontend_feature_flag(:linked_work_items, project&.linked_work_items_feature_flag_enabled?) end feature_category :team_planning diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 2ad0f11dc91f7ea2221cfee5522ea6889ceaf917..6a246219f7d394a7f733c78034d5962f93b4961c 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -46,6 +46,7 @@ class ProjectsController < Projects::ApplicationController push_force_frontend_feature_flag(:work_items, @project&.work_items_feature_flag_enabled?) push_force_frontend_feature_flag(:work_items_mvc, @project&.work_items_mvc_feature_flag_enabled?) push_force_frontend_feature_flag(:work_items_mvc_2, @project&.work_items_mvc_2_feature_flag_enabled?) + push_force_frontend_feature_flag(:linked_work_items, @project&.linked_work_items_feature_flag_enabled?) end layout :determine_layout diff --git a/ee/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql b/ee/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql index 5b7aa8e652e454b8bcbaaaf6cd43b91107b9e4fd..0c7e85131e9a185d54c0a160a0d015bf49e0b17f 100644 --- a/ee/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql +++ b/ee/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql @@ -1,7 +1,6 @@ #import "~/graphql_shared/fragments/label.fragment.graphql" #import "~/graphql_shared/fragments/user.fragment.graphql" #import "~/work_items/graphql/milestone.fragment.graphql" - #import "./work_item_metadata_widgets.fragment.graphql" fragment WorkItemWidgets on WorkItemWidget { @@ -126,4 +125,8 @@ fragment WorkItemWidgets on WorkItemWidget { ... on WorkItemWidgetAwardEmoji { type } + + ... on WorkItemWidgetLinkedItems { + type + } } diff --git a/ee/spec/features/projects/work_items/okr_spec.rb b/ee/spec/features/projects/work_items/okr_spec.rb index 724badfb7750c43008ebe6a49127e7c5ee5706e4..00f36891b8ec1e1693e47533116234ed9933d9b4 100644 --- a/ee/spec/features/projects/work_items/okr_spec.rb +++ b/ee/spec/features/projects/work_items/okr_spec.rb @@ -134,15 +134,15 @@ it 'toggles widget body', :aggregate_failures do page.within('[data-testid="work-item-tree"]') do - expect(page).to have_selector('[data-testid="widget-body"]') + expect(page).to have_selector('[data-testid="work-item-tree"] [data-testid="widget-body"]') click_button 'Collapse' - expect(page).not_to have_selector('[data-testid="widget-body"]') + expect(page).not_to have_selector('[data-testid="work-item-tree"] [data-testid="widget-body"]') click_button 'Expand' - expect(page).to have_selector('[data-testid="widget-body"]') + expect(page).to have_selector('[data-testid="work-item-tree"] [data-testid="widget-body"]') end end @@ -249,7 +249,7 @@ close_rich_text_promo_popover_if_present wait_for_all_requests - page.within('[data-testid="widget-body"]') do + page.within('[data-testid="work-item-tree"] [data-testid="widget-body"]') do click_button 'Expand' wait_for_all_requests diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f796e1933c7e8bda74697af8541e40b5a36ffdcb..17c6337db0089b3f163af71cf1de30a4d54cc194 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -53609,6 +53609,12 @@ msgstr "" msgid "WorkItem|Key result" msgstr "" +msgid "WorkItem|Link work items together to show that they're related or that one is blocking others." +msgstr "" + +msgid "WorkItem|Linked Items" +msgstr "" + msgid "WorkItem|Mark as done" msgstr "" diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js index 803ff950cbe554cce3c34ace77147605c9ae196a..a624bbe85676eb104fc5295bf71733cfa1420d8c 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js @@ -93,8 +93,6 @@ describe('WorkItemLinkChild', () => { expect(findWorkItemLinkChildContents().props()).toEqual({ childItem: workItemObjectiveWithChild, canUpdate: true, - parentWorkItemId: 'gid://gitlab/WorkItem/2', - workItemType: 'Objective', childPath: '/gitlab-org/gitlab-test/-/work_items/12', }); }); diff --git a/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js b/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..18d13aa251e762cafa16e6394c435ef6aa3151a8 --- /dev/null +++ b/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js @@ -0,0 +1,25 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import WorkItemRelationships from '~/work_items/components/work_item_relationships/work_item_relationships.vue'; + +describe('WorkItemRelationships', () => { + let wrapper; + + const createComponent = async () => { + wrapper = shallowMountExtended(WorkItemRelationships, { + propsData: { + workItem: {}, + workItemIid: '1', + workItemFullpath: 'gitlab/path', + }, + }); + + await waitForPromises(); + }; + + it('renders the component', () => { + createComponent(); + + expect(wrapper.find('.work-item-relationships').exists()).toBe(true); + }); +}); diff --git a/spec/frontend/work_items/utils_spec.js b/spec/frontend/work_items/utils_spec.js index aa24b80cf08037828ec944a25d331955edcdf472..8a49140119d16d567b66bebff599cbe6b8a5044e 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, workItemPath } from '~/work_items/utils'; describe('autocompleteDataSources', () => { beforeEach(() => { @@ -25,3 +25,14 @@ describe('markdownPreviewPath', () => { ); }); }); + +describe('workItemPath', () => { + it('returns corrrect data sources', () => { + expect(workItemPath('project/group', '2')).toEqual('/project/group/-/work_items/2'); + }); + + it('returns corrrect data sources with relative url root', () => { + gon.relative_url_root = '/foobar'; + expect(workItemPath('project/group', '2')).toEqual('/foobar/project/group/-/work_items/2'); + }); +});
+ {{ $options.i18n.emptyStateMessage }} +