Commit 76eec000 authored by Paul Slaughter's avatar Paul Slaughter

Merge branch '37102-sidebar-time-tracking-should-be-a-vue-app' into 'master'

Add time tracking feature to boards sidebar

See merge request gitlab-org/gitlab!45490
parents efcc4347 c5fd4fae
......@@ -90,6 +90,7 @@ export default () => {
labelsFetchPath: $boardApp.dataset.labelsFetchPath,
labelsManagePath: $boardApp.dataset.labelsManagePath,
labelsFilterBasePath: $boardApp.dataset.labelsFilterBasePath,
timeTrackingLimitToHours: parseBoolean($boardApp.dataset.timeTrackingLimitToHours),
},
store,
apolloProvider,
......
......@@ -57,7 +57,6 @@ export default {
:human-time-estimate="store.humanTimeEstimate"
:human-time-spent="store.humanTotalTimeSpent"
:limit-to-hours="store.timeTrackingLimitToHours"
:root-path="store.rootPath"
/>
</div>
</template>
......@@ -44,6 +44,21 @@ export default {
default: false,
required: false,
},
/*
In issue list, "time-tracking-collapsed-state" is always rendered even if the sidebar isn't collapsed.
The actual hiding is controlled with css classes:
Hide "time-tracking-collapsed-state"
if .right-sidebar .right-sidebar-collapsed .sidebar-collapsed-icon
Show "time-tracking-collapsed-state"
if .right-sidebar .right-sidebar-expanded .sidebar-collapsed-icon
In Swimlanes sidebar, we do not use collapsed state at all.
*/
showCollapsed: {
type: Boolean,
default: true,
required: false,
},
},
data() {
return {
......@@ -93,8 +108,9 @@ export default {
</script>
<template>
<div v-cloak class="time_tracker time-tracking-component-wrap">
<div v-cloak class="time-tracker time-tracking-component-wrap" data-testid="time-tracker">
<time-tracking-collapsed-state
v-if="showCollapsed"
:show-comparison-state="showComparisonState"
:show-no-time-tracking-state="showNoTimeTrackingState"
:show-help-state="showHelpState"
......@@ -103,7 +119,7 @@ export default {
:time-spent-human-readable="humanTimeSpent"
:time-estimate-human-readable="humanTimeEstimate"
/>
<div class="title hide-collapsed">
<div class="title hide-collapsed gl-mb-3">
{{ __('Time tracking') }}
<div
v-if="!showHelpState"
......
......@@ -808,11 +808,7 @@
}
}
.time_tracker {
padding-bottom: 0;
border-bottom: 0;
.time-tracker {
.sidebar-collapsed-icon {
> .stopwatch-svg {
display: inline-block;
......
......@@ -7,6 +7,7 @@ import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees
import IssuableTitle from '~/boards/components/issuable_title.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import BoardSidebarEpicSelect from './sidebar/board_sidebar_epic_select.vue';
import BoardSidebarTimeTracker from './sidebar/board_sidebar_time_tracker.vue';
import BoardSidebarWeightInput from './sidebar/board_sidebar_weight_input.vue';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
......@@ -17,6 +18,7 @@ export default {
GlDrawer,
IssuableTitle,
BoardSidebarEpicSelect,
BoardSidebarTimeTracker,
BoardSidebarWeightInput,
BoardSidebarLabelsSelect,
},
......@@ -48,6 +50,7 @@ export default {
<template>
<issuable-assignees :users="getActiveIssue.assignees" />
<board-sidebar-epic-select />
<board-sidebar-time-tracker class="swimlanes-sidebar-time-tracker" />
<board-sidebar-weight-input v-if="glFeatures.issueWeights" />
<board-sidebar-labels-select />
</template>
......
<script>
import { mapGetters } from 'vuex';
import IssuableTimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue';
export default {
inject: ['timeTrackingLimitToHours'],
components: {
IssuableTimeTracker,
},
computed: {
...mapGetters(['getActiveIssue']),
},
};
</script>
<template>
<issuable-time-tracker
:time-estimate="getActiveIssue.timeEstimate"
:time-spent="getActiveIssue.totalTimeSpent"
:human-time-estimate="getActiveIssue.humanTimeEstimate"
:human-time-spent="getActiveIssue.humanTotalTimeSpent"
:limit-to-hours="timeTrackingLimitToHours"
:show-collapsed="false"
/>
</template>
......@@ -7,6 +7,9 @@ fragment IssueNode on Issue {
referencePath: reference(full: true)
dueDate
timeEstimate
totalTimeSpent
humanTimeEstimate
humanTotalTimeSpent
weight
confidential
webUrl
......
......@@ -119,3 +119,15 @@ $epic-icons-spacing: 40px;
max-width: calc(100vw - #{$contextual-sidebar-width} - #{$gutter-width} - #{$epic-icons-spacing});
}
}
.swimlanes-sidebar-time-tracker {
.time-tracking-help-state {
margin: 16px -24px 0;
@include gl-px-6;
@include gl-py-2;
@include gl-border-gray-100;
@include gl-border-t-solid;
@include gl-border-t-1;
@include gl-border-b-0;
}
}
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'epics swimlanes sidebar', :js do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, :public, group: group) }
let_it_be(:board) { create(:board, project: project) }
let_it_be(:list) { create(:list, board: board, position: 0) }
let_it_be(:epic1) { create(:epic, group: group) }
let_it_be(:issue1, reload: true) { create(:issue, project: project) }
let_it_be(:epic_issue1, reload: true) { create(:epic_issue, epic: epic1, issue: issue1) }
before do
project.add_maintainer(user)
stub_licensed_features(epics: true, swimlanes: true)
sign_in(user)
visit project_boards_path(project)
wait_for_requests
page.within('.board-swimlanes-toggle-wrapper') do
page.find('.dropdown-toggle').click
page.find('.dropdown-item', text: 'Epic').click
end
wait_for_all_requests
end
context 'time tracking' do
it 'displays time tracking feature with default message' do
click_first_issue_card
page.within('[data-testid="time-tracker"]') do
expect(page).to have_content('Time tracking')
expect(page).to have_content('No estimate or time spent')
end
end
context 'when only spent time is recorded' do
before do
issue1.timelogs.create!(time_spent: 3600, user: user)
click_first_issue_card
end
it 'shows the total time spent only' do
page.within('[data-testid="time-tracker"]') do
expect(page).to have_content('Spent: 1h')
expect(page).not_to have_content('Estimated')
end
end
end
context 'when only estimated time is recorded' do
before do
issue1.update!(time_estimate: 3600)
click_first_issue_card
end
it 'shows the estimated time only' do
page.within('[data-testid="time-tracker"]') do
expect(page).to have_content('Estimated: 1h')
expect(page).not_to have_content('Spent')
end
end
end
context 'when estimated and spent times are available' do
before do
issue1.update!(time_estimate: 3600)
issue1.timelogs.create!(time_spent: 1800, user: user)
click_first_issue_card
end
it 'shows time tracking progress bar' do
page.within('[data-testid="time-tracker"]') do
expect(page).to have_selector('[data-testid="timeTrackingComparisonPane"]')
end
end
it 'shows both estimated and spent time text' do
page.within('[data-testid="time-tracker"]') do
expect(page).to have_content('Spent 30m')
expect(page).to have_content('Est 1h')
end
end
end
context 'when limitedToHours instance option is turned on' do
before do
stub_application_setting(time_tracking_limit_to_hours: true)
# 3600+3600*24 = 1d 1h or 25h
issue1.timelogs.create!(time_spent: 3600 + 3600 * 24, user: user)
click_first_issue_card
end
it 'shows the total time spent only' do
page.within('[data-testid="time-tracker"]') do
expect(page).to have_content('Spent: 25h')
end
end
end
end
def click_first_issue_card
page.within("[data-testid='board-epic-lane-issues']") do
first("[data-testid='board_card']").click
end
end
end
......@@ -19,6 +19,7 @@ describe('ee/BoardContentSidebar', () => {
store,
stubs: {
'board-sidebar-epic-select': '<div></div>',
'board-sidebar-time-tracker': '<div></div>',
'board-sidebar-weight-input': '<div></div>',
'board-sidebar-labels-select': '<div></div>',
},
......
......@@ -11,6 +11,9 @@ describe('ee/BoardContent', () => {
const createComponent = () => {
wrapper = shallowMount(BoardContent, {
store,
provide: {
timeTrackingLimitToHours: false,
},
propsData: {
lists: [],
canAdminList: false,
......
/*
To avoid duplicating tests in time_tracker.spec,
this spec only contains a simple test to check rendering.
A detailed feature spec is used to test time tracking feature
in swimlanes sidebar.
*/
import { shallowMount } from '@vue/test-utils';
import BoardSidebarTimeTracker from 'ee/boards/components/sidebar/board_sidebar_time_tracker.vue';
import IssuableTimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue';
import { createStore } from '~/boards/stores';
describe('BoardSidebarTimeTracker', () => {
let wrapper;
let store;
const createComponent = options => {
wrapper = shallowMount(BoardSidebarTimeTracker, {
store,
...options,
});
};
beforeEach(() => {
store = createStore();
store.state.issues = {
'1': {
timeEstimate: 3600,
totalTimeSpent: 1800,
humanTimeEstimate: '1h',
humanTotalTimeSpent: '30min',
},
};
store.state.activeId = '1';
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it.each([[true], [false]])(
'renders IssuableTimeTracker with correct spent and estimated time (timeTrackingLimitToHours=%s)',
timeTrackingLimitToHours => {
createComponent({ provide: { timeTrackingLimitToHours } });
expect(wrapper.find(IssuableTimeTracker).props()).toEqual({
timeEstimate: 3600,
timeSpent: 1800,
humanTimeEstimate: '1h',
humanTimeSpent: '30min',
limitToHours: timeTrackingLimitToHours,
showCollapsed: false,
});
},
);
});
......@@ -16,7 +16,6 @@ describe('Issuable Time Tracker', () => {
humanTimeEstimate: '2h 46m',
humanTimeSpent: '1h 23m',
limitToHours: false,
rootPath: '/',
};
const mountComponent = ({ props = {} } = {}) =>
......@@ -52,6 +51,24 @@ describe('Issuable Time Tracker', () => {
});
describe('Content panes', () => {
describe('Collapsed state', () => {
it('should render "time-tracking-collapsed-state" by default when "showCollapsed" prop is not specified', () => {
wrapper = mountComponent();
expect(findCollapsedState().exists()).toBe(true);
});
it('should not render "time-tracking-collapsed-state" when "showCollapsed" is false', () => {
wrapper = mountComponent({
props: {
showCollapsed: false,
},
});
expect(findCollapsedState().exists()).toBe(false);
});
});
describe('Comparison pane', () => {
beforeEach(() => {
wrapper = mountComponent({
......
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