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 {
inject: ['timeTrackingLimitToHours'],
computed: {
...mapGetters(['activeBoardItem']),
initialTimeTracking() {
const {
timeEstimate,
totalTimeSpent,
humanTimeEstimate,
humanTotalTimeSpent,
} = this.activeBoardItem;
return {
timeEstimate,
totalTimeSpent,
humanTimeEstimate,
humanTotalTimeSpent,
};
},
},
};
</script>
<template>
<issuable-time-tracker
:issuable-id="activeBoardItem.id.toString()"
:time-estimate="activeBoardItem.timeEstimate"
:time-spent="activeBoardItem.totalTimeSpent"
:human-time-estimate="activeBoardItem.humanTimeEstimate"
:human-time-spent="activeBoardItem.humanTotalTimeSpent"
:issuable-iid="activeBoardItem.iid.toString()"
:limit-to-hours="timeTrackingLimitToHours"
:initial-time-tracking="initialTimeTracking"
:show-collapsed="false"
/>
</template>
......@@ -2,5 +2,6 @@ export default class IssueProject {
constructor(obj) {
this.id = obj.id;
this.path = obj.path;
this.fullPath = obj.path_with_namespace;
}
}
......@@ -5,8 +5,6 @@ import { intersection } from 'lodash';
import '~/smart_interval';
import eventHub from '../../event_hub';
import Mediator from '../../sidebar_mediator';
import Store from '../../stores/sidebar_store';
import IssuableTimeTracker from './time_tracker.vue';
export default {
......@@ -14,16 +12,20 @@ export default {
IssuableTimeTracker,
},
props: {
issuableId: {
fullPath: {
type: String,
required: false,
default: '',
},
issuableIid: {
type: String,
required: true,
},
},
data() {
return {
mediator: new Mediator(),
store: new Store(),
};
limitToHours: {
type: Boolean,
required: false,
default: false,
},
},
mounted() {
this.listenForQuickActions();
......@@ -47,7 +49,7 @@ export default {
changedCommands = [];
}
if (changedCommands && intersection(subscribedCommands, changedCommands).length) {
this.mediator.fetch();
eventHub.$emit('timeTracker:refresh');
}
},
},
......@@ -57,12 +59,9 @@ export default {
<template>
<div class="block">
<issuable-time-tracker
:issuable-id="issuableId"
:time-estimate="store.timeEstimate"
:time-spent="store.totalTimeSpent"
:human-time-estimate="store.humanTimeEstimate"
:human-time-spent="store.humanTotalTimeSpent"
:limit-to-hours="store.timeTrackingLimitToHours"
:full-path="fullPath"
:issuable-iid="issuableIid"
:limit-to-hours="limitToHours"
/>
</div>
</template>
<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 { s__, __ } from '~/locale';
import { timeTrackingQueries } from '~/sidebar/constants';
import eventHub from '../../event_hub';
import TimeTrackingCollapsedState from './collapsed_state.vue';
import TimeTrackingComparisonPane from './comparison_pane.vue';
......@@ -19,6 +21,7 @@ export default {
GlIcon,
GlLink,
GlModal,
GlLoadingIcon,
TimeTrackingCollapsedState,
TimeTrackingSpentOnlyPane,
TimeTrackingComparisonPane,
......@@ -30,33 +33,25 @@ export default {
},
inject: ['issuableType'],
props: {
timeEstimate: {
type: Number,
required: true,
},
timeSpent: {
type: Number,
required: true,
limitToHours: {
type: Boolean,
default: false,
required: false,
},
humanTimeEstimate: {
fullPath: {
type: String,
required: false,
default: '',
},
humanTimeSpent: {
issuableIid: {
type: String,
required: false,
default: '',
},
limitToHours: {
type: Boolean,
default: false,
initialTimeTracking: {
type: Object,
required: false,
},
issuableId: {
type: String,
required: false,
default: '',
default: null,
},
/*
In issue list, "time-tracking-collapsed-state" is always rendered even if the sidebar isn't collapsed.
......@@ -77,26 +72,73 @@ export default {
data() {
return {
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: {
hasTimeSpent() {
return Boolean(this.timeSpent);
isTimeTrackingInfoLoading() {
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() {
return Boolean(this.timeEstimate);
},
showComparisonState() {
return this.hasTimeEstimate && this.hasTimeSpent;
return this.hasTimeEstimate && this.hasTotalTimeSpent;
},
showEstimateOnlyState() {
return this.hasTimeEstimate && !this.hasTimeSpent;
return this.hasTimeEstimate && !this.hasTotalTimeSpent;
},
showSpentOnlyState() {
return this.hasTimeSpent && !this.hasTimeEstimate;
return this.hasTotalTimeSpent && !this.hasTimeEstimate;
},
showNoTimeTrackingState() {
return !this.hasTimeEstimate && !this.hasTimeSpent;
return !this.hasTimeEstimate && !this.hasTotalTimeSpent;
},
showHelpState() {
return Boolean(this.showHelp);
......@@ -104,26 +146,29 @@ export default {
isTimeReportSupported() {
return (
[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() {
eventHub.$on('timeTracker:updateData', this.update);
eventHub.$on('timeTracker:refresh', this.refresh);
},
methods: {
toggleHelpState(show) {
this.showHelp = show;
},
update(data) {
const { timeEstimate, timeSpent, humanTimeEstimate, humanTimeSpent } = data;
/* eslint-disable vue/no-mutating-props */
this.timeEstimate = timeEstimate;
this.timeSpent = timeSpent;
this.humanTimeEstimate = humanTimeEstimate;
this.humanTimeSpent = humanTimeSpent;
/* eslint-enable vue/no-mutating-props */
refresh() {
this.$apollo.queries.issuableTimeTracking.refetch();
},
},
};
......@@ -138,11 +183,12 @@ export default {
:show-help-state="showHelpState"
:show-spent-only-state="showSpentOnlyState"
:show-estimate-only-state="showEstimateOnlyState"
:time-spent-human-readable="humanTimeSpent"
:time-spent-human-readable="humanTotalTimeSpent"
:time-estimate-human-readable="humanTimeEstimate"
/>
<div class="hide-collapsed gl-line-height-20 gl-text-gray-900">
{{ __('Time tracking') }}
<gl-loading-icon v-if="isTimeTrackingInfoLoading" inline />
<div
v-if="!showHelpState"
data-testid="helpButton"
......@@ -160,14 +206,14 @@ export default {
<gl-icon name="close" />
</div>
</div>
<div class="hide-collapsed">
<div v-if="!isTimeTrackingInfoLoading" class="hide-collapsed">
<div v-if="showEstimateOnlyState" data-testid="estimateOnlyPane">
<span class="gl-font-weight-bold">{{ $options.i18n.estimatedOnlyText }} </span
>{{ humanTimeEstimate }}
</div>
<time-tracking-spent-only-pane
v-if="showSpentOnlyState"
:time-spent-human-readable="humanTimeSpent"
:time-spent-human-readable="humanTotalTimeSpent"
/>
<div v-if="showNoTimeTrackingState" data-testid="noTrackingPane">
<span class="gl-text-gray-500">{{ $options.i18n.noTimeTrackingText }}</span>
......@@ -175,14 +221,14 @@ export default {
<time-tracking-comparison-pane
v-if="showComparisonState"
:time-estimate="timeEstimate"
:time-spent="timeSpent"
:time-spent-human-readable="humanTimeSpent"
:time-spent="totalTimeSpent"
:time-spent-human-readable="humanTotalTimeSpent"
:time-estimate-human-readable="humanTimeEstimate"
:limit-to-hours="limitToHours"
/>
<template v-if="isTimeReportSupported">
<gl-link
v-if="hasTimeSpent"
v-if="hasTotalTimeSpent"
v-gl-modal="'time-tracking-report'"
data-testid="reportLink"
href="#"
......@@ -194,7 +240,7 @@ export default {
:title="__('Time tracking report')"
: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>
</template>
<transition name="help-state-toggle">
......
......@@ -9,8 +9,10 @@ import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.g
import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql';
import issueReferenceQuery from '~/sidebar/queries/issue_reference.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 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 updateEpicDueDateMutation from '~/sidebar/queries/update_epic_due_date.mutation.graphql';
import updateEpicStartDateMutation from '~/sidebar/queries/update_epic_start_date.mutation.graphql';
......@@ -120,6 +122,15 @@ export const subscribedQueries = {
},
};
export const timeTrackingQueries = {
[IssuableType.Issue]: {
query: issueTimeTrackingQuery,
},
[IssuableType.MergeRequest]: {
query: mergeRequestTimeTrackingQuery,
},
};
export const dueDateQueries = {
[IssuableType.Issue]: {
query: issueDueDateQuery,
......
......@@ -15,7 +15,7 @@ export default class SidebarMilestone {
humanTimeEstimate,
humanTimeSpent,
limitToHours,
id,
iid,
} = el.dataset;
// eslint-disable-next-line no-new
......@@ -30,12 +30,14 @@ export default class SidebarMilestone {
render: (createElement) =>
createElement('timeTracker', {
props: {
timeEstimate: parseInt(timeEstimate, 10),
timeSpent: parseInt(timeSpent, 10),
humanTimeEstimate,
humanTimeSpent,
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() {
function mountTimeTrackingComponent() {
const el = document.getElementById('issuable-time-tracker');
const { id, issuableType } = getSidebarOptions();
const { iid, fullPath, issuableType, timeTrackingLimitToHours } = getSidebarOptions();
if (!el) return;
......@@ -403,7 +403,9 @@ function mountTimeTrackingComponent() {
render: (createElement) =>
createElement(SidebarTimeTracking, {
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
end
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
expose :milestone, if: -> (issue) { issue.milestone } do |issue|
......
.block.time-tracking
%time-tracker{ ":time-estimate" => "issue.timeEstimate || 0",
":time-spent" => "issue.timeSpent || 0",
":human-time-estimate" => "issue.humanTimeEstimate",
":human-time-spent" => "issue.humanTimeSpent",
":limit-to-hours" => "timeTrackingLimitToHours",
":issuable-id" => "issue.id ? issue.id.toString() : ''",
%time-tracker{ ":limit-to-hours" => "timeTrackingLimitToHours",
":issuable-iid" => "issue.iid ? issue.iid.toString() : ''",
":full-path" => "issue.project ? issue.project.fullPath : ''",
"root-path" => "#{root_url}" }
......@@ -98,7 +98,7 @@
time_spent: @milestone.total_time_spent,
human_time_estimate: @milestone.human_total_time_estimate,
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 } }
= render_if_exists 'shared/milestones/weight', milestone: milestone
......
......@@ -2,22 +2,21 @@
import createFlash from '~/flash';
import { __ } from '~/locale';
import eventHub from '~/sidebar/event_hub';
import Mediator from '../../sidebar_mediator';
import weightComponent from './weight.vue';
export default {
components: {
weight: weightComponent,
},
props: {
mediator: {
required: true,
type: Object,
validator(mediatorObject) {
return mediatorObject.updateWeight && mediatorObject.store;
},
},
data() {
return {
// Defining `mediator` here as a data prop
// makes it reactive for any internal updates
// which wouldn't happen otherwise.
mediator: new Mediator(),
};
},
created() {
eventHub.$on('updateWeight', this.onUpdateWeight);
},
......
......@@ -14,7 +14,7 @@ import { IssuableAttributeType } from './constants';
Vue.use(VueApollo);
const mountWeightComponent = (mediator) => {
const mountWeightComponent = () => {
const el = document.querySelector('.js-sidebar-weight-entry-point');
if (!el) return false;
......@@ -24,12 +24,7 @@ const mountWeightComponent = (mediator) => {
components: {
SidebarWeight,
},
render: (createElement) =>
createElement('sidebar-weight', {
props: {
mediator,
},
}),
render: (createElement) => createElement('sidebar-weight'),
});
};
......@@ -140,7 +135,7 @@ function mountIterationSelect() {
export default function mountSidebar(mediator) {
CEMountSidebar.mountSidebar(mediator);
mountWeightComponent(mediator);
mountWeightComponent();
mountStatusComponent(mediator);
mountEpicsSelect();
mountIterationSelect();
......
......@@ -36,6 +36,7 @@ RSpec.describe 'Issue Sidebar' do
before do
project.add_maintainer(user)
visit_issue(project, issue)
wait_for_all_requests
end
it 'updates weight in sidebar to 1' do
......
......@@ -10,10 +10,8 @@ describe('Sidebar Weight', () => {
let sidebarMediator;
let wrapper;
const createComponent = (props = {}) => {
wrapper = shallowMount(SidebarWeight, {
propsData: { ...props },
});
const createComponent = () => {
wrapper = shallowMount(SidebarWeight);
};
beforeEach(() => {
......
......@@ -18,7 +18,8 @@
"type": "object",
"properties": {
"id": { "type": "integer" },
"path": { "type": "string" }
"path": { "type": "string" },
"path_with_namespace": { "type": "string" }
}
},
"milestone": {
......
......@@ -26,7 +26,7 @@ describe('BoardSidebarTimeTracker', () => {
store = createStore();
store.state.boardItems = {
1: {
id: 1,
iid: 1,
timeEstimate: 3600,
totalTimeSpent: 1800,
humanTimeEstimate: '1h',
......@@ -47,13 +47,16 @@ describe('BoardSidebarTimeTracker', () => {
createComponent({ provide: { timeTrackingLimitToHours } });
expect(wrapper.find(IssuableTimeTracker).props()).toEqual({
timeEstimate: 3600,
timeSpent: 1800,
humanTimeEstimate: '1h',
humanTimeSpent: '30min',
limitToHours: timeTrackingLimitToHours,
showCollapsed: false,
issuableId: '1',
issuableIid: '1',
fullPath: '',
initialTimeTracking: {
timeEstimate: 3600,
totalTimeSpent: 1800,
humanTimeEstimate: '1h',
humanTotalTimeSpent: '30min',
},
});
},
);
......
import { mount } from '@vue/test-utils';
import { stubTransition } from 'helpers/stub_transition';
import { createMockDirective } from 'helpers/vue_mock_directive';
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', () => {
let wrapper;
......@@ -13,16 +17,18 @@ describe('Issuable Time Tracker', () => {
const findReportLink = () => findByTestId('reportLink');
const defaultProps = {
timeEstimate: 10_000, // 2h 46m
timeSpent: 5_000, // 1h 23m
humanTimeEstimate: '2h 46m',
humanTimeSpent: '1h 23m',
limitToHours: false,
issuableId: '1',
fullPath: 'gitlab-org/gitlab-test',
issuableIid: '1',
initialTimeTracking: {
...issuableTimeTrackingResponse.data.workspace.issuable,
},
};
const mountComponent = ({ props = {}, issuableType = 'issue' } = {}) =>
mount(TimeTracker, {
const issuableTimeTrackingRefetchSpy = jest.fn();
const mountComponent = ({ props = {}, issuableType = 'issue', loading = false } = {}) => {
return mount(TimeTracker, {
propsData: { ...defaultProps, ...props },
directives: { GlTooltip: createMockDirective() },
stubs: {
......@@ -31,7 +37,19 @@ describe('Issuable Time Tracker', () => {
provide: {
issuableType,
},
mocks: {
$apollo: {
queries: {
issuableTimeTracking: {
loading,
refetch: issuableTimeTrackingRefetchSpy,
query: jest.fn().mockResolvedValue(issuableTimeTrackingResponse),
},
},
},
},
});
};
afterEach(() => {
wrapper.destroy();
......@@ -48,13 +66,13 @@ describe('Issuable Time Tracker', () => {
it('should correctly render timeEstimate', () => {
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(
defaultProps.humanTimeSpent,
defaultProps.initialTimeTracking.humanTotalTimeSpent,
);
});
});
......@@ -82,10 +100,12 @@ describe('Issuable Time Tracker', () => {
beforeEach(() => {
wrapper = mountComponent({
props: {
timeEstimate: 100_000, // 1d 3h
timeSpent: 5_000, // 1h 23m
humanTimeEstimate: '1d 3h',
humanTimeSpent: '1h 23m',
initialTimeTracking: {
timeEstimate: 100_000, // 1d 3h
totalTimeSpent: 5_000, // 1h 23m
humanTimeEstimate: '1d 3h',
humanTotalTimeSpent: '1h 23m',
},
},
});
});
......@@ -112,8 +132,11 @@ describe('Issuable Time Tracker', () => {
it('should display the remaining meter with the correct background color when over estimate', () => {
wrapper = mountComponent({
props: {
timeEstimate: 10_000, // 2h 46m
timeSpent: 20_000_000, // 231 days
initialTimeTracking: {
...defaultProps.initialTimeTracking,
timeEstimate: 10_000, // 2h 46m
totalTimeSpent: 20_000_000, // 231 days
},
},
});
......@@ -126,8 +149,11 @@ describe('Issuable Time Tracker', () => {
beforeEach(async () => {
wrapper = mountComponent({
props: {
timeEstimate: 100_000, // 1d 3h
limitToHours: true,
initialTimeTracking: {
...defaultProps.initialTimeTracking,
timeEstimate: 100_000, // 1d 3h
},
},
});
});
......@@ -144,10 +170,12 @@ describe('Issuable Time Tracker', () => {
beforeEach(async () => {
wrapper = mountComponent({
props: {
timeEstimate: 10_000, // 2h 46m
timeSpent: 0,
timeEstimateHumanReadable: '2h 46m',
timeSpentHumanReadable: '',
initialTimeTracking: {
timeEstimate: 10_000, // 2h 46m
totalTimeSpent: 0,
humanTimeEstimate: '2h 46m',
humanTotalTimeSpent: '',
},
},
});
await wrapper.vm.$nextTick();
......@@ -163,10 +191,12 @@ describe('Issuable Time Tracker', () => {
beforeEach(() => {
wrapper = mountComponent({
props: {
timeEstimate: 0,
timeSpent: 5_000, // 1h 23m
timeEstimateHumanReadable: '2h 46m',
timeSpentHumanReadable: '1h 23m',
initialTimeTracking: {
timeEstimate: 0,
totalTimeSpent: 5_000, // 1h 23m
humanTimeEstimate: '2h 46m',
humanTotalTimeSpent: '1h 23m',
},
},
});
});
......@@ -181,10 +211,12 @@ describe('Issuable Time Tracker', () => {
beforeEach(() => {
wrapper = mountComponent({
props: {
timeEstimate: 0,
timeSpent: 0,
timeEstimateHumanReadable: '',
timeSpentHumanReadable: '',
initialTimeTracking: {
timeEstimate: 0,
totalTimeSpent: 0,
humanTimeEstimate: '',
humanTotalTimeSpent: '',
},
},
});
});
......@@ -202,8 +234,11 @@ describe('Issuable Time Tracker', () => {
beforeEach(() => {
wrapper = mountComponent({
props: {
timeSpent: 0,
timeSpentHumanReadable: '',
initialTimeTracking: {
...defaultProps.initialTimeTracking,
totalTimeSpent: 0,
humanTotalTimeSpent: '',
},
},
});
});
......@@ -236,7 +271,16 @@ describe('Issuable Time Tracker', () => {
const findCloseHelpButton = () => findByTestId('closeHelpButton');
beforeEach(async () => {
wrapper = mountComponent({ props: { timeEstimate: 0, timeSpent: 0 } });
wrapper = mountComponent({
props: {
initialTimeTracking: {
timeEstimate: 0,
totalTimeSpent: 0,
humanTimeEstimate: '',
humanTotalTimeSpent: '',
},
},
});
await wrapper.vm.$nextTick();
});
......@@ -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 = {
},
};
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;
......@@ -15,7 +15,7 @@ RSpec.describe IssueBoardEntity do
it 'has basic attributes' do
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
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