Commit d89c4976 authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch '300644-use-graphql-for-sidebar-time-tracking' into 'master'

Use GraphQL for Time tracking info on Issuable Sidebar

See merge request gitlab-org/gitlab!63773
parents ab5a960d ea7b1fa1
...@@ -9,18 +9,29 @@ export default { ...@@ -9,18 +9,29 @@ export default {
inject: ['timeTrackingLimitToHours'], inject: ['timeTrackingLimitToHours'],
computed: { computed: {
...mapGetters(['activeBoardItem']), ...mapGetters(['activeBoardItem']),
initialTimeTracking() {
const {
timeEstimate,
totalTimeSpent,
humanTimeEstimate,
humanTotalTimeSpent,
} = this.activeBoardItem;
return {
timeEstimate,
totalTimeSpent,
humanTimeEstimate,
humanTotalTimeSpent,
};
},
}, },
}; };
</script> </script>
<template> <template>
<issuable-time-tracker <issuable-time-tracker
:issuable-id="activeBoardItem.id.toString()" :issuable-iid="activeBoardItem.iid.toString()"
:time-estimate="activeBoardItem.timeEstimate"
:time-spent="activeBoardItem.totalTimeSpent"
:human-time-estimate="activeBoardItem.humanTimeEstimate"
:human-time-spent="activeBoardItem.humanTotalTimeSpent"
:limit-to-hours="timeTrackingLimitToHours" :limit-to-hours="timeTrackingLimitToHours"
:initial-time-tracking="initialTimeTracking"
:show-collapsed="false" :show-collapsed="false"
/> />
</template> </template>
...@@ -2,5 +2,6 @@ export default class IssueProject { ...@@ -2,5 +2,6 @@ export default class IssueProject {
constructor(obj) { constructor(obj) {
this.id = obj.id; this.id = obj.id;
this.path = obj.path; this.path = obj.path;
this.fullPath = obj.path_with_namespace;
} }
} }
...@@ -5,8 +5,6 @@ import { intersection } from 'lodash'; ...@@ -5,8 +5,6 @@ import { intersection } from 'lodash';
import '~/smart_interval'; import '~/smart_interval';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import Mediator from '../../sidebar_mediator';
import Store from '../../stores/sidebar_store';
import IssuableTimeTracker from './time_tracker.vue'; import IssuableTimeTracker from './time_tracker.vue';
export default { export default {
...@@ -14,16 +12,20 @@ export default { ...@@ -14,16 +12,20 @@ export default {
IssuableTimeTracker, IssuableTimeTracker,
}, },
props: { props: {
issuableId: { fullPath: {
type: String,
required: false,
default: '',
},
issuableIid: {
type: String, type: String,
required: true, required: true,
}, },
}, limitToHours: {
data() { type: Boolean,
return { required: false,
mediator: new Mediator(), default: false,
store: new Store(), },
};
}, },
mounted() { mounted() {
this.listenForQuickActions(); this.listenForQuickActions();
...@@ -47,7 +49,7 @@ export default { ...@@ -47,7 +49,7 @@ export default {
changedCommands = []; changedCommands = [];
} }
if (changedCommands && intersection(subscribedCommands, changedCommands).length) { if (changedCommands && intersection(subscribedCommands, changedCommands).length) {
this.mediator.fetch(); eventHub.$emit('timeTracker:refresh');
} }
}, },
}, },
...@@ -57,12 +59,9 @@ export default { ...@@ -57,12 +59,9 @@ export default {
<template> <template>
<div class="block"> <div class="block">
<issuable-time-tracker <issuable-time-tracker
:issuable-id="issuableId" :full-path="fullPath"
:time-estimate="store.timeEstimate" :issuable-iid="issuableIid"
:time-spent="store.totalTimeSpent" :limit-to-hours="limitToHours"
:human-time-estimate="store.humanTimeEstimate"
:human-time-spent="store.humanTotalTimeSpent"
:limit-to-hours="store.timeTrackingLimitToHours"
/> />
</div> </div>
</template> </template>
<script> <script>
import { GlIcon, GlLink, GlModal, GlModalDirective } from '@gitlab/ui'; import { GlIcon, GlLink, GlModal, GlModalDirective, GlLoadingIcon } from '@gitlab/ui';
import { IssuableType } from '~/issue_show/constants'; import { IssuableType } from '~/issue_show/constants';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import { timeTrackingQueries } from '~/sidebar/constants';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import TimeTrackingCollapsedState from './collapsed_state.vue'; import TimeTrackingCollapsedState from './collapsed_state.vue';
import TimeTrackingComparisonPane from './comparison_pane.vue'; import TimeTrackingComparisonPane from './comparison_pane.vue';
...@@ -19,6 +21,7 @@ export default { ...@@ -19,6 +21,7 @@ export default {
GlIcon, GlIcon,
GlLink, GlLink,
GlModal, GlModal,
GlLoadingIcon,
TimeTrackingCollapsedState, TimeTrackingCollapsedState,
TimeTrackingSpentOnlyPane, TimeTrackingSpentOnlyPane,
TimeTrackingComparisonPane, TimeTrackingComparisonPane,
...@@ -30,33 +33,25 @@ export default { ...@@ -30,33 +33,25 @@ export default {
}, },
inject: ['issuableType'], inject: ['issuableType'],
props: { props: {
timeEstimate: { limitToHours: {
type: Number, type: Boolean,
required: true, default: false,
}, required: false,
timeSpent: {
type: Number,
required: true,
}, },
humanTimeEstimate: { fullPath: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
}, },
humanTimeSpent: { issuableIid: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
}, },
limitToHours: { initialTimeTracking: {
type: Boolean, type: Object,
default: false,
required: false, required: false,
}, default: null,
issuableId: {
type: String,
required: false,
default: '',
}, },
/* /*
In issue list, "time-tracking-collapsed-state" is always rendered even if the sidebar isn't collapsed. In issue list, "time-tracking-collapsed-state" is always rendered even if the sidebar isn't collapsed.
...@@ -77,26 +72,73 @@ export default { ...@@ -77,26 +72,73 @@ export default {
data() { data() {
return { return {
showHelp: false, showHelp: false,
timeTracking: {
...this.initialTimeTracking,
},
}; };
}, },
apollo: {
issuableTimeTracking: {
query() {
return timeTrackingQueries[this.issuableType].query;
},
skip() {
// We don't fetch info via GraphQL in following cases
// 1. Time tracking info was provided via prop
// 2. issuableIid and fullPath are not provided.
if (!this.initialTimeTracking) {
return false;
} else if (this.issuableIid && this.fullPath) {
return false;
}
return true;
},
variables() {
return {
iid: this.issuableIid,
fullPath: this.fullPath,
};
},
update(data) {
this.timeTracking = {
...data.workspace?.issuable,
};
},
},
},
computed: { computed: {
hasTimeSpent() { isTimeTrackingInfoLoading() {
return Boolean(this.timeSpent); return this.$apollo?.queries.issuableTimeTracking.loading ?? false;
},
timeEstimate() {
return this.timeTracking?.timeEstimate || 0;
},
totalTimeSpent() {
return this.timeTracking?.totalTimeSpent || 0;
},
humanTimeEstimate() {
return this.timeTracking?.humanTimeEstimate || '';
},
humanTotalTimeSpent() {
return this.timeTracking?.humanTotalTimeSpent || '';
},
hasTotalTimeSpent() {
return Boolean(this.totalTimeSpent);
}, },
hasTimeEstimate() { hasTimeEstimate() {
return Boolean(this.timeEstimate); return Boolean(this.timeEstimate);
}, },
showComparisonState() { showComparisonState() {
return this.hasTimeEstimate && this.hasTimeSpent; return this.hasTimeEstimate && this.hasTotalTimeSpent;
}, },
showEstimateOnlyState() { showEstimateOnlyState() {
return this.hasTimeEstimate && !this.hasTimeSpent; return this.hasTimeEstimate && !this.hasTotalTimeSpent;
}, },
showSpentOnlyState() { showSpentOnlyState() {
return this.hasTimeSpent && !this.hasTimeEstimate; return this.hasTotalTimeSpent && !this.hasTimeEstimate;
}, },
showNoTimeTrackingState() { showNoTimeTrackingState() {
return !this.hasTimeEstimate && !this.hasTimeSpent; return !this.hasTimeEstimate && !this.hasTotalTimeSpent;
}, },
showHelpState() { showHelpState() {
return Boolean(this.showHelp); return Boolean(this.showHelp);
...@@ -104,26 +146,29 @@ export default { ...@@ -104,26 +146,29 @@ export default {
isTimeReportSupported() { isTimeReportSupported() {
return ( return (
[IssuableType.Issue, IssuableType.MergeRequest].includes(this.issuableType) && [IssuableType.Issue, IssuableType.MergeRequest].includes(this.issuableType) &&
this.issuableId this.issuableIid
); );
}, },
}, },
watch: {
/**
* When `initialTimeTracking` is provided via prop,
* we don't query the same via GraphQl and instead
* monitor it for any updates (eg; Epic Swimlanes)
*/
initialTimeTracking(timeTracking) {
this.timeTracking = timeTracking;
},
},
created() { created() {
eventHub.$on('timeTracker:updateData', this.update); eventHub.$on('timeTracker:refresh', this.refresh);
}, },
methods: { methods: {
toggleHelpState(show) { toggleHelpState(show) {
this.showHelp = show; this.showHelp = show;
}, },
update(data) { refresh() {
const { timeEstimate, timeSpent, humanTimeEstimate, humanTimeSpent } = data; this.$apollo.queries.issuableTimeTracking.refetch();
/* eslint-disable vue/no-mutating-props */
this.timeEstimate = timeEstimate;
this.timeSpent = timeSpent;
this.humanTimeEstimate = humanTimeEstimate;
this.humanTimeSpent = humanTimeSpent;
/* eslint-enable vue/no-mutating-props */
}, },
}, },
}; };
...@@ -138,11 +183,12 @@ export default { ...@@ -138,11 +183,12 @@ export default {
:show-help-state="showHelpState" :show-help-state="showHelpState"
:show-spent-only-state="showSpentOnlyState" :show-spent-only-state="showSpentOnlyState"
:show-estimate-only-state="showEstimateOnlyState" :show-estimate-only-state="showEstimateOnlyState"
:time-spent-human-readable="humanTimeSpent" :time-spent-human-readable="humanTotalTimeSpent"
:time-estimate-human-readable="humanTimeEstimate" :time-estimate-human-readable="humanTimeEstimate"
/> />
<div class="hide-collapsed gl-line-height-20 gl-text-gray-900"> <div class="hide-collapsed gl-line-height-20 gl-text-gray-900">
{{ __('Time tracking') }} {{ __('Time tracking') }}
<gl-loading-icon v-if="isTimeTrackingInfoLoading" inline />
<div <div
v-if="!showHelpState" v-if="!showHelpState"
data-testid="helpButton" data-testid="helpButton"
...@@ -160,14 +206,14 @@ export default { ...@@ -160,14 +206,14 @@ export default {
<gl-icon name="close" /> <gl-icon name="close" />
</div> </div>
</div> </div>
<div class="hide-collapsed"> <div v-if="!isTimeTrackingInfoLoading" class="hide-collapsed">
<div v-if="showEstimateOnlyState" data-testid="estimateOnlyPane"> <div v-if="showEstimateOnlyState" data-testid="estimateOnlyPane">
<span class="gl-font-weight-bold">{{ $options.i18n.estimatedOnlyText }} </span <span class="gl-font-weight-bold">{{ $options.i18n.estimatedOnlyText }} </span
>{{ humanTimeEstimate }} >{{ humanTimeEstimate }}
</div> </div>
<time-tracking-spent-only-pane <time-tracking-spent-only-pane
v-if="showSpentOnlyState" v-if="showSpentOnlyState"
:time-spent-human-readable="humanTimeSpent" :time-spent-human-readable="humanTotalTimeSpent"
/> />
<div v-if="showNoTimeTrackingState" data-testid="noTrackingPane"> <div v-if="showNoTimeTrackingState" data-testid="noTrackingPane">
<span class="gl-text-gray-500">{{ $options.i18n.noTimeTrackingText }}</span> <span class="gl-text-gray-500">{{ $options.i18n.noTimeTrackingText }}</span>
...@@ -175,14 +221,14 @@ export default { ...@@ -175,14 +221,14 @@ export default {
<time-tracking-comparison-pane <time-tracking-comparison-pane
v-if="showComparisonState" v-if="showComparisonState"
:time-estimate="timeEstimate" :time-estimate="timeEstimate"
:time-spent="timeSpent" :time-spent="totalTimeSpent"
:time-spent-human-readable="humanTimeSpent" :time-spent-human-readable="humanTotalTimeSpent"
:time-estimate-human-readable="humanTimeEstimate" :time-estimate-human-readable="humanTimeEstimate"
:limit-to-hours="limitToHours" :limit-to-hours="limitToHours"
/> />
<template v-if="isTimeReportSupported"> <template v-if="isTimeReportSupported">
<gl-link <gl-link
v-if="hasTimeSpent" v-if="hasTotalTimeSpent"
v-gl-modal="'time-tracking-report'" v-gl-modal="'time-tracking-report'"
data-testid="reportLink" data-testid="reportLink"
href="#" href="#"
...@@ -194,7 +240,7 @@ export default { ...@@ -194,7 +240,7 @@ export default {
:title="__('Time tracking report')" :title="__('Time tracking report')"
:hide-footer="true" :hide-footer="true"
> >
<time-tracking-report :limit-to-hours="limitToHours" :issuable-id="issuableId" /> <time-tracking-report :limit-to-hours="limitToHours" :issuable-iid="issuableIid" />
</gl-modal> </gl-modal>
</template> </template>
<transition name="help-state-toggle"> <transition name="help-state-toggle">
......
...@@ -9,8 +9,10 @@ import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.g ...@@ -9,8 +9,10 @@ import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.g
import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql'; import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql';
import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql'; import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
import issueSubscribedQuery from '~/sidebar/queries/issue_subscribed.query.graphql'; import issueSubscribedQuery from '~/sidebar/queries/issue_subscribed.query.graphql';
import issueTimeTrackingQuery from '~/sidebar/queries/issue_time_tracking.query.graphql';
import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql'; import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql';
import mergeRequestSubscribed from '~/sidebar/queries/merge_request_subscribed.query.graphql'; import mergeRequestSubscribed from '~/sidebar/queries/merge_request_subscribed.query.graphql';
import mergeRequestTimeTrackingQuery from '~/sidebar/queries/merge_request_time_tracking.query.graphql';
import updateEpicConfidentialMutation from '~/sidebar/queries/update_epic_confidential.mutation.graphql'; import updateEpicConfidentialMutation from '~/sidebar/queries/update_epic_confidential.mutation.graphql';
import updateEpicDueDateMutation from '~/sidebar/queries/update_epic_due_date.mutation.graphql'; import updateEpicDueDateMutation from '~/sidebar/queries/update_epic_due_date.mutation.graphql';
import updateEpicStartDateMutation from '~/sidebar/queries/update_epic_start_date.mutation.graphql'; import updateEpicStartDateMutation from '~/sidebar/queries/update_epic_start_date.mutation.graphql';
...@@ -120,6 +122,15 @@ export const subscribedQueries = { ...@@ -120,6 +122,15 @@ export const subscribedQueries = {
}, },
}; };
export const timeTrackingQueries = {
[IssuableType.Issue]: {
query: issueTimeTrackingQuery,
},
[IssuableType.MergeRequest]: {
query: mergeRequestTimeTrackingQuery,
},
};
export const dueDateQueries = { export const dueDateQueries = {
[IssuableType.Issue]: { [IssuableType.Issue]: {
query: issueDueDateQuery, query: issueDueDateQuery,
......
...@@ -15,7 +15,7 @@ export default class SidebarMilestone { ...@@ -15,7 +15,7 @@ export default class SidebarMilestone {
humanTimeEstimate, humanTimeEstimate,
humanTimeSpent, humanTimeSpent,
limitToHours, limitToHours,
id, iid,
} = el.dataset; } = el.dataset;
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
...@@ -30,12 +30,14 @@ export default class SidebarMilestone { ...@@ -30,12 +30,14 @@ export default class SidebarMilestone {
render: (createElement) => render: (createElement) =>
createElement('timeTracker', { createElement('timeTracker', {
props: { props: {
timeEstimate: parseInt(timeEstimate, 10),
timeSpent: parseInt(timeSpent, 10),
humanTimeEstimate,
humanTimeSpent,
limitToHours: parseBoolean(limitToHours), limitToHours: parseBoolean(limitToHours),
issuableId: id.toString(), issuableIid: iid.toString(),
initialTimeTracking: {
timeEstimate: parseInt(timeEstimate, 10),
totalTimeSpent: parseInt(timeSpent, 10),
humanTimeEstimate,
humanTotalTimeSpent: humanTimeSpent,
},
}, },
}), }),
}); });
......
...@@ -391,7 +391,7 @@ function mountSubscriptionsComponent() { ...@@ -391,7 +391,7 @@ function mountSubscriptionsComponent() {
function mountTimeTrackingComponent() { function mountTimeTrackingComponent() {
const el = document.getElementById('issuable-time-tracker'); const el = document.getElementById('issuable-time-tracker');
const { id, issuableType } = getSidebarOptions(); const { iid, fullPath, issuableType, timeTrackingLimitToHours } = getSidebarOptions();
if (!el) return; if (!el) return;
...@@ -403,7 +403,9 @@ function mountTimeTrackingComponent() { ...@@ -403,7 +403,9 @@ function mountTimeTrackingComponent() {
render: (createElement) => render: (createElement) =>
createElement(SidebarTimeTracking, { createElement(SidebarTimeTracking, {
props: { props: {
issuableId: id.toString(), fullPath,
issuableIid: iid.toString(),
limitToHours: timeTrackingLimitToHours,
}, },
}), }),
}); });
......
query issueTimeTracking($fullPath: ID!, $iid: String) {
workspace: project(fullPath: $fullPath) {
__typename
issuable: issue(iid: $iid) {
__typename
id
humanTimeEstimate
humanTotalTimeSpent
timeEstimate
totalTimeSpent
}
}
}
query mergeRequestTimeTracking($fullPath: ID!, $iid: String!) {
workspace: project(fullPath: $fullPath) {
__typename
issuable: mergeRequest(iid: $iid) {
__typename
id
humanTimeEstimate
humanTotalTimeSpent
timeEstimate
totalTimeSpent
}
}
}
...@@ -17,7 +17,7 @@ class IssueBoardEntity < Grape::Entity ...@@ -17,7 +17,7 @@ class IssueBoardEntity < Grape::Entity
end end
expose :project do |issue| expose :project do |issue|
API::Entities::Project.represent issue.project, only: [:id, :path] API::Entities::Project.represent issue.project, only: [:id, :path, :path_with_namespace]
end end
expose :milestone, if: -> (issue) { issue.milestone } do |issue| expose :milestone, if: -> (issue) { issue.milestone } do |issue|
......
.block.time-tracking .block.time-tracking
%time-tracker{ ":time-estimate" => "issue.timeEstimate || 0", %time-tracker{ ":limit-to-hours" => "timeTrackingLimitToHours",
":time-spent" => "issue.timeSpent || 0", ":issuable-iid" => "issue.iid ? issue.iid.toString() : ''",
":human-time-estimate" => "issue.humanTimeEstimate", ":full-path" => "issue.project ? issue.project.fullPath : ''",
":human-time-spent" => "issue.humanTimeSpent",
":limit-to-hours" => "timeTrackingLimitToHours",
":issuable-id" => "issue.id ? issue.id.toString() : ''",
"root-path" => "#{root_url}" } "root-path" => "#{root_url}" }
...@@ -98,7 +98,7 @@ ...@@ -98,7 +98,7 @@
time_spent: @milestone.total_time_spent, time_spent: @milestone.total_time_spent,
human_time_estimate: @milestone.human_total_time_estimate, human_time_estimate: @milestone.human_total_time_estimate,
human_time_spent: @milestone.human_total_time_spent, human_time_spent: @milestone.human_total_time_spent,
id: @milestone.id, iid: @milestone.iid,
limit_to_hours: Gitlab::CurrentSettings.time_tracking_limit_to_hours.to_s } } limit_to_hours: Gitlab::CurrentSettings.time_tracking_limit_to_hours.to_s } }
= render_if_exists 'shared/milestones/weight', milestone: milestone = render_if_exists 'shared/milestones/weight', milestone: milestone
......
...@@ -2,22 +2,21 @@ ...@@ -2,22 +2,21 @@
import createFlash from '~/flash'; import createFlash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import eventHub from '~/sidebar/event_hub'; import eventHub from '~/sidebar/event_hub';
import Mediator from '../../sidebar_mediator';
import weightComponent from './weight.vue'; import weightComponent from './weight.vue';
export default { export default {
components: { components: {
weight: weightComponent, weight: weightComponent,
}, },
props: { data() {
mediator: { return {
required: true, // Defining `mediator` here as a data prop
type: Object, // makes it reactive for any internal updates
validator(mediatorObject) { // which wouldn't happen otherwise.
return mediatorObject.updateWeight && mediatorObject.store; mediator: new Mediator(),
}, };
},
}, },
created() { created() {
eventHub.$on('updateWeight', this.onUpdateWeight); eventHub.$on('updateWeight', this.onUpdateWeight);
}, },
......
...@@ -14,7 +14,7 @@ import { IssuableAttributeType } from './constants'; ...@@ -14,7 +14,7 @@ import { IssuableAttributeType } from './constants';
Vue.use(VueApollo); Vue.use(VueApollo);
const mountWeightComponent = (mediator) => { const mountWeightComponent = () => {
const el = document.querySelector('.js-sidebar-weight-entry-point'); const el = document.querySelector('.js-sidebar-weight-entry-point');
if (!el) return false; if (!el) return false;
...@@ -24,12 +24,7 @@ const mountWeightComponent = (mediator) => { ...@@ -24,12 +24,7 @@ const mountWeightComponent = (mediator) => {
components: { components: {
SidebarWeight, SidebarWeight,
}, },
render: (createElement) => render: (createElement) => createElement('sidebar-weight'),
createElement('sidebar-weight', {
props: {
mediator,
},
}),
}); });
}; };
...@@ -140,7 +135,7 @@ function mountIterationSelect() { ...@@ -140,7 +135,7 @@ function mountIterationSelect() {
export default function mountSidebar(mediator) { export default function mountSidebar(mediator) {
CEMountSidebar.mountSidebar(mediator); CEMountSidebar.mountSidebar(mediator);
mountWeightComponent(mediator); mountWeightComponent();
mountStatusComponent(mediator); mountStatusComponent(mediator);
mountEpicsSelect(); mountEpicsSelect();
mountIterationSelect(); mountIterationSelect();
......
...@@ -36,6 +36,7 @@ RSpec.describe 'Issue Sidebar' do ...@@ -36,6 +36,7 @@ RSpec.describe 'Issue Sidebar' do
before do before do
project.add_maintainer(user) project.add_maintainer(user)
visit_issue(project, issue) visit_issue(project, issue)
wait_for_all_requests
end end
it 'updates weight in sidebar to 1' do it 'updates weight in sidebar to 1' do
......
...@@ -10,10 +10,8 @@ describe('Sidebar Weight', () => { ...@@ -10,10 +10,8 @@ describe('Sidebar Weight', () => {
let sidebarMediator; let sidebarMediator;
let wrapper; let wrapper;
const createComponent = (props = {}) => { const createComponent = () => {
wrapper = shallowMount(SidebarWeight, { wrapper = shallowMount(SidebarWeight);
propsData: { ...props },
});
}; };
beforeEach(() => { beforeEach(() => {
......
...@@ -18,7 +18,8 @@ ...@@ -18,7 +18,8 @@
"type": "object", "type": "object",
"properties": { "properties": {
"id": { "type": "integer" }, "id": { "type": "integer" },
"path": { "type": "string" } "path": { "type": "string" },
"path_with_namespace": { "type": "string" }
} }
}, },
"milestone": { "milestone": {
......
...@@ -26,7 +26,7 @@ describe('BoardSidebarTimeTracker', () => { ...@@ -26,7 +26,7 @@ describe('BoardSidebarTimeTracker', () => {
store = createStore(); store = createStore();
store.state.boardItems = { store.state.boardItems = {
1: { 1: {
id: 1, iid: 1,
timeEstimate: 3600, timeEstimate: 3600,
totalTimeSpent: 1800, totalTimeSpent: 1800,
humanTimeEstimate: '1h', humanTimeEstimate: '1h',
...@@ -47,13 +47,16 @@ describe('BoardSidebarTimeTracker', () => { ...@@ -47,13 +47,16 @@ describe('BoardSidebarTimeTracker', () => {
createComponent({ provide: { timeTrackingLimitToHours } }); createComponent({ provide: { timeTrackingLimitToHours } });
expect(wrapper.find(IssuableTimeTracker).props()).toEqual({ expect(wrapper.find(IssuableTimeTracker).props()).toEqual({
timeEstimate: 3600,
timeSpent: 1800,
humanTimeEstimate: '1h',
humanTimeSpent: '30min',
limitToHours: timeTrackingLimitToHours, limitToHours: timeTrackingLimitToHours,
showCollapsed: false, showCollapsed: false,
issuableId: '1', issuableIid: '1',
fullPath: '',
initialTimeTracking: {
timeEstimate: 3600,
totalTimeSpent: 1800,
humanTimeEstimate: '1h',
humanTotalTimeSpent: '30min',
},
}); });
}, },
); );
......
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { stubTransition } from 'helpers/stub_transition'; import { stubTransition } from 'helpers/stub_transition';
import { createMockDirective } from 'helpers/vue_mock_directive'; import { createMockDirective } from 'helpers/vue_mock_directive';
import TimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue'; import TimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue';
import SidebarEventHub from '~/sidebar/event_hub';
import { issuableTimeTrackingResponse } from '../../mock_data';
describe('Issuable Time Tracker', () => { describe('Issuable Time Tracker', () => {
let wrapper; let wrapper;
...@@ -13,16 +17,18 @@ describe('Issuable Time Tracker', () => { ...@@ -13,16 +17,18 @@ describe('Issuable Time Tracker', () => {
const findReportLink = () => findByTestId('reportLink'); const findReportLink = () => findByTestId('reportLink');
const defaultProps = { const defaultProps = {
timeEstimate: 10_000, // 2h 46m
timeSpent: 5_000, // 1h 23m
humanTimeEstimate: '2h 46m',
humanTimeSpent: '1h 23m',
limitToHours: false, limitToHours: false,
issuableId: '1', fullPath: 'gitlab-org/gitlab-test',
issuableIid: '1',
initialTimeTracking: {
...issuableTimeTrackingResponse.data.workspace.issuable,
},
}; };
const mountComponent = ({ props = {}, issuableType = 'issue' } = {}) => const issuableTimeTrackingRefetchSpy = jest.fn();
mount(TimeTracker, {
const mountComponent = ({ props = {}, issuableType = 'issue', loading = false } = {}) => {
return mount(TimeTracker, {
propsData: { ...defaultProps, ...props }, propsData: { ...defaultProps, ...props },
directives: { GlTooltip: createMockDirective() }, directives: { GlTooltip: createMockDirective() },
stubs: { stubs: {
...@@ -31,7 +37,19 @@ describe('Issuable Time Tracker', () => { ...@@ -31,7 +37,19 @@ describe('Issuable Time Tracker', () => {
provide: { provide: {
issuableType, issuableType,
}, },
mocks: {
$apollo: {
queries: {
issuableTimeTracking: {
loading,
refetch: issuableTimeTrackingRefetchSpy,
query: jest.fn().mockResolvedValue(issuableTimeTrackingResponse),
},
},
},
},
}); });
};
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -48,13 +66,13 @@ describe('Issuable Time Tracker', () => { ...@@ -48,13 +66,13 @@ describe('Issuable Time Tracker', () => {
it('should correctly render timeEstimate', () => { it('should correctly render timeEstimate', () => {
expect(findByTestId('timeTrackingComparisonPane').html()).toContain( expect(findByTestId('timeTrackingComparisonPane').html()).toContain(
defaultProps.humanTimeEstimate, defaultProps.initialTimeTracking.humanTimeEstimate,
); );
}); });
it('should correctly render time_spent', () => { it('should correctly render totalTimeSpent', () => {
expect(findByTestId('timeTrackingComparisonPane').html()).toContain( expect(findByTestId('timeTrackingComparisonPane').html()).toContain(
defaultProps.humanTimeSpent, defaultProps.initialTimeTracking.humanTotalTimeSpent,
); );
}); });
}); });
...@@ -82,10 +100,12 @@ describe('Issuable Time Tracker', () => { ...@@ -82,10 +100,12 @@ describe('Issuable Time Tracker', () => {
beforeEach(() => { beforeEach(() => {
wrapper = mountComponent({ wrapper = mountComponent({
props: { props: {
timeEstimate: 100_000, // 1d 3h initialTimeTracking: {
timeSpent: 5_000, // 1h 23m timeEstimate: 100_000, // 1d 3h
humanTimeEstimate: '1d 3h', totalTimeSpent: 5_000, // 1h 23m
humanTimeSpent: '1h 23m', humanTimeEstimate: '1d 3h',
humanTotalTimeSpent: '1h 23m',
},
}, },
}); });
}); });
...@@ -112,8 +132,11 @@ describe('Issuable Time Tracker', () => { ...@@ -112,8 +132,11 @@ describe('Issuable Time Tracker', () => {
it('should display the remaining meter with the correct background color when over estimate', () => { it('should display the remaining meter with the correct background color when over estimate', () => {
wrapper = mountComponent({ wrapper = mountComponent({
props: { props: {
timeEstimate: 10_000, // 2h 46m initialTimeTracking: {
timeSpent: 20_000_000, // 231 days ...defaultProps.initialTimeTracking,
timeEstimate: 10_000, // 2h 46m
totalTimeSpent: 20_000_000, // 231 days
},
}, },
}); });
...@@ -126,8 +149,11 @@ describe('Issuable Time Tracker', () => { ...@@ -126,8 +149,11 @@ describe('Issuable Time Tracker', () => {
beforeEach(async () => { beforeEach(async () => {
wrapper = mountComponent({ wrapper = mountComponent({
props: { props: {
timeEstimate: 100_000, // 1d 3h
limitToHours: true, limitToHours: true,
initialTimeTracking: {
...defaultProps.initialTimeTracking,
timeEstimate: 100_000, // 1d 3h
},
}, },
}); });
}); });
...@@ -144,10 +170,12 @@ describe('Issuable Time Tracker', () => { ...@@ -144,10 +170,12 @@ describe('Issuable Time Tracker', () => {
beforeEach(async () => { beforeEach(async () => {
wrapper = mountComponent({ wrapper = mountComponent({
props: { props: {
timeEstimate: 10_000, // 2h 46m initialTimeTracking: {
timeSpent: 0, timeEstimate: 10_000, // 2h 46m
timeEstimateHumanReadable: '2h 46m', totalTimeSpent: 0,
timeSpentHumanReadable: '', humanTimeEstimate: '2h 46m',
humanTotalTimeSpent: '',
},
}, },
}); });
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
...@@ -163,10 +191,12 @@ describe('Issuable Time Tracker', () => { ...@@ -163,10 +191,12 @@ describe('Issuable Time Tracker', () => {
beforeEach(() => { beforeEach(() => {
wrapper = mountComponent({ wrapper = mountComponent({
props: { props: {
timeEstimate: 0, initialTimeTracking: {
timeSpent: 5_000, // 1h 23m timeEstimate: 0,
timeEstimateHumanReadable: '2h 46m', totalTimeSpent: 5_000, // 1h 23m
timeSpentHumanReadable: '1h 23m', humanTimeEstimate: '2h 46m',
humanTotalTimeSpent: '1h 23m',
},
}, },
}); });
}); });
...@@ -181,10 +211,12 @@ describe('Issuable Time Tracker', () => { ...@@ -181,10 +211,12 @@ describe('Issuable Time Tracker', () => {
beforeEach(() => { beforeEach(() => {
wrapper = mountComponent({ wrapper = mountComponent({
props: { props: {
timeEstimate: 0, initialTimeTracking: {
timeSpent: 0, timeEstimate: 0,
timeEstimateHumanReadable: '', totalTimeSpent: 0,
timeSpentHumanReadable: '', humanTimeEstimate: '',
humanTotalTimeSpent: '',
},
}, },
}); });
}); });
...@@ -202,8 +234,11 @@ describe('Issuable Time Tracker', () => { ...@@ -202,8 +234,11 @@ describe('Issuable Time Tracker', () => {
beforeEach(() => { beforeEach(() => {
wrapper = mountComponent({ wrapper = mountComponent({
props: { props: {
timeSpent: 0, initialTimeTracking: {
timeSpentHumanReadable: '', ...defaultProps.initialTimeTracking,
totalTimeSpent: 0,
humanTotalTimeSpent: '',
},
}, },
}); });
}); });
...@@ -236,7 +271,16 @@ describe('Issuable Time Tracker', () => { ...@@ -236,7 +271,16 @@ describe('Issuable Time Tracker', () => {
const findCloseHelpButton = () => findByTestId('closeHelpButton'); const findCloseHelpButton = () => findByTestId('closeHelpButton');
beforeEach(async () => { beforeEach(async () => {
wrapper = mountComponent({ props: { timeEstimate: 0, timeSpent: 0 } }); wrapper = mountComponent({
props: {
initialTimeTracking: {
timeEstimate: 0,
totalTimeSpent: 0,
humanTimeEstimate: '',
humanTotalTimeSpent: '',
},
},
});
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
}); });
...@@ -265,4 +309,14 @@ describe('Issuable Time Tracker', () => { ...@@ -265,4 +309,14 @@ describe('Issuable Time Tracker', () => {
}); });
}); });
}); });
describe('Event listeners', () => {
it('refetches issuableTimeTracking query when eventHub emits `timeTracker:refresh` event', async () => {
SidebarEventHub.$emit('timeTracker:refresh');
await wrapper.vm.$nextTick();
expect(issuableTimeTrackingRefetchSpy).toHaveBeenCalled();
});
});
}); });
...@@ -592,4 +592,21 @@ export const emptyProjectMilestonesResponse = { ...@@ -592,4 +592,21 @@ export const emptyProjectMilestonesResponse = {
}, },
}; };
export const issuableTimeTrackingResponse = {
data: {
workspace: {
__typename: 'Project',
issuable: {
__typename: 'Issue',
id: 'gid://gitlab/Issue/1',
title: 'Commodi incidunt eos eos libero dicta dolores sed.',
timeEstimate: 10_000, // 2h 46m
totalTimeSpent: 5_000, // 1h 23m
humanTimeEstimate: '2h 46m',
humanTotalTimeSpent: '1h 23m',
},
},
},
};
export default mockData; export default mockData;
...@@ -15,7 +15,7 @@ RSpec.describe IssueBoardEntity do ...@@ -15,7 +15,7 @@ RSpec.describe IssueBoardEntity do
it 'has basic attributes' do it 'has basic attributes' do
expect(subject).to include(:id, :iid, :title, :confidential, :due_date, :project_id, :relative_position, expect(subject).to include(:id, :iid, :title, :confidential, :due_date, :project_id, :relative_position,
:labels, :assignees, project: hash_including(:id, :path)) :labels, :assignees, project: hash_including(:id, :path, :path_with_namespace))
end end
it 'has path and endpoints' do it 'has path and endpoints' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment