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 40e9e0604bb55421cc6037f70456b8aefb21e9ac..37d3c95cd5894167b5a94045b87895e5c301cde7 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -326,12 +326,6 @@ export default { workItemWeight() { return this.isWidgetPresent(WIDGET_TYPE_WEIGHT); }, - showRolledUpWeight() { - return this.workItemWeight?.widgetDefinition?.rollUp; - }, - rolledUpWeight() { - return this.workItemWeight?.rolledUpWeight; - }, workItemBodyClass() { return { 'gl-pt-5': !this.updateError && !this.isModal, @@ -729,8 +723,6 @@ export default { :work-item-iid="workItemIid" :can-update="canUpdate" :can-update-children="canUpdateChildren" - :rolled-up-weight="rolledUpWeight" - :show-rolled-up-weight="showRolledUpWeight" :confidential="workItem.confidential" :allowed-child-types="allowedChildTypes" @show-modal="openInModal" diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_rolled_up_data.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_rolled_up_data.vue new file mode 100644 index 0000000000000000000000000000000000000000..ba2bdb73899b545b50a02837861484e0dc6c1fcc --- /dev/null +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_rolled_up_data.vue @@ -0,0 +1,125 @@ +<script> +import { GlIcon, GlTooltip, GlPopover } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; +import { findWidget } from '~/issues/list/utils'; +import { i18n, WIDGET_TYPE_WEIGHT } from '../../constants'; + +export default { + components: { + GlIcon, + GlTooltip, + GlPopover, + }, + i18n: { + progressLabel: s__('WorkItem|Progress'), + weightCompletedLabel: s__('WorkItem|issue weight completed'), + }, + props: { + fullPath: { + type: String, + required: true, + }, + workItemType: { + type: String, + required: true, + }, + workItemId: { + type: String, + required: true, + }, + workItemIid: { + type: String, + required: false, + default: null, + }, + }, + apollo: { + workItem: { + query: workItemByIidQuery, + variables() { + return { + fullPath: this.fullPath, + iid: this.workItemIid, + }; + }, + update(data) { + return data.workspace?.workItem || {}; + }, + skip() { + return !this.workItemIid; + }, + error(e) { + this.$emit('error', i18n.fetchError); + this.error = e.message || i18n.fetchError; + }, + }, + }, + computed: { + workItemWeight() { + return findWidget(WIDGET_TYPE_WEIGHT, this.workItem); + }, + shouldRolledUpWeightBeVisible() { + return this.showRolledUpWeight && this.rolledUpWeight !== null; + }, + showRolledUpProgress() { + return this.rolledUpWeight && this.rolledUpCompletedWeight !== null; + }, + showRolledUpWeight() { + return this.workItemWeight?.widgetDefinition?.rollUp; + }, + rolledUpWeight() { + return this.workItemWeight?.rolledUpWeight; + }, + rolledUpCompletedWeight() { + return this.workItemWeight?.rolledUpCompletedWeight; + }, + completedWeightPercentage() { + return Math.round((this.rolledUpCompletedWeight / this.rolledUpWeight) * 100); + }, + }, +}; +</script> + +<template> + <div class="gl-flex"> + <!-- Rolled up weight --> + <span + v-if="shouldRolledUpWeightBeVisible" + ref="weightData" + tabindex="0" + data-testid="work-item-rollup-weight" + class="gl-ml-3 gl-flex gl-cursor-help gl-items-center gl-gap-2 gl-font-normal gl-text-secondary" + > + <gl-icon name="weight" class="gl-text-secondary" /> + <span data-testid="work-item-weight-value" class="gl-text-sm">{{ rolledUpWeight }}</span> + <gl-tooltip :target="() => $refs.weightData"> + <span class="gl-font-bold"> + {{ __('Weight') }} + </span> + </gl-tooltip> + </span> + <!--- END Rolled up weight --> + + <!-- Rolled up Progress --> + <span + v-if="showRolledUpProgress" + ref="progressBadge" + tabindex="0" + data-testid="work-item-rollup-progress" + class="gl-ml-3 gl-flex gl-items-center gl-gap-2 gl-font-normal gl-text-secondary" + > + <gl-icon name="progress" class="gl-text-secondary" /> + <span data-testid="work-item-progress-value" class="gl-text-sm" + >{{ completedWeightPercentage }}%</span + > + + <gl-popover triggers="hover focus" :target="() => $refs.progressBadge"> + <template #title>{{ $options.i18n.progressLabel }}</template> + <span class="gl-font-bold">{{ rolledUpCompletedWeight }}/{{ rolledUpWeight }}</span> + {{ $options.i18n.weightCompletedLabel }} + </gl-popover> + </span> + <!-- END Rolled up Progress --> + </div> +</template> diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue index ffa5962f100f5693c7c3cc57dd4401df3ba43fd5..72bc91de4795b6c0c37477f78ac57c8e567152e4 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue @@ -1,5 +1,5 @@ <script> -import { GlAlert, GlIcon, GlTooltip } from '@gitlab/ui'; +import { GlAlert } from '@gitlab/ui'; import { sprintf, s__ } from '~/locale'; import { createAlert } from '~/alert'; import CrudComponent from '~/vue_shared/components/crud_component.vue'; @@ -20,6 +20,7 @@ import WorkItemMoreActions from '../shared/work_item_more_actions.vue'; import WorkItemActionsSplitButton from './work_item_actions_split_button.vue'; import WorkItemLinksForm from './work_item_links_form.vue'; import WorkItemChildrenWrapper from './work_item_children_wrapper.vue'; +import WorkItemRolledUpData from './work_item_rolled_up_data.vue'; export default { FORM_TYPES, @@ -28,14 +29,13 @@ export default { WORK_ITEM_TYPE_ENUM_KEY_RESULT, components: { GlAlert, - GlIcon, - GlTooltip, WorkItemActionsSplitButton, CrudComponent, WorkItemLinksForm, WorkItemChildrenWrapper, WorkItemChildrenLoadMore, WorkItemMoreActions, + WorkItemRolledUpData, }, inject: ['hasSubepicsFeature'], props: { @@ -86,16 +86,6 @@ export default { required: false, default: () => [], }, - showRolledUpWeight: { - type: Boolean, - required: false, - default: false, - }, - rolledUpWeight: { - type: Number, - required: false, - default: 0, - }, }, data() { return { @@ -256,20 +246,12 @@ export default { data-testid="work-item-tree" > <template #count> - <span - v-if="shouldRolledUpWeightBeVisible" - ref="weightData" - data-testid="rollup-weight" - class="gl-ml-3 gl-flex gl-cursor-help gl-items-center gl-gap-2 gl-font-normal gl-text-secondary" - > - <gl-icon name="weight" class="gl-text-subtle" /> - <span data-testid="weight-value" class="gl-text-sm">{{ rolledUpWeight }}</span> - <gl-tooltip :target="() => $refs.weightData"> - <span class="gl-font-bold"> - {{ __('Weight') }} - </span> - </gl-tooltip> - </span> + <work-item-rolled-up-data + :work-item-id="workItemId" + :work-item-iid="workItemIid" + :work-item-type="workItemType" + :full-path="fullPath" + /> </template> <template #actions> diff --git a/app/assets/javascripts/work_items/graphql/cache_utils.js b/app/assets/javascripts/work_items/graphql/cache_utils.js index 44f9fbb58899baff75c0944da5e268f192a8c208..31cd9e1b052e33a9d51fd4e637011dc15092473c 100644 --- a/app/assets/javascripts/work_items/graphql/cache_utils.js +++ b/app/assets/javascripts/work_items/graphql/cache_utils.js @@ -380,6 +380,7 @@ export const setNewWorkItemCache = async ( type: 'WEIGHT', weight: null, rolledUpWeight: 0, + rolledUpCompletedWeight: 0, widgetDefinition: { editable: weightWidgetData?.editable, rollUp: weightWidgetData?.rollUp, 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 7420f6de8ae0478a76bf437ddb5a8e17bc34df99..31c1c34d388229336d97c31027edeac4f960842d 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 @@ -53,6 +53,7 @@ fragment WorkItemWidgets on WorkItemWidget { ... on WorkItemWidgetWeight { weight rolledUpWeight + rolledUpCompletedWeight widgetDefinition { editable rollUp diff --git a/ee/spec/frontend/work_items/components/work_item_links/work_item_rolled_up_data_spec.js b/ee/spec/frontend/work_items/components/work_item_links/work_item_rolled_up_data_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..588b6553f31f45c4ec4b4b85f1773a2e565617cb --- /dev/null +++ b/ee/spec/frontend/work_items/components/work_item_links/work_item_rolled_up_data_spec.js @@ -0,0 +1,124 @@ +import Vue from 'vue'; +import { GlIcon } from '@gitlab/ui'; +import VueApollo from 'vue-apollo'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import WorkItemRolledUpData from '~/work_items/components/work_item_links/work_item_rolled_up_data.vue'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; +import { workItemByIidResponseFactory } from 'jest/work_items/mock_data'; + +Vue.use(VueApollo); + +describe('WorkItemRollUpData', () => { + let wrapper; + + const findRolledUpWeight = () => wrapper.findByTestId('work-item-rollup-weight'); + const findRolledUpWeightValue = () => wrapper.findByTestId('work-item-weight-value'); + const findRolledUpProgress = () => wrapper.findByTestId('work-item-rollup-progress'); + const findRolledUpProgressValue = () => wrapper.findByTestId('work-item-progress-value'); + + const workItemQueryResponse = workItemByIidResponseFactory({ + canUpdate: true, + canDelete: true, + }); + const workItemSuccessQueryHandler = jest.fn().mockResolvedValue(workItemQueryResponse); + + const createComponent = ({ + workItemType = 'Objective', + workItemIid = '2', + workItemQueryHandler = workItemSuccessQueryHandler, + } = {}) => { + wrapper = shallowMountExtended(WorkItemRolledUpData, { + propsData: { + fullPath: 'test/project', + workItemType, + workItemIid, + workItemId: 'gid://gitlab/WorkItem/2', + }, + apolloProvider: createMockApollo([[workItemByIidQuery, workItemQueryHandler]]), + }); + }; + + describe('rolled up weight', () => { + it.each` + isRollUp | rolledUpWeight | rollUpWeightVisible | expected + ${false} | ${0} | ${false} | ${'rollup weight is not displayed'} + ${false} | ${10} | ${false} | ${'rollup weight is not displayed'} + ${true} | ${0} | ${true} | ${'rollup weight is displayed'} + ${true} | ${null} | ${false} | ${'rollup weight is not displayed'} + ${true} | ${10} | ${true} | ${'rollup weight is displayed'} + `( + 'When the roll up weight is $isRollUp and rolledUpWeight is $rolledUpWeight, $expected', + async ({ isRollUp, rollUpWeightVisible, rolledUpWeight }) => { + const workItemResponse = workItemByIidResponseFactory({ + canUpdate: true, + rolledUpWeight, + editableWeightWidget: !isRollUp, + }); + createComponent({ + workItemQueryHandler: jest.fn().mockResolvedValue(workItemResponse), + }); + + await waitForPromises(); + + expect(findRolledUpWeight().exists()).toBe(rollUpWeightVisible); + }, + ); + + it('should show the correct value when rolledUpWeight is visible', async () => { + const workItemResponse = workItemByIidResponseFactory({ + canUpdate: true, + rolledUpWeight: 10, + editableWeightWidget: false, + }); + createComponent({ workItemQueryHandler: jest.fn().mockResolvedValue(workItemResponse) }); + + await waitForPromises(); + + expect(findRolledUpWeight().exists()).toBe(true); + expect(findRolledUpWeight().findComponent(GlIcon).props('name')).toBe('weight'); + expect(findRolledUpWeightValue().text()).toBe('10'); + }); + }); + + describe('rolled up progress', () => { + it('should not show the rolled up progress when rolled up weight is null', async () => { + const workItemResponse = workItemByIidResponseFactory({ + canUpdate: true, + rolledUpWeight: null, + editableWeightWidget: false, + }); + createComponent({ workItemQueryHandler: jest.fn().mockResolvedValue(workItemResponse) }); + + await waitForPromises(); + + expect(findRolledUpProgress().exists()).toBe(false); + expect(findRolledUpProgressValue().exists()).toBe(false); + }); + + it('should show the correct value when rolledUpWeight and rolledUpCompletedWeight exist', async () => { + const workItemResponse = workItemByIidResponseFactory({ + canUpdate: true, + rolledUpWeight: 12, + rolledUpCompletedWeight: 5, + editableWeightWidget: false, + }); + createComponent({ workItemQueryHandler: jest.fn().mockResolvedValue(workItemResponse) }); + + await waitForPromises(); + + expect(findRolledUpProgress().exists()).toBe(true); + expect(findRolledUpProgress().findComponent(GlIcon).props('name')).toBe('progress'); + expect(findRolledUpProgressValue().text()).toBe('42%'); + }); + }); + + it('when the query is not successful , and error is emitted', async () => { + const errorHandler = jest.fn().mockRejectedValue('Oops'); + createComponent({ workItemQueryHandler: errorHandler }); + await waitForPromises(); + + expect(wrapper.emitted('error')).toHaveLength(1); + }); +}); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 754f9daa8336b79c5070e447549878f39a50a7a4..6027d1b9e1cc24d7d6e0d4b9d77e71cf5ba704fa 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -61444,6 +61444,9 @@ msgstr "" msgid "WorkItem|Pink" msgstr "" +msgid "WorkItem|Progress" +msgstr "" + msgid "WorkItem|Promoted to objective." msgstr "" @@ -61660,6 +61663,9 @@ msgstr "" msgid "WorkItem|is blocked by" msgstr "" +msgid "WorkItem|issue weight completed" +msgstr "" + msgid "WorkItem|item" msgstr "" diff --git a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js index 5438446c496008a0f286564f5ec298bd90e8dcc5..e560812855d6f422a03d5288e8ef20457d6a2d3e 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js @@ -1,6 +1,6 @@ import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; -import { GlAlert, GlIcon, GlLoadingIcon } from '@gitlab/ui'; +import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; @@ -46,9 +46,6 @@ describe('WorkItemTree', () => { const findErrorMessage = () => wrapper.findComponent(GlAlert); const findWorkItemLinkChildrenWrapper = () => wrapper.findComponent(WorkItemChildrenWrapper); const findMoreActions = () => wrapper.findComponent(WorkItemMoreActions); - const findRolledUpWeight = () => wrapper.findByTestId('rollup-weight'); - const findRolledUpWeightValue = () => wrapper.findByTestId('weight-value'); - const findCrudComponent = () => wrapper.findComponent(CrudComponent); const createComponent = async ({ workItemType = 'Objective', @@ -59,8 +56,6 @@ describe('WorkItemTree', () => { canUpdateChildren = true, hasSubepicsFeature = true, workItemHierarchyTreeHandler = workItemHierarchyTreeResponseHandler, - showRolledUpWeight = false, - rolledUpWeight = 0, } = {}) => { wrapper = shallowMountExtended(WorkItemTree, { propsData: { @@ -72,8 +67,6 @@ describe('WorkItemTree', () => { confidential, canUpdate, canUpdateChildren, - showRolledUpWeight, - rolledUpWeight, }, apolloProvider: createMockApollo([[getWorkItemTreeQuery, workItemHierarchyTreeHandler]]), provide: { @@ -302,39 +295,4 @@ describe('WorkItemTree', () => { expect(findWorkItemLinkChildrenWrapper().props('showLabels')).toBe(true); }); }); - - describe('rollup data', () => { - describe('rolledUp weight', () => { - it.each` - showRolledUpWeight | rolledUpWeight | rollUpWeightVisible | expected - ${false} | ${0} | ${false} | ${'rollup weight is not displayed'} - ${false} | ${10} | ${false} | ${'rollup weight is not displayed'} - ${true} | ${0} | ${true} | ${'rollup weight is displayed'} - ${true} | ${null} | ${false} | ${'rollup weight is not displayed'} - ${true} | ${10} | ${true} | ${'rollup weight is displayed'} - `( - 'When showRolledUpWeight is $showRolledUpWeight and rolledUpWeight is $rolledUpWeight, $expected', - ({ showRolledUpWeight, rollUpWeightVisible, rolledUpWeight }) => { - createComponent({ showRolledUpWeight, rolledUpWeight }); - - expect(findRolledUpWeight().exists()).toBe(rollUpWeightVisible); - }, - ); - - it('should show the correct value when rolledUpWeight is visible', async () => { - await createComponent({ showRolledUpWeight: true, rolledUpWeight: 10 }); - - expect(findRolledUpWeight().exists()).toBe(true); - expect(findRolledUpWeight().findComponent(GlIcon).props('name')).toBe('weight'); - expect(findRolledUpWeightValue().text()).toBe('10'); - }); - }); - }); - - it('renders children count', async () => { - await createComponent({ showRolledUpWeight: true }); - - expect(findCrudComponent().props('icon')).toBe('issue-type-task'); - expect(findCrudComponent().props('count')).toBe(1); - }); }); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index d8bdab12f0e90dfda54916b17fbde3c794c94da8..208477a876bb62f52734dfddbbc7d473c292f8a1 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -1069,6 +1069,8 @@ export const workItemResponseFactory = ({ editableWeightWidget = true, hasParent = false, healthStatus = 'onTrack', + rolledUpWeight = 0, + rolledUpCompletedWeight = 0, } = {}) => ({ data: { workItem: { @@ -1161,7 +1163,8 @@ export const workItemResponseFactory = ({ ? { type: 'WEIGHT', weight: null, - rolledUpWeight: 0, + rolledUpWeight, + rolledUpCompletedWeight, widgetDefinition: { editable: editableWeightWidget, rollUp: !editableWeightWidget,