diff --git a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue index d198c834a8208b67dc98e21afde5645aca922db0..f7c96699f92c0e59b35cc17e0d55454727fa0c47 100644 --- a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue +++ b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue @@ -52,6 +52,11 @@ export default { type: Object, required: true, }, + groupPath: { + type: String, + required: false, + default: '', + }, }, computed: { workItemType() { @@ -247,6 +252,7 @@ export default { :work-item-id="workItem.id" :work-item-type="workItemType" :parent="workItemParent" + :group-path="groupPath" @error="$emit('error', $event)" /> 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 b957b16f8882cc3ccf3b5045d04a13ec33638910..f13bcb5cd07580cf592ea238138f910781de10b8 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -78,7 +78,7 @@ export default { WorkItemLoading, }, mixins: [glFeatureFlagMixin()], - inject: ['fullPath', 'isGroup', 'reportAbusePath'], + inject: ['fullPath', 'isGroup', 'reportAbusePath', 'groupPath'], props: { isModal: { type: Boolean, @@ -608,6 +608,7 @@ export default { 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 0357ff752f2a8ad863d067b04d7d9d31f1c2ee37..36debe26ae98888db9c58a461fe6b2d3d6400764 100644 --- a/app/assets/javascripts/work_items/components/work_item_parent.vue +++ b/app/assets/javascripts/work_items/components/work_item_parent.vue @@ -14,6 +14,7 @@ import { I18N_WORK_ITEM_ERROR_UPDATING, sprintfWorkItem, SUPPORTED_PARENT_TYPE_MAP, + WORK_ITEM_TYPE_VALUE_ISSUE, } from '../constants'; export default { @@ -57,6 +58,11 @@ export default { required: false, default: false, }, + groupPath: { + type: String, + required: false, + default: '', + }, }, data() { return { @@ -73,6 +79,9 @@ export default { hasParent() { return this.parent !== null; }, + isIssue() { + return this.workItemType === WORK_ITEM_TYPE_VALUE_ISSUE; + }, isLoading() { return this.$apollo.queries.availableWorkItems.loading; }, @@ -105,16 +114,19 @@ export default { apollo: { availableWorkItems: { query() { - return this.isGroup ? groupWorkItemsQuery : projectWorkItemsQuery; + // TODO: Remove the this.isIssue check once issues are migrated to work items + return this.isGroup || this.isIssue ? groupWorkItemsQuery : projectWorkItemsQuery; }, variables() { + // TODO: Remove the this.isIssue check once issues are migrated to work items return { - fullPath: this.fullPath, + fullPath: this.isIssue ? this.groupPath : this.fullPath, searchTerm: this.search, types: this.parentType, in: this.search ? 'TITLE' : undefined, iid: null, isNumber: false, + includeAncestors: true, }; }, skip() { diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js index 3c9feed4f7ebf69ec7f3ef821ff61a67d3b416d1..5179d9159864439fd280a1121a4d38fe9dcba7f1 100644 --- a/app/assets/javascripts/work_items/constants.js +++ b/app/assets/javascripts/work_items/constants.js @@ -322,6 +322,7 @@ export const SUPPORTED_PARENT_TYPE_MAP = { [WORK_ITEM_TYPE_VALUE_KEY_RESULT]: [WORK_ITEM_TYPE_ENUM_OBJECTIVE], [WORK_ITEM_TYPE_VALUE_TASK]: [WORK_ITEM_TYPE_ENUM_ISSUE], [WORK_ITEM_TYPE_VALUE_EPIC]: [WORK_ITEM_TYPE_ENUM_EPIC], + [WORK_ITEM_TYPE_VALUE_ISSUE]: [WORK_ITEM_TYPE_ENUM_EPIC], }; export const LINKED_ITEMS_ANCHOR = 'linkeditems'; 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 bfee0452acd9a79028cfbcd05b93e19969c64385..567a8db46d75449ab168c02529d419da15b676ab 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 @@ -3,10 +3,11 @@ query groupWorkItems( $fullPath: ID! $types: [IssueType!] $in: [IssuableSearchableField!] + $includeAncestors: Boolean = false ) { workspace: group(fullPath: $fullPath) { id - workItems(search: $searchTerm, types: $types, in: $in) { + workItems(search: $searchTerm, types: $types, in: $in, includeAncestors: $includeAncestors) { nodes { id iid diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js index f803831ddf7151679889ca5631c0abd64a1bc7d2..d6b4c9ab0c1461fd956f2fb4473090b39abdce9a 100644 --- a/app/assets/javascripts/work_items/index.js +++ b/app/assets/javascripts/work_items/index.js @@ -25,6 +25,7 @@ export const initWorkItemsRoot = ({ workItemType, workspaceType } = {}) => { const { fullPath, + groupPath, hasIssueWeightsFeature, iid, issuesListPath, @@ -57,6 +58,7 @@ export const initWorkItemsRoot = ({ workItemType, workspaceType } = {}) => { hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature), newCommentTemplatePaths: JSON.parse(newCommentTemplatePaths), reportAbusePath, + groupPath, }, mounted() { performanceMarkAndMeasure({ diff --git a/app/helpers/work_items_helper.rb b/app/helpers/work_items_helper.rb index a1364c312f9c07caf2858dd37800a9ec5c73d785..492633d78669737b25ac1cb941b2dd6ab7060c2e 100644 --- a/app/helpers/work_items_helper.rb +++ b/app/helpers/work_items_helper.rb @@ -6,6 +6,7 @@ def work_items_show_data(resource_parent) { full_path: resource_parent.full_path, + group_path: group&.full_path, issues_list_path: resource_parent.is_a?(Group) ? issues_group_path(resource_parent) : project_issues_path(resource_parent), register_path: new_user_registration_path(redirect_to_referer: 'yes'), diff --git a/doc/architecture/blueprints/work_items/index.md b/doc/architecture/blueprints/work_items/index.md index 85f000a2caa2290839d729503c95f3c4a421f9af..956779821bad3a9984ce4b608189115edfa98869 100644 --- a/doc/architecture/blueprints/work_items/index.md +++ b/doc/architecture/blueprints/work_items/index.md @@ -137,6 +137,16 @@ Parent-child relationships form the basis of **hierarchy** in work items. Each w As types expand, and parent items have their own parent items, the hierarchy capability can grow exponentially. +Currently, following are the allowed Parent-child relationships: + +| Type | Can be parent of | Can be child of | +|------------|------------------|------------------| +| Epic | Epic | Epic | +| Issue | Task | Epic | +| Task | None | Issue | +| Objective | Objective | Objective | +| Key result | None | Objective | + ### Work Item view The new frontend view that renders Work Items of any type using global Work Item `id` as an identifier. diff --git a/ee/app/assets/javascripts/sidebar/queries/group_epics.query.graphql b/ee/app/assets/javascripts/sidebar/queries/group_epics.query.graphql index 4e89da3ac79c22f1a76b9ba4b7437ab569cb0157..2f3e2a24cebd731244f97106171beb5a58c8aee2 100644 --- a/ee/app/assets/javascripts/sidebar/queries/group_epics.query.graphql +++ b/ee/app/assets/javascripts/sidebar/queries/group_epics.query.graphql @@ -24,7 +24,8 @@ query issueEpics( state } } - workItems(types: $types) @include(if: $includeWorkItems) { + workItems(types: $types, includeAncestors: true, includeDescendants: false) + @include(if: $includeWorkItems) { nodes { id title diff --git a/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js b/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js index 17a464b90a749d1358e09777b50066af3ac014e6..bfb2156b4a172515df869a6a8a5b81a67cb32909 100644 --- a/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js +++ b/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js @@ -9,14 +9,7 @@ import WorkItemParent from '~/work_items/components/work_item_parent.vue'; import WorkItemTimeTracking from '~/work_items/components/work_item_time_tracking.vue'; import waitForPromises from 'helpers/wait_for_promises'; import WorkItemAttributesWrapper from '~/work_items/components/work_item_attributes_wrapper.vue'; -import { - workItemResponseFactory, - taskType, - objectiveType, - keyResultType, - issueType, - epicType, -} from '../mock_data'; +import { workItemResponseFactory } from '../mock_data'; describe('WorkItemAttributesWrapper component', () => { let wrapper; @@ -34,11 +27,13 @@ describe('WorkItemAttributesWrapper component', () => { const createComponent = ({ workItem = workItemQueryResponse.data.workItem, workItemsBeta = true, + groupPath = '', } = {}) => { wrapper = shallowMount(WorkItemAttributesWrapper, { propsData: { fullPath: 'group/project', workItem, + groupPath, }, provide: { hasIssueWeightsFeature: true, @@ -140,26 +135,9 @@ describe('WorkItemAttributesWrapper component', () => { }); describe('parent widget', () => { - describe.each` - description | workItemType | exists - ${'when work item type is task'} | ${taskType} | ${true} - ${'when work item type is objective'} | ${objectiveType} | ${true} - ${'when work item type is key result'} | ${keyResultType} | ${true} - ${'when work item type is issue'} | ${issueType} | ${true} - ${'when work item type is epic'} | ${epicType} | ${true} - `('$description', ({ workItemType, exists }) => { - it(`${exists ? 'renders' : 'does not render'} parent component`, async () => { - const response = workItemResponseFactory({ workItemType }); - createComponent({ workItem: response.data.workItem }); - - await waitForPromises(); - - expect(findWorkItemParent().exists()).toBe(exists); - }); - }); - - it('renders WorkItemParent when workItemsBeta enabled', async () => { - createComponent(); + it(`renders parent component with proper data`, async () => { + const response = workItemResponseFactory(); + createComponent({ workItem: response.data.workItem }); await waitForPromises(); diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js index ea27d75853ebc7e34cf1e5907ba45e873965be0b..931698c6bc48b12b8d7a9360e16ed94ff0d65c48 100644 --- a/spec/frontend/work_items/components/work_item_detail_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_spec.js @@ -136,6 +136,7 @@ describe('WorkItemDetail component', () => { hasIssuableHealthStatusFeature: true, projectNamespace: 'namespace', fullPath: 'group/project', + groupPath: 'group', isGroup, reportAbusePath: '/report/abuse/path', }, 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 3462b3db75b97dc18d44d985959ea6eb984e63da..7a360c060db285af51a086368ebe6472071780ba 100644 --- a/spec/frontend/work_items/components/work_item_parent_spec.js +++ b/spec/frontend/work_items/components/work_item_parent_spec.js @@ -292,6 +292,7 @@ describe('WorkItemParent component', () => { isNumber: false, searchByIid: false, searchByText: true, + includeAncestors: true, }); await findCollapsibleListbox().vm.$emit('search', 'Objective 101'); @@ -305,6 +306,7 @@ describe('WorkItemParent component', () => { isNumber: false, searchByIid: false, searchByText: true, + includeAncestors: true, }); await nextTick(); diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js index c44e72323b0d2f538d0fc8e6fae6de4d7be95896..475497f95bc8fe68f23b93d6a6d3c8c0caba601d 100644 --- a/spec/frontend/work_items/router_spec.js +++ b/spec/frontend/work_items/router_spec.js @@ -41,6 +41,7 @@ describe('Work items router', () => { router, provide: { fullPath: 'full-path', + groupPath: '', isGroup: false, issuesListPath: 'full-path/-/issues', hasIssueWeightsFeature: false, diff --git a/spec/helpers/work_items_helper_spec.rb b/spec/helpers/work_items_helper_spec.rb index 587faa8dfca57677e1fc7bd61465998e8cbb6c75..768b9742fec2e2ce264cfd22a7c818ea1bf32372 100644 --- a/spec/helpers/work_items_helper_spec.rb +++ b/spec/helpers/work_items_helper_spec.rb @@ -3,6 +3,7 @@ require "spec_helper" RSpec.describe WorkItemsHelper, feature_category: :team_planning do + include Devise::Test::ControllerHelpers describe '#work_items_show_data' do subject(:work_items_show_data) { helper.work_items_show_data(project) } @@ -12,6 +13,7 @@ expect(work_items_show_data).to include( { full_path: project.full_path, + group_path: nil, issues_list_path: project_issues_path(project), register_path: new_user_registration_path(redirect_to_referer: 'yes'), sign_in_path: user_session_path(redirect_to_referer: 'yes'), @@ -21,6 +23,21 @@ } ) end + + context 'when project is under a group' do + let(:group) { build(:group) } + let(:group_project) { build(:project, group: group) } + + subject(:work_items_show_data) { helper.work_items_show_data(group_project) } + + it 'returns the expected group_path' do + expect(work_items_show_data).to include( + { + group_path: group_project.group.full_path + } + ) + end + end end describe '#work_items_list_data' do