diff --git a/app/assets/javascripts/issues/list/components/issue_card_time_info.vue b/app/assets/javascripts/issues/list/components/issue_card_time_info.vue index 67f72d199779a30d3ea0829581f98ad07fc3e26e..12beeebd08098cb7ef345c56842507653ea1f8b4 100644 --- a/app/assets/javascripts/issues/list/components/issue_card_time_info.vue +++ b/app/assets/javascripts/issues/list/components/issue_card_time_info.vue @@ -19,6 +19,11 @@ export default { type: Object, required: true, }, + isWorkItemList: { + type: Boolean, + required: false, + default: false, + }, }, computed: { milestone() { diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue index 2bc692d7b630282e04fb01c670bbfdb359447829..44cc5a56bd1a53e2849ba6746bfa010c68a52089 100644 --- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue +++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue @@ -538,12 +538,17 @@ export default { /> </ul> <div - v-gl-tooltip.bottom - class="gl-hidden gl-text-subtle sm:gl-inline-block" - :title="tooltipTitle(timestamp)" - data-testid="issuable-timestamp" + class="gl-hidden sm:gl-flex sm:gl-flex-col sm:gl-items-end md:gl-flex-row md:gl-items-center" > - {{ formattedTimestamp }} + <slot name="health-status"></slot> + <div + v-gl-tooltip.bottom + class="gl-text-subtle sm:gl-inline-block" + :title="tooltipTitle(timestamp)" + data-testid="issuable-timestamp" + > + {{ formattedTimestamp }} + </div> </div> </div> </li> diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue index f1d64d8f59541c16c05ef5cdedd4f58055ac03df..a5609a8d0273c9338dd0821469d1dc3ae8b965ec 100644 --- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue +++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue @@ -438,6 +438,9 @@ export default { <template #discussions> <slot name="discussions" :issuable="issuable"></slot> </template> + <template #health-status> + <slot name="health-status" :issuable="issuable"></slot> + </template> </issuable-item> </component> <div v-else-if="issuables.length > 0 && isGridView"> diff --git a/app/assets/javascripts/work_items/components/work_item_health_status.vue b/app/assets/javascripts/work_items/components/work_item_health_status.vue new file mode 100644 index 0000000000000000000000000000000000000000..5bb56a7f710040ce32a4b2107f20b9878f5c85bd --- /dev/null +++ b/app/assets/javascripts/work_items/components/work_item_health_status.vue @@ -0,0 +1,43 @@ +<script> +import { isHealthStatusWidget } from '~/work_items/utils'; + +export default { + components: { + IssueHealthStatus: () => + import('ee_component/related_items_tree/components/issue_health_status.vue'), + }, + inject: ['hasIssuableHealthStatusFeature'], + props: { + issue: { + type: Object, + required: true, + }, + }, + computed: { + healthStatus() { + return ( + this.issue.healthStatus || this.issue.widgets?.find(isHealthStatusWidget)?.healthStatus + ); + }, + hasUpdateTimeStamp() { + return this.issue.updatedAt !== this.issue.createdAt; + }, + showHealthStatus() { + return this.hasIssuableHealthStatusFeature && this.healthStatus; + }, + }, +}; +</script> + +<template> + <issue-health-status + v-if="showHealthStatus" + class="gl-text-nowrap" + display-as-text + text-size="sm" + :class="{ + 'md:gl-border-r md:gl-mr-3 md:gl-border-gray-100 md:gl-pr-3': hasUpdateTimeStamp, + }" + :health-status="healthStatus" + /> +</template> diff --git a/app/assets/javascripts/work_items/pages/work_items_list_app.vue b/app/assets/javascripts/work_items/pages/work_items_list_app.vue index a35606b21d9f266d93c1f3743e96a030dba0e60a..fe996990ec9e21311fdedc761c8f66b7e00a91c6 100644 --- a/app/assets/javascripts/work_items/pages/work_items_list_app.vue +++ b/app/assets/javascripts/work_items/pages/work_items_list_app.vue @@ -4,6 +4,7 @@ import { isEmpty } from 'lodash'; import * as Sentry from '~/sentry/sentry_browser_wrapper'; import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue'; import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue'; +import WorkItemHealthStatus from '~/work_items/components/work_item_health_status.vue'; import { convertToApiParams, convertToSearchQuery, @@ -100,6 +101,7 @@ export default { IssueCardStatistics, IssueCardTimeInfo, WorkItemDrawer, + WorkItemHealthStatus, }, mixins: [glFeatureFlagMixin()], inject: [ @@ -662,7 +664,7 @@ export default { </template> <template #timeframe="{ issuable = {} }"> - <issue-card-time-info :issue="issuable" /> + <issue-card-time-info :issue="issuable" :is-work-item-list="true" /> </template> <template #status="{ issuable }"> @@ -688,6 +690,10 @@ export default { <template #sidebar-items="{ checkedIssuables }"> <slot name="sidebar-items" :checked-issuables="checkedIssuables"></slot> </template> + + <template #health-status="{ issuable = {} }"> + <work-item-health-status :issue="issuable" /> + </template> </issuable-list> </div> diff --git a/ee/app/assets/javascripts/issues/list/components/issue_card_time_info.vue b/ee/app/assets/javascripts/issues/list/components/issue_card_time_info.vue index 3bb81c9d1e7b794839a5e9359bcdfdfea63782bb..ee33e2ee5f8ece40a71230ed0d856c7046a25452 100644 --- a/ee/app/assets/javascripts/issues/list/components/issue_card_time_info.vue +++ b/ee/app/assets/javascripts/issues/list/components/issue_card_time_info.vue @@ -18,6 +18,11 @@ export default { type: Object, required: true, }, + isWorkItemList: { + type: Boolean, + required: false, + default: false, + }, }, computed: { healthStatus() { @@ -26,7 +31,7 @@ export default { ); }, showHealthStatus() { - return this.hasIssuableHealthStatusFeature && this.healthStatus; + return this.hasIssuableHealthStatusFeature && this.healthStatus && !this.isWorkItemList; }, weight() { return this.issue.weight || this.issue.widgets?.find(isWeightWidget)?.weight; diff --git a/ee/spec/frontend/issues/list/components/issue_card_time_info_spec.js b/ee/spec/frontend/issues/list/components/issue_card_time_info_spec.js index 6d6cbb44ebdf1dce338ad06238d537d11b268b96..c84fa98090bda362cf352fa7fad4c4dbfd3ed725 100644 --- a/ee/spec/frontend/issues/list/components/issue_card_time_info_spec.js +++ b/ee/spec/frontend/issues/list/components/issue_card_time_info_spec.js @@ -32,10 +32,11 @@ describe('EE IssueCardTimeInfo component', () => { issue, hasIssuableHealthStatusFeature = false, hasIssueWeightsFeature = false, + isWorkItemList = false, } = {}) => shallowMount(IssueCardTimeInfo, { provide: { hasIssuableHealthStatusFeature, hasIssueWeightsFeature }, - propsData: { issue }, + propsData: { issue, isWorkItemList }, }); describe.each` @@ -52,6 +53,30 @@ describe('EE IssueCardTimeInfo component', () => { }); describe('health status', () => { + describe('when isWorkItemList=true', () => { + it('does not renders', () => { + wrapper = mountComponent({ + issue: obj, + hasIssuableHealthStatusFeature: true, + isWorkItemList: true, + }); + + expect(findIssueHealthStatus().exists()).toBe(false); + }); + }); + + describe('when isWorkItemList=false', () => { + it('renders', () => { + wrapper = mountComponent({ + issue: obj, + hasIssuableHealthStatusFeature: true, + isWorkItemList: false, + }); + + expect(findIssueHealthStatus().props('healthStatus')).toBe('onTrack'); + }); + }); + describe('when hasIssuableHealthStatusFeature=true', () => { it('renders', () => { wrapper = mountComponent({ hasIssuableHealthStatusFeature: true, issue: obj }); diff --git a/ee/spec/frontend/work_items/list/components/work_items_list_app_spec.js b/ee/spec/frontend/work_items/list/components/work_items_list_app_spec.js index 841e2a464751c6a5adcbc70c9fff71842d1c89d4..6a7b7ffd1806692738f5685bdfedfd1723d1b327 100644 --- a/ee/spec/frontend/work_items/list/components/work_items_list_app_spec.js +++ b/ee/spec/frontend/work_items/list/components/work_items_list_app_spec.js @@ -52,6 +52,7 @@ describeSkipVue3(skipReason, () => { isSignedIn: true, hasOkrsFeature: true, hasQualityManagementFeature: true, + hasIssuableHealthStatusFeature: true, }; const mountComponent = ({ @@ -109,6 +110,7 @@ describeSkipVue3(skipReason, () => { stubs: { IssuableItem: true, IssueCardTimeInfo: true, + IssueHealthStatus: true, }, }); diff --git a/spec/frontend/work_items/components/work_item_health_status_spec.js b/spec/frontend/work_items/components/work_item_health_status_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..9b443f0ec87938823cadcc07c8ddb3fa7060d8ac --- /dev/null +++ b/spec/frontend/work_items/components/work_item_health_status_spec.js @@ -0,0 +1,73 @@ +import { shallowMount } from '@vue/test-utils'; +import WorkItemHealthStatus from '~/work_items/components/work_item_health_status.vue'; +import { WIDGET_TYPE_HEALTH_STATUS } from '~/work_items/constants'; + +const IssueHealthStatus = { template: '<div></div>', props: ['healthStatus'] }; + +describe('WorkItemHealthStatus', () => { + let wrapper; + + const issueWithHealthStatus = { + healthStatus: 'onTrack', + }; + + const issueWithWidgetHealthStatus = { + widgets: [ + { + type: WIDGET_TYPE_HEALTH_STATUS, + healthStatus: 'onTrack', + }, + ], + }; + + const issueWithoutHealthStatus = {}; + + const findIssueHealthStatus = () => wrapper.findComponent(IssueHealthStatus); + + const mountComponent = ({ issue, hasIssuableHealthStatusFeature = false } = {}) => + shallowMount(WorkItemHealthStatus, { + provide: { hasIssuableHealthStatusFeature }, + propsData: { issue }, + stubs: { IssueHealthStatus }, + }); + + describe('when hasIssuableHealthStatusFeature=false', () => { + it('does not render IssueHealthStatus', () => { + wrapper = mountComponent({ + issue: issueWithHealthStatus, + hasIssuableHealthStatusFeature: false, + }); + + expect(findIssueHealthStatus().exists()).toBe(false); + }); + }); + + describe('when hasIssuableHealthStatusFeature=true', () => { + it('renders IssueHealthStatus when healthStatus is defined on issue', () => { + wrapper = mountComponent({ + issue: issueWithHealthStatus, + hasIssuableHealthStatusFeature: true, + }); + + expect(findIssueHealthStatus().props('healthStatus')).toBe('onTrack'); + }); + + it('renders IssueHealthStatus when healthStatus is defined on widget', () => { + wrapper = mountComponent({ + issue: issueWithWidgetHealthStatus, + hasIssuableHealthStatusFeature: true, + }); + + expect(findIssueHealthStatus().props('healthStatus')).toBe('onTrack'); + }); + + it('does not render IssueHealthStatus when no health status is defined', () => { + wrapper = mountComponent({ + issue: issueWithoutHealthStatus, + hasIssuableHealthStatusFeature: true, + }); + + expect(findIssueHealthStatus().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/work_items/list/components/work_items_list_app_spec.js b/spec/frontend/work_items/list/components/work_items_list_app_spec.js index dbd87c5e42cd8f43498a2c90325fcbb2bb4aa072..ec9e612d1fed9a9e5d2acdc306e14000c97cd457 100644 --- a/spec/frontend/work_items/list/components/work_items_list_app_spec.js +++ b/spec/frontend/work_items/list/components/work_items_list_app_spec.js @@ -7,6 +7,7 @@ import VueRouter from 'vue-router'; import * as Sentry from '~/sentry/sentry_browser_wrapper'; import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue'; import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue'; +import WorkItemHealthStatus from '~/work_items/components/work_item_health_status.vue'; import createMockApollo from 'helpers/mock_apollo_helper'; import { describeSkipVue3, SkipReason } from 'helpers/vue3_conditional'; import waitForPromises from 'helpers/wait_for_promises'; @@ -76,6 +77,7 @@ describeSkipVue3(skipReason, () => { const findIssuableList = () => wrapper.findComponent(IssuableList); const findIssueCardStatistics = () => wrapper.findComponent(IssueCardStatistics); const findIssueCardTimeInfo = () => wrapper.findComponent(IssueCardTimeInfo); + const findWorkItemHealthStatus = () => wrapper.findComponent(WorkItemHealthStatus); const findDrawer = () => wrapper.findComponent(WorkItemDrawer); const mountComponent = ({ @@ -152,6 +154,11 @@ describeSkipVue3(skipReason, () => { it('renders IssueCardTimeInfo component', () => { expect(findIssueCardTimeInfo().exists()).toBe(true); + expect(findIssueCardTimeInfo().props('isWorkItemList')).toBe(true); + }); + + it('renders IssueHealthStatus component', () => { + expect(findWorkItemHealthStatus().exists()).toBe(true); }); it('renders work items', () => {